diff --git a/CHANGES.txt b/CHANGES.txt index 97c6386821..b4a5206aa3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Port Harry v2 to trunk (CASSANDRA-20200) * Enable filtering of snapshots on keyspace, table and snapshot name in nodetool listsnapshots (CASSANDRA-20151) * Create manifest upon loading where it does not exist or enrich it (CASSANDRA-20150) * Propagate true size of snapshot in SnapshotDetailsTabularData to not call JMX twice in nodetool listsnapshots (CASSANDRA-20149) diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index e3662a6095..113135016d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -351,11 +351,6 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa return operations.staticOperations(); } - public Iterable allOperations() - { - return operations; - } - public Iterable getColumnsWithConditions() { return conditions.getColumns(); diff --git a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java index b462fa3ad5..b81c6a5c4f 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java +++ b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NavigableSet; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -172,6 +173,22 @@ public class QueryController return partition.queryMemtableAndDisk(cfs, executionController); } + private static Runnable getIndexReleaser(Set referencedIndexes) + { + return new Runnable() + { + boolean closed; + @Override + public void run() + { + if (closed) + return; + closed = true; + referencedIndexes.forEach(SSTableIndex::releaseQuietly); + } + }; + } + /** * Build a {@link KeyRangeIterator.Builder} from the given list of {@link Expression}s. *

@@ -200,7 +217,7 @@ public class QueryController expressions = expressions.stream().filter(e -> e.getIndexOperator() != Expression.IndexOperator.ANN).collect(Collectors.toList()); QueryViewBuilder.QueryView queryView = new QueryViewBuilder(expressions, mergeRange).build(); - Runnable onClose = () -> queryView.referencedIndexes.forEach(SSTableIndex::releaseQuietly); + Runnable onClose = getIndexReleaser(queryView.referencedIndexes); KeyRangeIterator.Builder builder = command.rowFilter().isStrict() ? KeyRangeIntersectionIterator.builder(expressions.size(), onClose) : KeyRangeUnionIterator.builder(expressions.size(), onClose); @@ -319,7 +336,7 @@ public class QueryController KeyRangeIterator memtableResults = index.memtableIndexManager().searchMemtableIndexes(queryContext, planExpression, mergeRange); QueryViewBuilder.QueryView queryView = new QueryViewBuilder(Collections.singleton(planExpression), mergeRange).build(); - Runnable onClose = () -> queryView.referencedIndexes.forEach(SSTableIndex::releaseQuietly); + Runnable onClose = getIndexReleaser(queryView.referencedIndexes); try { @@ -359,7 +376,7 @@ public class QueryController // search memtable before referencing sstable indexes; otherwise we may miss newly flushed memtable index KeyRangeIterator memtableResults = index.memtableIndexManager().limitToTopResults(queryContext, sourceKeys, planExpression); QueryViewBuilder.QueryView queryView = new QueryViewBuilder(Collections.singleton(planExpression), mergeRange).build(); - Runnable onClose = () -> queryView.referencedIndexes.forEach(SSTableIndex::releaseQuietly); + Runnable onClose = getIndexReleaser(queryView.referencedIndexes); try { diff --git a/src/java/org/apache/cassandra/utils/ArrayUtils.java b/src/java/org/apache/cassandra/utils/ArrayUtils.java new file mode 100644 index 0000000000..118e230ba1 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/ArrayUtils.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +import java.util.Arrays; + +public class ArrayUtils +{ + public static int hashCode(Object object) + { + if (object.getClass().isArray()) + { + Class klass = object.getClass(); + if (klass == Object[].class) return Arrays.hashCode((Object[]) object); + else if (klass == byte[].class) return Arrays.hashCode((byte[]) object); + else if (klass == char[].class) return Arrays.hashCode((char[]) object); + else if (klass == short[].class) return Arrays.hashCode((short[]) object); + else if (klass == int[].class) return Arrays.hashCode((int[]) object); + else if (klass == long[].class) return Arrays.hashCode((long[]) object); + else if (klass == double[].class) return Arrays.hashCode((double[]) object); + else if (klass == float[].class) return Arrays.hashCode((float[]) object); + else if (klass == boolean[].class) return Arrays.hashCode((boolean[]) object); + else throw new IllegalArgumentException("Unknown type: " + klass); + } + else + { + return object.hashCode(); + } + } + + public static String toString(Object object) + { + if (object.getClass().isArray()) + { + Class klass = object.getClass(); + if (klass == Object[].class) return Arrays.toString((Object[]) object); + else if (klass == byte[].class) return Arrays.toString((byte[]) object); + else if (klass == char[].class) return Arrays.toString((char[]) object); + else if (klass == short[].class) return Arrays.toString((short[]) object); + else if (klass == int[].class) return Arrays.toString((int[]) object); + else if (klass == long[].class) return Arrays.toString((long[]) object); + else if (klass == double[].class) return Arrays.toString((double[]) object); + else if (klass == float[].class) return Arrays.toString((float[]) object); + else if (klass == boolean[].class) return Arrays.toString((boolean[]) object); + else throw new IllegalArgumentException("Unknown type: " + klass); + } + else + { + return object.toString(); + } + } +} diff --git a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java index 156c1c4bc8..73725a6ce3 100644 --- a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java +++ b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java @@ -27,6 +27,8 @@ import java.io.DataInput; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @@ -34,10 +36,23 @@ import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import net.nicoulaj.compilecommand.annotations.Inline; import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.TimestampType; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; @@ -534,6 +549,8 @@ public class ByteBufferUtil return ByteBufferUtil.bytes((float) obj); else if (obj instanceof Double) return ByteBufferUtil.bytes((double) obj); + else if (obj instanceof Boolean) + return BooleanType.instance.decompose((Boolean) obj); else if (obj instanceof UUID) return ByteBufferUtil.bytes((UUID) obj); else if (obj instanceof InetAddress) @@ -544,6 +561,57 @@ public class ByteBufferUtil return ByteBuffer.wrap((byte[]) obj); else if (obj instanceof ByteBuffer) return (ByteBuffer) obj; + else if (obj instanceof BigInteger) + return ByteBuffer.wrap(((BigInteger)obj).toByteArray()); + else if (obj instanceof BigDecimal) + { + BigDecimal cast = (BigDecimal) obj; + BigInteger bi = cast.unscaledValue(); + int scale = cast.scale(); + byte[] bibytes = bi.toByteArray(); + + ByteBuffer bytes = ByteBuffer.allocate(4 + bibytes.length); + bytes.putInt(scale); + bytes.put(bibytes); + bytes.rewind(); + return bytes; + } + else if (obj instanceof List) + { + List list = (List) obj; + // convert subtypes to BB + List bbs = list.stream().map(ByteBufferUtil::objectToBytes).collect(Collectors.toList()); + // decompose/serializer doesn't use the isMultiCell, so safe to do this + return ListType.getInstance(BytesType.instance, false).decompose(bbs); + } + else if (obj instanceof Map) + { + Map map = (Map) obj; + // convert subtypes to BB + Map bbs = new LinkedHashMap<>(); + for (Map.Entry e : map.entrySet()) + { + Object key = e.getKey(); + ByteBuffer previousValue = bbs.put(objectToBytes(key), objectToBytes(e.getValue())); + if (previousValue != null) + throw new IllegalStateException("Key " + key + " already maps to value " + previousValue); + } + // decompose/serializer doesn't use the isMultiCell, so safe to do this + return MapType.getInstance(BytesType.instance, BytesType.instance, false).decompose(bbs); + } + else if (obj instanceof Set) + { + Set set = (Set) obj; + // convert subtypes to BB + Set bbs = new LinkedHashSet<>(); + for (Object o : set) + if (!bbs.add(objectToBytes(o))) + throw new IllegalStateException("Object " + o + " maps to a buffer that already exists in the set"); + // decompose/serializer doesn't use the isMultiCell, so safe to do this + return SetType.getInstance(BytesType.instance, false).decompose(bbs); + } + else if (obj instanceof Date) + return TimestampType.instance.decompose((Date) obj); else throw new IllegalArgumentException(String.format("Cannot convert value %s of type %s", obj, diff --git a/test/distributed/org/apache/cassandra/distributed/Constants.java b/test/distributed/org/apache/cassandra/distributed/Constants.java index cdd8843d8b..04955adbee 100644 --- a/test/distributed/org/apache/cassandra/distributed/Constants.java +++ b/test/distributed/org/apache/cassandra/distributed/Constants.java @@ -42,4 +42,12 @@ public final class Constants public static final String KEY_DTEST_FULL_STARTUP = "dtest.api.startup.full_startup"; public static final String KEY_DTEST_JOIN_RING = "dtest.api.startup.join_ring"; + + /** + * Adds a timeout to instance startup so the tests don't block forever when startup bugs can happen. This should + * almost always be paired with {@link #KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN} as the startup still needs to cleanup. + * + * {@code c.set(Constants.KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN, false); } + */ + public static final String KEY_DTEST_STARTUP_TIMEOUT = "dtest.api.startup.timeout"; } diff --git a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java index feca65208a..7f98f9003d 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java @@ -680,6 +680,10 @@ public abstract class AbstractCluster implements ICluster !i.isShutdown()).findFirst().get(); + } public List get(int... nodes) { if (nodes == null || nodes.length == 0) diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java b/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java index 71cf5aaa5a..20293ad119 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Coordinator.java @@ -28,6 +28,7 @@ import java.util.concurrent.Future; import com.google.common.collect.Iterators; +import accord.utils.Invariants; import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryProcessor; @@ -126,7 +127,8 @@ public class Coordinator implements ICoordinator boundBBValues.add(ByteBufferUtil.objectToBytes(boundValue)); prepared.validate(clientState); - assert prepared instanceof SelectStatement : "Only SELECT statements can be executed with paging"; + Invariants.checkState(prepared instanceof SelectStatement, + "Only SELECT statements can be executed with paging %s", prepared); Dispatcher.RequestTime requestTime = Dispatcher.RequestTime.forImmediateExecution(); SelectStatement selectStatement = (SelectStatement) prepared; diff --git a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java index 313dde0c54..cbb6c42046 100644 --- a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java +++ b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java @@ -43,21 +43,17 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; +import org.apache.cassandra.distributed.api.*; +import org.assertj.core.api.Assertions; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.distributed.api.Feature; -import org.apache.cassandra.distributed.api.ICluster; -import org.apache.cassandra.distributed.api.IInstance; -import org.apache.cassandra.distributed.api.IInstanceConfig; -import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.distributed.api.IMessageFilters; -import org.apache.cassandra.distributed.api.NodeToolResult; +import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.impl.AbstractCluster; import org.apache.cassandra.distributed.impl.InstanceConfig; import org.apache.cassandra.distributed.impl.TestChangeListener; @@ -170,6 +166,33 @@ public class ClusterUtils cluster.stream().forEach(ClusterUtils::stopUnchecked); } + /** + * Create a new instance and add it to the cluster, without starting it. + * + * @param cluster to add to + * @param fn function to add to the config before starting + * @param instance type + * @return the instance added + */ + public static I addInstance(AbstractCluster cluster, Consumer fn) + { + I inst = cluster.stream().filter(i -> !i.isShutdown()).findFirst().get(); + return addInstance(cluster, inst.config(), fn); + } + + /** + * Create a new instance and add it to the cluster, without starting it. + * + * @param cluster to add to + * @param instance type + * @return the instance added + */ + public static I addInstance(AbstractCluster cluster) + { + I inst = cluster.stream().filter(i -> !i.isShutdown()).findFirst().get(); + return addInstance(cluster, inst.config(), c -> {}); + } + /** * Create a new instance and add it to the cluster, without starting it. * @@ -464,11 +487,14 @@ public class ClusterUtils return () -> { try { + logger.info("EpochPause Waiting before enacting epoch {}", epoch); promise.get(wait, waitUnit); + logger.info("EpochPause stopped waiting before enacting epoch {}", epoch); return null; } catch (Throwable e) { + logger.info("EpochPause Timed out waiting for before enacting epoch {}", epoch); throw new RuntimeException(e); } }; @@ -492,11 +518,14 @@ public class ClusterUtils return () -> { try { + logger.info("EpochPause Waiting after enacting epoch {}", epoch); promise.get(wait, waitUnit); + logger.info("EpochPause done waiting after enacting epoch {}", epoch); return null; } catch (Throwable e) { + logger.info("EpochPause Timed out waiting for after enacting epoch {}", epoch); throw new RuntimeException(e); } }; @@ -512,6 +541,7 @@ public class ClusterUtils return () -> { try { + logger.info("EpochPause Waiting before commit"); return promise.get(30, TimeUnit.SECONDS).getEpoch(); } catch (Throwable e) @@ -594,9 +624,9 @@ public class ClusterUtils public String toString() { return "Version{" + - "node=" + node + - ", epoch=" + epoch + - '}'; + "node=" + node + + ", epoch=" + epoch + + '}'; } } @@ -606,7 +636,7 @@ public class ClusterUtils waitForCMSToQuiesce(cluster, maxEpoch(cluster, cmsNodes)); } - private static Epoch maxEpoch(ICluster cluster, int[] cmsNodes) + public static Epoch maxEpoch(ICluster cluster, int[] cmsNodes) { Epoch max = null; for (int id : cmsNodes) @@ -623,6 +653,11 @@ public class ClusterUtils } public static void waitForCMSToQuiesce(ICluster cluster, Epoch awaitedEpoch, int...ignored) + { + waitForCMSToQuiesce(cluster, awaitedEpoch, false, ignored); + } + + public static void waitForCMSToQuiesce(ICluster cluster, Epoch awaitedEpoch, boolean fetchLogWhenBehind, int...ignored) { List notMatching = new ArrayList<>(); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30); @@ -639,9 +674,12 @@ public class ClusterUtils if (skip) continue; - if (cluster.get(j).isShutdown()) + IInvokableInstance inst = cluster.get(j); + if (inst.isShutdown()) continue; - Epoch version = getClusterMetadataVersion(cluster.get(j)); + Epoch version = getClusterMetadataVersion(inst); + if (fetchLogWhenBehind && version.getEpoch() < awaitedEpoch.getEpoch()) + version = fetchLogFromCMS(inst, awaitedEpoch); if (version.getEpoch() < awaitedEpoch.getEpoch()) notMatching.add(new ClusterMetadataVersion(j, version)); } @@ -653,6 +691,17 @@ public class ClusterUtils throw new AssertionError(String.format("Some instances have not reached schema agreement with the leader. Awaited %s; diverging nodes: %s. ", awaitedEpoch, notMatching)); } + public static Epoch fetchLogFromCMS(IInvokableInstance inst, Epoch awaitedEpoch) + { + return fetchLogFromCMS(inst, awaitedEpoch.getEpoch()); + } + + public static Epoch fetchLogFromCMS(IInvokableInstance inst, long awaitedEpoch) + { + long latest = inst.callOnInstance(() -> ClusterMetadataService.instance().fetchLogFromCMS(Epoch.create(awaitedEpoch)).epoch.getEpoch()); + return Epoch.create(latest); + } + public static Epoch getCurrentEpoch(IInvokableInstance inst) { return decode(inst.callOnInstance(() -> encode(ClusterMetadata.current().epoch))); @@ -689,8 +738,8 @@ public class ClusterUtils return epochs; }); return map.entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> decode(e.getValue()))); + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> decode(e.getValue()))); } public static Set getCMSMembers(IInvokableInstance inst) @@ -804,10 +853,9 @@ public class ClusterUtils List match = ring.stream() .filter(d -> d.address.equals(targetAddress)) .collect(Collectors.toList()); - assertThat(match) - .isNotEmpty() - .as("State was expected to be %s but was not", state) - .anyMatch(r -> r.state.equals(state)); + assertThat(match).isNotEmpty() + .as("State was expected to be %s but was not", state) + .anyMatch(r -> r.state.equals(state)); return ring; } @@ -839,7 +887,19 @@ public class ClusterUtils } sleepUninterruptibly(1, TimeUnit.SECONDS); } - throw new AssertionError(errorMessage + "\n" + ring); + throw new AssertionError(errorMessage + "\nsrc=" + src + "\nring=" + ring); + } + + /** + * Wait for the target to be in the ring as seen by the source instances + * @param cluster to check on + * @param nodes in the cluster to check + * @param expectedInRing instance to wait for + */ + public static void awaitRingJoin(Cluster cluster, int[] nodes, IInvokableInstance expectedInRing) + { + for (IInvokableInstance inst : cluster.get(nodes)) + awaitRingJoin(inst, expectedInRing); } /** @@ -883,8 +943,8 @@ public class ClusterUtils public static List awaitRingHealthy(IInstance src) { return awaitRing(src, "Timeout waiting for ring to become healthy", - ring -> - ring.stream().allMatch(ClusterUtils::isRingInstanceDetailsHealthy)); + ring -> + ring.stream().allMatch(ClusterUtils::isRingInstanceDetailsHealthy)); } /** @@ -951,8 +1011,8 @@ public class ClusterUtils public static List assertRingIs(IInstance instance, Collection expectedInRing) { Set expectedRingAddresses = expectedInRing.stream() - .map(i -> i.config().broadcastAddress().getAddress().getHostAddress()) - .collect(Collectors.toSet()); + .map(i -> i.config().broadcastAddress().getAddress().getHostAddress()) + .collect(Collectors.toSet()); return assertRingIs(instance, expectedRingAddresses); } @@ -969,8 +1029,8 @@ public class ClusterUtils List ring = ring(instance); Set ringAddresses = ring.stream().map(d -> d.address).collect(Collectors.toSet()); assertThat(ringAddresses) - .as("Ring addreses did not match for instance %s", instance) - .isEqualTo(expectedRingAddresses); + .as("Ring addreses did not match for instance %s", instance) + .isEqualTo(expectedRingAddresses); return ring; } @@ -1085,9 +1145,9 @@ public class ClusterUtils for (int i = 0; i < 100; i++) { matches = cluster.stream().map(ClusterUtils::gossipInfo) - .map(gi -> Objects.requireNonNull(gi.get(getBroadcastAddressString(expectedInGossip)))) - .map(m -> m.get(key.name())) - .collect(Collectors.toSet()); + .map(gi -> Objects.requireNonNull(gi.get(getBroadcastAddressString(expectedInGossip)))) + .map(m -> m.get(key.name())) + .collect(Collectors.toSet()); if (matches.isEmpty() || matches.size() == 1) return; sleepUninterruptibly(1, TimeUnit.SECONDS); @@ -1342,7 +1402,7 @@ public class ClusterUtils public static InetSocketAddress getNativeInetSocketAddress(IInstance target) { return new InetSocketAddress(target.config().broadcastAddress().getAddress(), - getIntConfig(target.config(), "native_transport_port", 9042)); + getIntConfig(target.config(), "native_transport_port", 9042)); } /** @@ -1445,5 +1505,50 @@ public class ClusterUtils { System.setSecurityManager(new PreventSystemExit()); } + + public static void awaitInPeers(Cluster cluster, int[] nodes, IInstance expectedInPeers) + { + for (IInvokableInstance inst : cluster.get(nodes)) + { + if (inst.config().num() == expectedInPeers.config().num()) continue; // ignore self as self is not in peers + awaitInPeers(inst, expectedInPeers); + } + } + + public static void awaitInPeers(IInstance instance, IInstance expectedInPeers) + { + for (int i = 0; i < 100; i++) + { + if (isInPeers(instance, expectedInPeers)) + return; + sleepUninterruptibly(1, TimeUnit.SECONDS); + } + throw new AssertionError("Unable to find " + expectedInPeers.config().broadcastAddress() + " in peers"); + } + + public static boolean isInPeers(IInstance instance, IInstance expectedInPeers) + { + SimpleQueryResult qr = instance.executeInternalWithResult("select tokens, data_center, rack from system.peers WHERE peer=?", expectedInPeers.config().broadcastAddress().getAddress()); + if (!qr.hasNext()) return false; + Row row = qr.next(); + // peer is known, but is it fully defined? + Collection tokens = row.get("tokens"); + String dc = row.getString("data_center"); + String rack = row.getString("rack"); + return tokens != null && !tokens.isEmpty() && !Strings.isNullOrEmpty(dc) && !Strings.isNullOrEmpty(rack); + } + + public static StorageService.Mode mode(IInvokableInstance inst) + { + String name = inst.callOnInstance(() -> StorageService.instance.operationMode().name()); + return StorageService.Mode.valueOf(name); + } + + public static void assertModeJoined(IInvokableInstance inst) + { + Assertions.assertThat(mode(inst)) + .describedAs("Unexpected StorageService operation mode") + .isEqualTo(StorageService.Mode.NORMAL); + } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/IntegrationTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/IntegrationTestBase.java new file mode 100644 index 0000000000..16f4e58f30 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/IntegrationTestBase.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.util.function.Consumer; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInstanceConfig; + +public class IntegrationTestBase extends TestBaseImpl +{ + protected static final Logger logger = LoggerFactory.getLogger(IntegrationTestBase.class); + protected static Cluster cluster; + + private static boolean initialized = false; + + @BeforeClass + public static void before() throws Throwable + { + init(1, defaultConfig()); + } + + protected static void init(int nodes, Consumer cfg) throws Throwable + { + Invariants.checkState(!initialized); + cluster = Cluster.build() + .withNodes(nodes) + .withConfig(cfg) + .createWithoutStarting(); + cluster.startup(); + cluster = init(cluster); + initialized = true; + } + + @AfterClass + public static void afterClass() + { + if (cluster != null) + cluster.close(); + } + + // TODO: meta-randomize this + public static Consumer defaultConfig() + { + return (cfg) -> { + cfg.set("row_cache_size", "50MiB") + .set("index_summary_capacity", "50MiB") + .set("counter_cache_size", "50MiB") + .set("key_cache_size", "50MiB") + .set("file_cache_size", "50MiB") + .set("index_summary_capacity", "50MiB") + .set("memtable_heap_space", "128MiB") + .set("memtable_offheap_space", "128MiB") + .set("memtable_flush_writers", 1) + .set("concurrent_compactors", 1) + .set("concurrent_reads", 5) + .set("concurrent_writes", 5) + .set("compaction_throughput_mb_per_sec", 10) + .set("hinted_handoff_enabled", false); + }; + } + +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java index 8c188a2fec..3bcac51f64 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java @@ -19,127 +19,153 @@ package org.apache.cassandra.distributed.test.log; import java.io.IOException; +import java.util.Iterator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; import com.google.common.util.concurrent.Uninterruptibles; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Constants; -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.harry.SchemaSpec; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.HistoryBuilderHelper; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.utils.concurrent.CountDownLatch; -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.apache.cassandra.harry.checker.TestHelper.withRandom; import static org.junit.Assert.fail; import static org.psjava.util.AssertStatus.assertTrue; public class BounceGossipTest extends TestBaseImpl { + private static final Logger logger = LoggerFactory.getLogger(BounceGossipTest.class); + @Test public void bounceTest() throws Exception { + Generator schemaGen = SchemaGenerators.schemaSpecGen("bounce_gossip_test", "bounce_test", 1000); try (Cluster cluster = init(builder().withNodes(3) .withConfig(config -> config.with(NETWORK, GOSSIP) .set(Constants.KEY_DTEST_FULL_STARTUP, true)) .start())) { + ExecutorService es = executorFactory().pooled("harry", 1); - 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))); - AtomicInteger down = new AtomicInteger(0); - 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(); + AtomicBoolean stop = new AtomicBoolean(false); - CountDownLatch stopLatch = CountDownLatch.newCountDownLatch(1); - Future f = es.submit(() -> { - try + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), 1000))); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); + + cluster.schemaChange("CREATE KEYSPACE " + schema.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};"); + cluster.schemaChange(schema.compile()); + + Future f = es.submit(new Runnable() { - new FlaggedRunner(config.createRun(), - config, - asList(pool("Writer", 1, MutatingVisitor::new), - pool("Reader", 1, (run) -> new RandomPartitionValidator(run, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP))), - stopLatch).run(); - } - catch (Throwable e) - { - throw new RuntimeException(e); - } - }); - Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); - - down.set(3); - cluster.get(3).shutdown(true).get(); - cluster.get(1).logs().watchFor("/127.0.0.3:.* is now DOWN"); - Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS); - cluster.get(3).startup(); - cluster.get(1).logs().watchFor("/127.0.0.3:.* is now UP"); - down.set(0); - stopLatch.decrement(); - f.get(); - - for (int inst = 1; inst <= 3; inst++) - { - cluster.get(inst).runOnInstance(() -> { - for (int i = 1; i <= 3; i++) + final HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + final Iterator iterator = history.iterator(); + final CQLVisitExecutor executor = InJvmDTestVisitExecutor.builder() + .nodeSelector(lts -> 1) + .retryPolicy(InJvmDTestVisitExecutor.RetryPolicy.RETRY_ON_TIMEOUT) + .build(schema, history, cluster); + @Override + public void run() { - boolean stateOk = false; - int tries = 0; - while (!stateOk) + while (!stop.get()) { - EndpointState epstate = Gossiper.instance.getEndpointStateForEndpoint(InetAddressAndPort.getByNameUnchecked("127.0.0." + i)); - stateOk = epstate.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.contains("NORMAL") && - epstate.getApplicationState(ApplicationState.TOKENS) != null && - epstate.getHeartBeatState().getGeneration() > 0; - if (!stateOk) + // Rate limit to ~10 per second + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); + HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history); + history.selectPartition(pkGen.generate(rng)); + + while (iterator.hasNext() && !stop.get()) { - tries++; - if (tries > 20) + try { - assertTrue(epstate.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.contains("NORMAL")); - assertTrue(epstate.getApplicationState(ApplicationState.TOKENS) != null); - assertTrue(epstate.getHeartBeatState().getGeneration() > 0); - fail("shouldn't reach this, but epstate: "+epstate); + executor.execute(iterator.next()); + } + catch (Throwable e) + { + if (e.getMessage().contains("Can't use shutdown node3")) + { + // ignore and continue + continue; + } + + if (e.getMessage().contains("does not exist")) + { + logger.error(e.getMessage()); + // ignore and continue + continue; + } + throw e; } - Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); } } } }); + Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); - } + cluster.get(3).shutdown(true).get(); + cluster.get(1).logs().watchFor("/127.0.0.3:.* is now DOWN"); + Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS); + cluster.get(3).startup(); + cluster.get(1).logs().watchFor("/127.0.0.3:.* is now UP"); + stop.set(true); + f.get(); + for (int inst = 1; inst <= 3; inst++) + { + cluster.get(inst).runOnInstance(() -> { + for (int i = 1; i <= 3; i++) + { + boolean stateOk = false; + int tries = 0; + while (!stateOk) + { + EndpointState epstate = Gossiper.instance.getEndpointStateForEndpoint(InetAddressAndPort.getByNameUnchecked("127.0.0." + i)); + stateOk = epstate.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.contains("NORMAL") && + epstate.getApplicationState(ApplicationState.TOKENS) != null && + epstate.getHeartBeatState().getGeneration() > 0; + if (!stateOk) + { + tries++; + if (tries > 20) + { + assertTrue(epstate.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.contains("NORMAL")); + assertTrue(epstate.getApplicationState(ApplicationState.TOKENS) != null); + assertTrue(epstate.getHeartBeatState().getGeneration() > 0); + fail("shouldn't reach this, but epstate: " + epstate); + } + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + } + } + } + }); + } + }); } } @@ -157,7 +183,7 @@ public class BounceGossipTest extends TestBaseImpl int correctTokensVersion; while ((correctTokensVersion = getGossipTokensVersion(cluster, 2)) == tokensBefore) Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); // wait for LegacyStateListener to actually update gossip - for (int inst : new int[] {1, 3}) + for (int inst : new int[]{ 1, 3 }) while (correctTokensVersion != getGossipTokensVersion(cluster, inst)) { System.out.println(correctTokensVersion + " ::: " + getGossipTokensVersion(cluster, inst)); @@ -172,75 +198,4 @@ public class BounceGossipTest extends TestBaseImpl return cluster.get(instance).callOnInstance(() -> Gossiper.instance.endpointStateMap.get(InetAddressAndPort.getByNameUnchecked("127.0.0.2")) .getApplicationState(ApplicationState.TOKENS).version); } - - private static String createHarryConf() - { - return "seed: " + currentTimeMillis() + "\n" + - "\n" + - "# Default schema provider generates random schema\n" + - "schema_provider:\n" + - " fixed:\n" + - " keyspace: harry\n" + - " table: test_table\n" + - " partition_keys:\n" + - " pk1: bigint\n" + - " pk2: ascii\n" + - " clustering_keys:\n" + - " ck1: ascii\n" + - " ck2: bigint\n" + - " regular_columns:\n" + - " v1: ascii\n" + - " v2: bigint\n" + - " v3: ascii\n" + - " v4: bigint\n" + - " static_keys:\n" + - " s1: ascii\n" + - " s2: bigint\n" + - " s3: ascii\n" + - " s4: bigint\n" + - "\n" + - "clock:\n" + - " offset:\n" + - " offset: 1000\n" + - "\n" + - "drop_schema: false\n" + - "create_schema: true\n" + - "truncate_table: true\n" + - "\n" + - "partition_descriptor_selector:\n" + - " default:\n" + - " window_size: 10\n" + - " slide_after_repeats: 100\n" + - "\n" + - "clustering_descriptor_selector:\n" + - " default:\n" + - " modifications_per_lts:\n" + - " type: \"constant\"\n" + - " constant: 2\n" + - " rows_per_modification:\n" + - " type: \"constant\"\n" + - " constant: 2\n" + - " operation_kind_weights:\n" + - " DELETE_RANGE: 0\n" + - " DELETE_SLICE: 0\n" + - " DELETE_ROW: 0\n" + - " DELETE_COLUMN: 0\n" + - " DELETE_PARTITION: 0\n" + - " DELETE_COLUMN_WITH_STATICS: 0\n" + - " INSERT_WITH_STATICS: 50\n" + - " INSERT: 50\n" + - " UPDATE_WITH_STATICS: 50\n" + - " UPDATE: 50\n" + - " column_mask_bitsets: null\n" + - " max_partition_size: 1000\n" + - "\n" + - "metric_reporter:\n" + - " no_op: {}\n" + - "\n" + - "data_tracker:\n" + - " locking:\n" + - " max_seen_lts: -1\n" + - " max_complete_lts: -1"; - } - -} +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java index 538bf4f491..c89b52c047 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java @@ -24,7 +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.harry.model.TokenPlacementModel; import org.apache.cassandra.locator.MetaStrategy; import org.apache.cassandra.schema.DistributedSchema; import org.apache.cassandra.schema.KeyspaceMetadata; diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java index 388185e8d2..42630b3a29 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java @@ -27,29 +27,32 @@ import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.cassandra.harry.sut.TokenPlacementModel.Replica; 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.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.harry.HarryHelper; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.ValueGeneratorHelper; +import org.apache.cassandra.harry.cql.WriteHelper; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.model.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel.Replica; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.util.ByteUtils; +import org.apache.cassandra.harry.util.TokenUtil; 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.harry.checker.TestHelper.withRandom; + public class CoordinatorPathTest extends CoordinatorPathTestBase { private static final TokenPlacementModel.SimpleReplicationFactor RF = new TokenPlacementModel.SimpleReplicationFactor(3); @@ -57,11 +60,8 @@ public class CoordinatorPathTest extends CoordinatorPathTestBase @Test public void writeConsistencyTest() throws Throwable { + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "write_consistency_test", 1000); coordinatorPathTest(RF, (cluster, simulatedCluster) -> { - Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() - .setSUT(() -> new InJvmSut(cluster)); - Run run = configBuilder.build().createRun(); - for (int ignored : new int[]{ 2, 3, 4, 5 }) simulatedCluster.createNode().register(); @@ -75,68 +75,72 @@ public class CoordinatorPathTest extends CoordinatorPathTestBase .prepareJoin() .startJoin(); - cluster.schemaChange("CREATE KEYSPACE " + run.schemaSpec.keyspace + - " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};"); - cluster.schemaChange(run.schemaSpec.compile().cql()); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + cluster.schemaChange("CREATE KEYSPACE " + schema.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};"); + cluster.schemaChange(schema.compile()); - while (true) - { - long lts = run.clock.nextLts(); - - long pd = run.pdSelector.pd(run.clock.nextLts(), run.schemaSpec); - - ByteBuffer[] pk = ByteUtils.objectsToBytes(run.schemaSpec.inflatePartitionKey(pd)); - long token = TokenUtil.token(ByteUtils.compose(pk)); - if (!prediction.state.get().isWriteTargetFor(token, prediction.node(6).matcher)) - continue; - - simulatedCluster.waitForQuiescense(); - List replicas = simulatedCluster.state.get().writePlacementsFor(token); - // At most 2 replicas should respond, so that when the pending node is added, results would be insufficient for recomputed blockFor - BooleanSupplier shouldRespond = atMostResponses(simulatedCluster.state.get().isWriteTargetFor(token, simulatedCluster.node(1).matcher) ? 1 : 2); - List> waiting = simulatedCluster - .filter((n) -> replicas.stream().map(Replica::node).anyMatch(n.matcher) && n.node.idx() != 1) - .map((nodeToBlockOn) -> nodeToBlockOn.blockOnReplica((node) -> new MutationAction(node, shouldRespond))) - .collect(Collectors.toList()); - - Future writeQuery = async(() -> { - long cd = run.descriptorSelector.cd(pd, lts, 0, run.schemaSpec); - CompiledStatement s = WriteHelper.inflateInsert(run.schemaSpec, - pd, - cd, - run.descriptorSelector.vds(pd, cd, lts, 0, OpSelectors.OperationKind.INSERT_WITH_STATICS, run.schemaSpec), - run.descriptorSelector.sds(pd, cd, lts, 0, OpSelectors.OperationKind.INSERT_WITH_STATICS, run.schemaSpec), - run.clock.rts(lts)); - cluster.coordinator(1).execute(s.cql(), ConsistencyLevel.QUORUM, s.bindings()); - return null; - }); - - waiting.forEach(WaitingAction::waitForMessage); - - simulatedCluster.createNode().register(); - simulatedCluster.node(6) - .lazyJoin() - .prepareJoin() - .startJoin(); - - simulatedCluster.waitForQuiescense(); - - waiting.forEach(WaitingAction::resume); - - try + HistoryBuilder.IndexedValueGenerators valueGenerators = (HistoryBuilder.IndexedValueGenerators) schema.valueGenerators; + for (int i = 0; i < valueGenerators.pkPopulation(); i++) { - writeQuery.get(); - Assert.fail("Should have thrown"); + long pd = valueGenerators.pkGen().descriptorAt(i); + + ByteBuffer[] pk = ByteUtils.objectsToBytes(valueGenerators.pkGen().inflate(pd)); + long token = TokenUtil.token(ByteUtils.compose(pk)); + if (!prediction.state.get().isWriteTargetFor(token, prediction.node(6).matcher)) + continue; + + simulatedCluster.waitForQuiescense(); + List replicas = simulatedCluster.state.get().writePlacementsFor(token); + // At most 2 replicas should respond, so that when the pending node is added, results would be insufficient for recomputed blockFor + BooleanSupplier shouldRespond = atMostResponses(simulatedCluster.state.get().isWriteTargetFor(token, simulatedCluster.node(1).matcher) ? 1 : 2); + List> waiting = simulatedCluster + .filter((n) -> replicas.stream().map(Replica::node).anyMatch(n.matcher) && n.node.idx() != 1) + .map((nodeToBlockOn) -> nodeToBlockOn.blockOnReplica((node) -> new MutationAction(node, shouldRespond))) + .collect(Collectors.toList()); + + long lts = 1L; + Future writeQuery = async(() -> { + + CompiledStatement s = WriteHelper.inflateInsert(new Operations.WriteOp(lts, pd, 0, + ValueGeneratorHelper.randomDescriptors(rng, valueGenerators::regularColumnGen, valueGenerators.regularColumnCount()), + ValueGeneratorHelper.randomDescriptors(rng, valueGenerators::staticColumnGen, valueGenerators.staticColumnCount()), + Operations.Kind.INSERT), + schema, + lts); + cluster.coordinator(1).execute(s.cql(), ConsistencyLevel.QUORUM, s.bindings()); + return null; + }); + + waiting.forEach(WaitingAction::waitForMessage); + + simulatedCluster.createNode().register(); + simulatedCluster.node(6) + .lazyJoin() + .prepareJoin() + .startJoin(); + + simulatedCluster.waitForQuiescense(); + + waiting.forEach(WaitingAction::resume); + + try + { + writeQuery.get(); + Assert.fail("Should have thrown"); + } + catch (Throwable t) + { + if (t.getMessage() == null) + throw t; + Assert.assertTrue("Expected a different error message, but got " + t.getMessage(), + t.getMessage().contains("the ring has changed")); + return; + } } - catch (Throwable t) - { - if (t.getMessage() == null) - throw t; - Assert.assertTrue("Expected a different error message, but got " + t.getMessage(), - t.getMessage().contains("the ring has changed")); - return; - } - } + + }); }); } @@ -253,4 +257,4 @@ public class CoordinatorPathTest extends CoordinatorPathTestBase }); } -} +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java index af9f1806bf..cfa0ec4be7 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java @@ -64,11 +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.model.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel.Node; +import org.apache.cassandra.harry.model.TokenPlacementModel.NodeFactory; import org.apache.cassandra.harry.util.ByteUtils; -import org.apache.cassandra.harry.util.TestRunner; +import org.apache.cassandra.harry.dsl.TestRunner; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.SimpleSeedProvider; import org.apache.cassandra.tcm.*; diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java index 5dadb0ed01..a5f88d9247 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java @@ -22,46 +22,43 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; 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.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; import net.bytebuddy.implementation.bind.annotation.SuperCall; 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.harry.HarryHelper; import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.shared.ClusterUtils.SerializableBiPredicate; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.streaming.IncomingStream; +import org.apache.cassandra.streaming.StreamReceiveTask; import org.apache.cassandra.tcm.Commit; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tcm.Transformation; import org.apache.cassandra.tcm.transformations.CancelInProgressSequence; import org.apache.cassandra.tcm.transformations.PrepareLeave; -import org.apache.cassandra.streaming.IncomingStream; -import org.apache.cassandra.streaming.StreamReceiveTask; import static net.bytebuddy.matcher.ElementMatchers.named; import static org.apache.cassandra.distributed.shared.ClusterUtils.cancelInProgressSequences; import static org.apache.cassandra.distributed.shared.ClusterUtils.decommission; import static org.apache.cassandra.distributed.shared.ClusterUtils.getClusterMetadataVersion; import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; public class FailedLeaveTest extends FuzzTestBase { @@ -89,7 +86,7 @@ public class FailedLeaveTest extends FuzzTestBase SerializableBiPredicate actionCommitted) throws Exception { - ExecutorService es = Executors.newSingleThreadExecutor(); + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "failed_leave_test", 1000); try (Cluster cluster = builder().withNodes(3) .withInstanceInitializer(BB::install) .appendConfig(c -> c.with(Feature.NETWORK)) @@ -98,64 +95,72 @@ public class FailedLeaveTest extends FuzzTestBase IInvokableInstance cmsInstance = cluster.get(1); IInvokableInstance leavingInstance = cluster.get(2); - Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() - .setSUT(() -> new InJvmSut(cluster)); - Run run = configBuilder.build().createRun(); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), 1000))); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); - cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + - " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", - ConsistencyLevel.ALL); - cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); - ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); + HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + (hb) -> InJvmDTestVisitExecutor.builder() + .nodeSelector(i -> 1) + .build(schema, hb, cluster)); + history.custom(() -> { + cluster.schemaChange("CREATE KEYSPACE " + schema.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};"); + cluster.schemaChange(schema.compile()); + }, "Setup"); - 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(); + Runnable writeAndValidate = () -> { + for (int i = 0; i < WRITES; i++) + history.insert(pkGen.generate(rng), ckGen.generate(rng)); - Epoch startEpoch = getClusterMetadataVersion(cmsInstance); - // Configure node 3 to fail when receiving streams, then start decommissioning node 2 - cluster.get(3).runOnInstance(() -> BB.failReceivingStream.set(true)); - Future success = es.submit(() -> decommission(leavingInstance)); - Assert.assertFalse(success.get()); + for (int pk : pkGen.generated()) + history.selectPartition(pk); + }; + writeAndValidate.run(); - // metadata event log should have advanced by 2 entries, PREPARE_LEAVE & START_LEAVE - ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); - Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); - Assert.assertEquals(startEpoch.getEpoch() + 2, currentEpoch.getEpoch()); + history.customThrowing(() -> { + ExecutorService es = Executors.newSingleThreadExecutor(); + Epoch startEpoch = getClusterMetadataVersion(cmsInstance); + // Configure node 3 to fail when receiving streams, then start decommissioning node 2 + cluster.get(3).runOnInstance(() -> BB.failReceivingStream.set(true)); + Future success = es.submit(() -> decommission(leavingInstance)); + Assert.assertFalse(success.get()); - // Node 2's leaving failed due to the streaming errors. If decommission is called again on the node, it should - // resume where it left off. Allow streaming to succeed this time and verify that the node is able to - // finish leaving. - cluster.get(3).runOnInstance(() -> BB.failReceivingStream.set(false)); + // metadata event log should have advanced by 2 entries, PREPARE_LEAVE & START_LEAVE + ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); + Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); + Assert.assertEquals(startEpoch.getEpoch() + 2, currentEpoch.getEpoch()); - // Run the desired action to mitigate the failure (i.e. retry or cancel) - success = runAfterFailure.apply(es, leavingInstance); + // Node 2's leaving failed due to the streaming errors. If decommission is called again on the node, it should + // resume where it left off. Allow streaming to succeed this time and verify that the node is able to + // finish leaving. + cluster.get(3).runOnInstance(() -> BB.failReceivingStream.set(false)); - // get the Epoch of the event resulting from that action, so we can wait for it - Epoch nextEpoch = getSequenceAfterCommit(cmsInstance, actionCommitted).call(); + // Run the desired action to mitigate the failure (i.e. retry or cancel) + success = runAfterFailure.apply(es, leavingInstance); - Assert.assertTrue(success.get()); + // get the Epoch of the event resulting from that action, so we can wait for it + Epoch nextEpoch = getSequenceAfterCommit(cmsInstance, actionCommitted).call(); - // wait for the cluster to all witness the event submitted after failure - // (i.e. the FINISH_JOIN or CANCEL_SEQUENCE). - ClusterUtils.waitForCMSToQuiesce(cluster, nextEpoch); + Assert.assertTrue(success.get()); - //validate the state of the cluster - for (int i = 0; i < WRITES; i++) - visitor.visit(); - model.validateAll(); + es.shutdown(); + es.awaitTermination(1, TimeUnit.MINUTES); + // wait for the cluster to all witness the event submitted after failure + // (i.e. the FINISH_JOIN or CANCEL_SEQUENCE). + ClusterUtils.waitForCMSToQuiesce(cluster, nextEpoch); + }, "Failed leave"); + + writeAndValidate.run(); + }); } } - public static class BB { static AtomicBoolean failReceivingStream = new AtomicBoolean(false); + public static void install(ClassLoader cl, int instance) { if (instance == 3) @@ -175,4 +180,4 @@ public class FailedLeaveTest extends FuzzTestBase zuper.call(); } } -} +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java index 86e5f681e1..48df35dd76 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java @@ -24,7 +24,15 @@ import org.junit.BeforeClass; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.test.TestBaseImpl; -import org.apache.cassandra.harry.HarryHelper; + +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; public class FuzzTestBase extends TestBaseImpl { @@ -32,9 +40,23 @@ public class FuzzTestBase extends TestBaseImpl public static void beforeClass() throws Throwable { TestBaseImpl.beforeClass(); - HarryHelper.init(); + init(); } + public static void init() + { + // setting both ways as changes between versions + LOG4J2_DISABLE_JMX.setBoolean(true); + LOG4J2_DISABLE_JMX_LEGACY.setBoolean(true); + LOG4J_SHUTDOWN_HOOK_ENABLED.setBoolean(false); + CASSANDRA_ALLOW_SIMPLE_STRATEGY.setBoolean(true); + CASSANDRA_MINIMUM_REPLICATION_FACTOR.setInt(0); + DISABLE_TCACTIVE_OPENSSL.setBoolean(true); + IO_NETTY_TRANSPORT_NONATIVE.setBoolean(true); + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + } + + @Override public Cluster.Builder builder() { return super.builder() diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java index 7e2d1880a2..414f74bbef 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java @@ -49,8 +49,7 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.harry.checker.ModelChecker; import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.sut.TokenPlacementModel.DCReplicas; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.locator.CMSPlacementStrategy; import org.apache.cassandra.locator.EndpointsForRange; import org.apache.cassandra.locator.InetAddressAndPort; @@ -73,12 +72,13 @@ import org.apache.cassandra.tcm.transformations.Register; import org.apache.cassandra.tcm.transformations.TriggerSnapshot; import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.Node; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.NtsReplicationFactor; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.ReplicationFactor; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.SimpleReplicationFactor; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.nodeFactory; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.nodeFactoryHumanReadable; +import static org.apache.cassandra.harry.model.TokenPlacementModel.DCReplicas; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Node; +import static org.apache.cassandra.harry.model.TokenPlacementModel.NtsReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.ReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.SimpleReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.nodeFactory; +import static org.apache.cassandra.harry.model.TokenPlacementModel.nodeFactoryHumanReadable; public class MetadataChangeSimulationTest extends CMSTestBase { diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataKeysTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataKeysTest.java index 04acd2369a..acc013aff0 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataKeysTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataKeysTest.java @@ -33,7 +33,7 @@ import org.apache.cassandra.harry.gen.EntropySource; import org.apache.cassandra.harry.gen.Generator; import org.apache.cassandra.harry.gen.Generators; import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.tcm.AtomicLongBackedProcessor; import org.apache.cassandra.tcm.ClusterMetadata; diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java b/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java index 3ad03fb6a0..0eb73b8694 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java @@ -28,8 +28,10 @@ import java.util.Random; import java.util.Set; import java.util.TreeMap; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.sut.TokenPlacementModel.DCReplicas; +import static org.apache.cassandra.harry.model.TokenPlacementModel.DCReplicas; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Node; +import static org.apache.cassandra.harry.model.TokenPlacementModel.NodeFactory; +import static org.apache.cassandra.harry.model.TokenPlacementModel.ReplicationFactor; public class ModelState { @@ -40,17 +42,17 @@ public class ModelState public final int[] cancelled; public final int[] finished; public final int bootstrappingCount; - public final List currentNodes; - public final Map> nodesByDc; - public final List registeredNodes; - public final List leavingNodes; - public final List movingNodes; + public final List currentNodes; + public final Map> nodesByDc; + public final List registeredNodes; + public final List leavingNodes; + public final List movingNodes; public final List inFlightOperations; public final PlacementSimulator.SimulatedPlacements simulatedPlacements; - public final TokenPlacementModel.NodeFactory nodeFactory; + public final NodeFactory nodeFactory; - public static ModelState empty(TokenPlacementModel.NodeFactory nodeFactory, int maxClusterSize, int maxConcurrency) + public static ModelState empty(NodeFactory nodeFactory, int maxClusterSize, int maxConcurrency) { return new ModelState(maxClusterSize, maxConcurrency, 0, 0, @@ -61,11 +63,11 @@ public class ModelState nodeFactory); } - public static Map> groupByDc(List nodes) + public static Map> groupByDc(List nodes) { // using treemap here since it is much easier to read/debug when it comes to that - Map> grouped = new TreeMap<>(); - for (TokenPlacementModel.Node node : nodes) + Map> grouped = new TreeMap<>(); + for (Node node : nodes) { grouped.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()) .add(node); @@ -79,13 +81,13 @@ public class ModelState int rejected, int[] cancelled, int[] finished, - List currentNodes, - List registeredNodes, - List leavingNodes, - List movingNodes, + List currentNodes, + List registeredNodes, + List leavingNodes, + List movingNodes, List operationStates, PlacementSimulator.SimulatedPlacements simulatedPlacements, - TokenPlacementModel.NodeFactory nodeFactory) + NodeFactory nodeFactory) { this.maxClusterSize = maxClusterSize; this.maxConcurrency = maxConcurrency; @@ -121,30 +123,30 @@ public class ModelState return withinConcurrencyLimit() && bootstrappingCount + currentNodes.size() < maxClusterSize; } - public boolean shouldLeave(TokenPlacementModel.ReplicationFactor rf, Random rng) + public boolean shouldLeave(ReplicationFactor rf, Random rng) { return canRemove(rf) && rng.nextDouble() > 0.7; } - public boolean shouldMove(TokenPlacementModel.ReplicationFactor rf, Random rng) + public boolean shouldMove(ReplicationFactor rf, Random rng) { return canRemove(rf) && rng.nextDouble() > 0.7; } - public boolean shouldReplace(TokenPlacementModel.ReplicationFactor rf, Random rng) + public boolean shouldReplace(ReplicationFactor rf, Random rng) { return canRemove(rf) && rng.nextDouble() > 0.8; } - private boolean canRemove(TokenPlacementModel.ReplicationFactor rfs) + private boolean canRemove(ReplicationFactor rfs) { if (!withinConcurrencyLimit()) return false; for (Map.Entry e : rfs.asMap().entrySet()) { String dc = e.getKey(); int rf = e.getValue().totalCount; - List nodes = nodesByDc.get(dc); - Set nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes); + List nodes = nodesByDc.get(dc); + Set nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes); for (SimulatedOperation op : inFlightOperations) nodesInDc.removeAll(Arrays.asList(op.nodes)); if (nodesInDc.size() > rf) @@ -182,13 +184,13 @@ public class ModelState // join/replace/leave/move private int[] cancelled; private int[] finished; - private List currentNodes; - private List registeredNodes; - private List leavingNodes; - private List movingNodes; + private List currentNodes; + private List registeredNodes; + private List leavingNodes; + private List movingNodes; private List operationStates; private PlacementSimulator.SimulatedPlacements simulatedPlacements; - private TokenPlacementModel.NodeFactory nodeFactory; + private NodeFactory nodeFactory; private Transformer(ModelState source) { @@ -257,26 +259,26 @@ public class ModelState return this; } - public Transformer withJoined(TokenPlacementModel.Node node) + public Transformer withJoined(Node node) { addToCluster(node); finished[0]++; return this; } - public Transformer recycleRejected(TokenPlacementModel.Node node) + public Transformer recycleRejected(Node node) { registeredNodes = new ArrayList<>(registeredNodes); registeredNodes.add(node); return this; } - public Transformer withMoved(TokenPlacementModel.Node movingNode, TokenPlacementModel.Node movedTo) + public Transformer withMoved(Node movingNode, Node movedTo) { assert currentNodes.contains(movingNode) : movingNode; - List tmp = currentNodes; + List tmp = currentNodes; currentNodes = new ArrayList<>(); - for (TokenPlacementModel.Node n : tmp) + for (Node n : tmp) { if (n.idx() == movingNode.idx()) currentNodes.add(movedTo); @@ -291,14 +293,14 @@ public class ModelState return this; } - private void addToCluster(TokenPlacementModel.Node node) + private void addToCluster(Node node) { // called during both join and replacement currentNodes = new ArrayList<>(currentNodes); currentNodes.add(node); } - public Transformer markMoving(TokenPlacementModel.Node moving) + public Transformer markMoving(Node moving) { assert currentNodes.contains(moving); movingNodes = new ArrayList<>(movingNodes); @@ -306,7 +308,7 @@ public class ModelState return this; } - public Transformer markLeaving(TokenPlacementModel.Node leaving) + public Transformer markLeaving(Node leaving) { assert currentNodes.contains(leaving); leavingNodes = new ArrayList<>(leavingNodes); @@ -314,7 +316,7 @@ public class ModelState return this; } - public Transformer withLeft(TokenPlacementModel.Node node) + public Transformer withLeft(Node node) { assert currentNodes.contains(node); // for now... assassinate may change this assertion @@ -324,7 +326,7 @@ public class ModelState return this; } - private void removeFromCluster(TokenPlacementModel.Node node) + private void removeFromCluster(Node node) { // called during both decommission and replacement currentNodes = new ArrayList<>(currentNodes); @@ -333,7 +335,7 @@ public class ModelState leavingNodes.remove(node); } - public Transformer withReplaced(TokenPlacementModel.Node oldNode, TokenPlacementModel.Node newNode) + public Transformer withReplaced(Node oldNode, Node newNode) { addToCluster(newNode); removeFromCluster(oldNode); diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java index 6e055b257c..c0ce61226b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java @@ -29,7 +29,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.Murmur3Partitioner; import static org.apache.cassandra.distributed.test.log.MetadataChangeSimulationTest.simulate; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.NtsReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.NtsReplicationFactor; public class NTSSimulationTest extends CMSTestBase { diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java index 85519006cf..2f3e9f81ef 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java @@ -30,7 +30,6 @@ 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.tcm.AtomicLongBackedProcessor; @@ -44,6 +43,13 @@ import org.apache.cassandra.tcm.sequences.Move; import org.apache.cassandra.tcm.transformations.Register; import org.apache.cassandra.tcm.transformations.UnsafeJoin; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Node; +import static org.apache.cassandra.harry.model.TokenPlacementModel.NodeFactory; +import static org.apache.cassandra.harry.model.TokenPlacementModel.NtsReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.ReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.SimpleReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.nodeFactory; + /** * Compare different operations, and make sure that executing operations such as move, bootstrap, etc., * is consistent with bootstrapping nodes with equivalent token ownership. Useful for testing operations @@ -61,30 +67,30 @@ public class OperationalEquivalenceTest extends CMSTestBase @Test public void testMove() throws Exception { - testMove(new TokenPlacementModel.SimpleReplicationFactor(2)); - testMove(new TokenPlacementModel.SimpleReplicationFactor(3)); - testMove(new TokenPlacementModel.SimpleReplicationFactor(5)); + testMove(new SimpleReplicationFactor(2)); + testMove(new SimpleReplicationFactor(3)); + testMove(new SimpleReplicationFactor(5)); - testMove(new TokenPlacementModel.NtsReplicationFactor(1, 2)); - testMove(new TokenPlacementModel.NtsReplicationFactor(1, 3)); - testMove(new TokenPlacementModel.NtsReplicationFactor(1, 5)); + testMove(new NtsReplicationFactor(1, 2)); + testMove(new NtsReplicationFactor(1, 3)); + testMove(new NtsReplicationFactor(1, 5)); - testMove(new TokenPlacementModel.NtsReplicationFactor(3, 2)); - testMove(new TokenPlacementModel.NtsReplicationFactor(3, 3)); - testMove(new TokenPlacementModel.NtsReplicationFactor(3, 5)); + testMove(new NtsReplicationFactor(3, 2)); + testMove(new NtsReplicationFactor(3, 3)); + testMove(new NtsReplicationFactor(3, 5)); - testMove(new TokenPlacementModel.SimpleReplicationFactor(3, 1)); - testMove(new TokenPlacementModel.SimpleReplicationFactor(3, 2)); - testMove(new TokenPlacementModel.NtsReplicationFactor(3, 3, 1)); - testMove(new TokenPlacementModel.NtsReplicationFactor(3, 5, 2)); + testMove(new SimpleReplicationFactor(3, 1)); + testMove(new SimpleReplicationFactor(3, 2)); + testMove(new NtsReplicationFactor(3, 3, 1)); + testMove(new NtsReplicationFactor(3, 5, 2)); } - public void testMove(TokenPlacementModel.ReplicationFactor rf) throws Exception + public void testMove(ReplicationFactor rf) throws Exception { - TokenPlacementModel.NodeFactory nodeFactory = TokenPlacementModel.nodeFactory(); + NodeFactory nodeFactory = nodeFactory(); ClusterMetadata withMove = null; - List equivalentNodes = new ArrayList<>(); + List equivalentNodes = new ArrayList<>(); int nodes = 30; try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf)) { @@ -92,14 +98,14 @@ public class OperationalEquivalenceTest extends CMSTestBase for (int i = 0; i < nodes; i++) { int dc = toDc(i, rf); - TokenPlacementModel.Node node = nodeFactory.make(counter.incrementAndGet(), dc, 1); + 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); } - TokenPlacementModel.Node toMove = equivalentNodes.get(rng.nextInt(equivalentNodes.size())); - TokenPlacementModel.Node moved = toMove.withNewToken(); + Node toMove = equivalentNodes.get(rng.nextInt(equivalentNodes.size())); + Node moved = toMove.withNewToken(); equivalentNodes.set(equivalentNodes.indexOf(toMove), moved); Move plan = SimulatedOperation.prepareMove(sut, toMove, moved.longToken()).get(); @@ -114,12 +120,12 @@ public class OperationalEquivalenceTest extends CMSTestBase withMove.placements); } - private static ClusterMetadata simulateAndCompare(TokenPlacementModel.ReplicationFactor rf, List nodes) throws Exception + private static ClusterMetadata simulateAndCompare(ReplicationFactor rf, List nodes) throws Exception { Collections.shuffle(nodes, rng); try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf)) { - for (TokenPlacementModel.Node node : nodes) + for (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())); @@ -154,7 +160,7 @@ public class OperationalEquivalenceTest extends CMSTestBase { return ep.stream().sorted(Replica::compareTo).collect(Collectors.toList()); } - private static int toDc(int i, TokenPlacementModel.ReplicationFactor rf) + private static int toDc(int i, ReplicationFactor rf) { return (i % rf.dcs()) + 1; } diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java index d9ca33a759..6bd3cd7e25 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java @@ -30,13 +30,13 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import org.apache.cassandra.harry.sut.TokenPlacementModel.Replica; import org.junit.Assert; -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; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.toRanges; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Node; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Range; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Replica; +import static org.apache.cassandra.harry.model.TokenPlacementModel.ReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.toRanges; /** * A small class that helps to avoid doing mental arithmetics on ranges. diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java index 2ea853bf01..e1013b0509 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java @@ -32,8 +32,6 @@ import java.util.function.Supplier; import org.junit.Test; import org.apache.cassandra.harry.checker.ModelChecker; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.sut.TokenPlacementModel.Replica; import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements; import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations; @@ -46,11 +44,14 @@ 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.apache.cassandra.harry.model.TokenPlacementModel.Node; +import static org.apache.cassandra.harry.model.TokenPlacementModel.NodeFactory; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Range; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Replica; +import static org.apache.cassandra.harry.model.TokenPlacementModel.ReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.SimpleReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.nodeFactory; +import static org.apache.cassandra.harry.model.TokenPlacementModel.nodeFactoryHumanReadable; import static org.junit.Assert.assertTrue; public class PlacementSimulatorTest @@ -76,7 +77,7 @@ public class PlacementSimulatorTest public void testMove(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf) { - NodeFactory factory = TokenPlacementModel.nodeFactory(); + NodeFactory factory = nodeFactory(); Node movingNode = factory.make(1, 1, 1).overrideToken(t1); List orig = Arrays.asList(movingNode, factory.make(2, 1, 1).overrideToken(t2), @@ -135,7 +136,7 @@ public class PlacementSimulatorTest public void testBootstrap(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf) { - NodeFactory factory = TokenPlacementModel.nodeFactory(); + NodeFactory factory = nodeFactory(); List orig = Arrays.asList(factory.make(1, 1, 1).overrideToken(t1), factory.make(2, 1, 1).overrideToken(t2), factory.make(3, 1, 1).overrideToken(t3), @@ -195,7 +196,7 @@ public class PlacementSimulatorTest public void testDecommission(long t1, long t2, long t3, long t4, long t5, ReplicationFactor rf) { - NodeFactory factory = TokenPlacementModel.nodeFactory(); + NodeFactory factory = nodeFactory(); Node leavingNode = factory.make(1, 1, 1).overrideToken(t1); List orig = Arrays.asList(leavingNode, factory.make(2, 1, 1).overrideToken(t2), @@ -263,7 +264,7 @@ public class PlacementSimulatorTest public void simulate(ReplicationFactor rf) throws Throwable { - NodeFactory factory = TokenPlacementModel.nodeFactory(); + NodeFactory factory = nodeFactory(); List orig = Collections.singletonList(factory.make(1, 1, 1)); ModelChecker modelChecker = new ModelChecker<>(); @@ -325,7 +326,7 @@ public class PlacementSimulatorTest for (int n : new int[]{ 2, 3, 5 }) { ReplicationFactor rf = new SimpleReplicationFactor(n); - NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable(); + NodeFactory factory = nodeFactoryHumanReadable(); List nodes = new ArrayList<>(10); for (int i = 1; i <= 10; i++) nodes.add(factory.make(i, 1, 1)); @@ -343,7 +344,7 @@ public class PlacementSimulatorTest for (int n : new int[]{ 2, 3, 5 }) { ReplicationFactor rf = new SimpleReplicationFactor(n); - NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable(); + NodeFactory factory = nodeFactoryHumanReadable(); List nodes = new ArrayList<>(10); for (int i = 1; i <= 10; i++) nodes.add(factory.make(i, 1, 1)); @@ -360,7 +361,7 @@ public class PlacementSimulatorTest for (int n : new int[]{ 2, 3, 5 }) { ReplicationFactor rf = new SimpleReplicationFactor(n); - NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable(); + NodeFactory factory = nodeFactoryHumanReadable(); List nodes = new ArrayList<>(10); for (int i = 1; i <= 10; i++) nodes.add(factory.make(i, 1, 1)); diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java index c1440a1342..07ec054d54 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java @@ -19,46 +19,54 @@ package org.apache.cassandra.distributed.test.log; import java.io.IOException; +import java.util.Iterator; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; 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.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.harry.HarryHelper; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +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.shared.ClusterUtils; import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.execution.RingAwareInJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.model.TokenPlacementModel; +import org.apache.cassandra.harry.op.Visit; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tcm.transformations.PrepareJoin; -import org.apache.cassandra.service.StorageService; import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; import static org.apache.cassandra.distributed.shared.ClusterUtils.getClusterMetadataVersion; import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; public class ResumableStartupTest extends FuzzTestBase { + private static final String KS = "resumable_startup_test"; private static int WRITES = 500; @Test public void bootstrapWithDeferredJoinTest() throws Throwable { + Generator schemaGen = SchemaGenerators.schemaSpecGen(KS, "bootstrap_with_deferred_join", 1000); try (Cluster cluster = builder().withNodes(1) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) @@ -66,89 +74,116 @@ public class ResumableStartupTest extends FuzzTestBase .createWithoutStarting()) { IInvokableInstance cmsInstance = cluster.get(1); - Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() - .setSUT(() -> new InJvmSut(cluster)); - Run run = configBuilder.build().createRun(); - cmsInstance.config().set("auto_bootstrap", true); cmsInstance.startup(); - cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + - " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", - ConsistencyLevel.ALL); - cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); - ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), 1000))); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); - 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(); + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + history.customThrowing(() -> { + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", schema.keyspace)); + cluster.schemaChange(schema.compile()); + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + }, "Setup"); - IInstanceConfig config = cluster.newInstanceConfig() - .set("auto_bootstrap", true) - .set(Constants.KEY_DTEST_FULL_STARTUP, true); - IInvokableInstance newInstance = cluster.bootstrap(config); + Runnable writeAndValidate = () -> { + for (int i = 0; i < WRITES; i++) + history.insert(pkGen.generate(rng), ckGen.generate(rng)); - withProperty(CassandraRelevantProperties.TEST_WRITE_SURVEY, true, newInstance::startup); + for (int pk : pkGen.generated()) + history.selectPartition(pk); + }; + writeAndValidate.run(); - // Write with ALL, replicate via pending range mechanism - visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitExecutor(run, - MutatingRowVisitor::new, - SystemUnderTest.ConsistencyLevel.ONE, - rf)); + // First write with ONE, as we only have 1 node + TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); + DataTracker tracker = new DataTracker.SequentialDataTracker(); + QuiescentChecker checker = new QuiescentChecker(schema.valueGenerators, tracker, history); - for (int i = 0; i < WRITES; i++) - visitor.visit(); + RingAwareInJvmDTestVisitExecutor executor; + // RF is ONE here since we have no pending nodes + executor = RingAwareInJvmDTestVisitExecutor.builder() + .replicationFactor(rf) + .consistencyLevel(ConsistencyLevel.ONE) + .build(schema, tracker, checker, cluster); + Iterator iterator = history.iterator(); + while (iterator.hasNext()) + executor.execute(iterator.next()); - 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(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); + AtomicReference newInstance = new AtomicReference<>(); + history.customThrowing(() -> { + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + newInstance.set(cluster.bootstrap(config)); + + withProperty(CassandraRelevantProperties.TEST_WRITE_SURVEY, true, newInstance.get()::startup); + }, "Bootstrap"); + + writeAndValidate.run(); + + rf = new TokenPlacementModel.SimpleReplicationFactor(2); + // RF is ONE here since we have 1 regular and 1 pending node, but want to check for RF2 + executor = RingAwareInJvmDTestVisitExecutor.builder() + .replicationFactor(rf) + .consistencyLevel(ConsistencyLevel.ONE) + .build(schema, tracker, checker, cluster); + while (iterator.hasNext()) + executor.execute(iterator.next()); + + history.customThrowing(() -> { + Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); + // Quick check that schema changes are possible with nodes in write survey mode (i.e. with ranges locked) + cluster.schemaChange(String.format("ALTER TABLE %s.%s WITH comment = 'Schema alterations which do not affect placements should not be restricted by in flight operations';", schema.keyspace, schema.table)); + + final String newAddress = ClusterUtils.getBroadcastAddressHostWithPortString(newInstance.get()); + final String keyspace = schema.keyspace; + boolean newReplicaInCorrectState = cluster.get(1).callOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ksm = metadata.schema.getKeyspaceMetadata(keyspace); + boolean isWriteReplica = false; + boolean isReadReplica = false; + for (InetAddressAndPort readReplica : metadata.placements.get(ksm.params.replication).reads.byEndpoint().keySet()) + { + if (readReplica.getHostAddressAndPort().equals(newAddress)) + isReadReplica = true; + } + for (InetAddressAndPort writeReplica : metadata.placements.get(ksm.params.replication).writes.byEndpoint().keySet()) + { + if (writeReplica.getHostAddressAndPort().equals(newAddress)) + isWriteReplica = true; + } + return (isWriteReplica && !isReadReplica); + }); + Assert.assertTrue("Expected new instance to be a write replica only", newReplicaInCorrectState); + + Callable finishedBootstrap = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareJoin.FinishJoin && r.isSuccess()); + newInstance.get().runOnInstance(() -> { + try + { + StorageService.instance.joinRing(); + } + catch (IOException e) + { + throw new RuntimeException("Error joining ring", e); + } + }); + Epoch next = finishedBootstrap.call(); + Assert.assertEquals(String.format("Expected epoch after schema change, mid join & finish join to be %s, but was %s", + next.getEpoch(), currentEpoch.getEpoch() + 3), + next.getEpoch(), currentEpoch.getEpoch() + 3); + + }, "Finish bootstrap"); + + writeAndValidate.run(); + + while (iterator.hasNext()) + executor.execute(iterator.next()); - final String newAddress = ClusterUtils.getBroadcastAddressHostWithPortString(newInstance); - final String keyspace = run.schemaSpec.keyspace; - boolean newReplicaInCorrectState = cluster.get(1).callOnInstance(() -> { - ClusterMetadata metadata = ClusterMetadata.current(); - KeyspaceMetadata ksm = metadata.schema.getKeyspaceMetadata(keyspace); - boolean isWriteReplica = false; - boolean isReadReplica = false; - for (InetAddressAndPort readReplica : metadata.placements.get(ksm.params.replication).reads.byEndpoint().keySet()) - { - if (readReplica.getHostAddressAndPort().equals(newAddress)) - isReadReplica = true; - } - for (InetAddressAndPort writeReplica : metadata.placements.get(ksm.params.replication).writes.byEndpoint().keySet()) - { - if (writeReplica.getHostAddressAndPort().equals(newAddress)) - isWriteReplica = true; - } - return (isWriteReplica && !isReadReplica); }); - Assert.assertTrue("Expected new instance to be a write replica only", newReplicaInCorrectState); - - Callable finishedBootstrap = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareJoin.FinishJoin && r.isSuccess()); - newInstance.runOnInstance(() -> { - try - { - StorageService.instance.joinRing(); - } - catch (IOException e) - { - throw new RuntimeException("Error joining ring", e); - } - }); - Epoch next = finishedBootstrap.call(); - Assert.assertEquals(String.format("Expected epoch after schema change, mid join & finish join to be %s, but was %s", - next.getEpoch(), currentEpoch.getEpoch() + 3), - next.getEpoch(), currentEpoch.getEpoch() + 3); - - for (int i = 0; i < WRITES; i++) - visitor.visit(); - - QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, new TokenPlacementModel.SimpleReplicationFactor(3)); - model.validateAll(); } } -} +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java index 0aaf8737eb..b8f98f68a0 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java @@ -29,7 +29,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.Murmur3Partitioner; import static org.apache.cassandra.distributed.test.log.MetadataChangeSimulationTest.simulate; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.SimpleReplicationFactor; +import static org.apache.cassandra.harry.model.TokenPlacementModel.SimpleReplicationFactor; public class SimpleStrategySimulationTest extends CMSTestBase { diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java b/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java index c8c2315529..7763831933 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java @@ -53,7 +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.harry.sut.TokenPlacementModel.*; +import static org.apache.cassandra.harry.model.TokenPlacementModel.Node; public abstract class SimulatedOperation { diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java index e8bf239f56..2e131bcf49 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.distributed.test.ExecUtil; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tcm.log.Entry; diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java index e165a0d16e..64810654d5 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java @@ -18,115 +18,135 @@ package org.apache.cassandra.distributed.upgrade; -import java.util.Collections; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; +import java.time.Duration; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import com.google.common.util.concurrent.Uninterruptibles; - -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 org.apache.cassandra.concurrent.Interruptible; import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; import org.apache.cassandra.schema.SchemaConstants; -import org.apache.cassandra.utils.concurrent.CountDownLatch; - +import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; 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.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE; +import static org.apache.cassandra.harry.ColumnSpec.asciiType; +import static org.apache.cassandra.harry.ColumnSpec.ck; +import static org.apache.cassandra.harry.ColumnSpec.int64Type; +import static org.apache.cassandra.harry.ColumnSpec.pk; +import static org.apache.cassandra.harry.ColumnSpec.regularColumn; +import static org.apache.cassandra.harry.ColumnSpec.staticColumn; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; import static org.apache.cassandra.tcm.log.SystemKeyspaceStorage.NAME; import static org.junit.Assert.assertEquals; public class ClusterMetadataUpgradeHarryTest extends UpgradeTestBase { @Test - public void simpleUpgradeTest() throws Throwable + public void simpleUpgradeTest() { - ExecutorService es = executorFactory().pooled("harry", 1); + AtomicReference executor = new AtomicReference<>(); + AtomicLong loops = new AtomicLong(0); Listener listener = new Listener(); - CountDownLatch stopLatch = CountDownLatch.newCountDownLatch(1); - AtomicReference> harryRunner = new AtomicReference<>(); - new UpgradeTestBase.TestCase() - .nodes(3) - .nodesToUpgrade(1, 2, 3) - .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP) - .set(Constants.KEY_DTEST_FULL_STARTUP, true)) - .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))); - - 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 Configuration.FixedSchemaProviderConfiguration(schema)) - .setDataTracker(new Configuration.LockingDataTrackerConfiguration(-1l, -1l, Collections.emptyList())) - .setSUT(new ExistingClusterSUT(cluster, listener)) - .build(); - - Future f = es.submit(() -> { + Runnable awaitHarryProgress = () -> { + long startingLoopCount = loops.get(); + long deadline = startingLoopCount + 100; + long nowNanos = System.nanoTime(); + boolean matched = false; + for (int i = 0; i < 20 && !(matched = loops.get() >= deadline); i++) + { try { - new FlaggedRunner(config.createRun(), - config, - asList(pool("Writer", 1, MutatingVisitor::new), - pool("Reader", 1, (run) -> new RandomPartitionValidator(run, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP.NO_OP))), - stopLatch).run(); + TimeUnit.MILLISECONDS.sleep(500); } - catch (Throwable e) + catch (InterruptedException e) { - throw new RuntimeException(e); + Thread.currentThread().interrupt(); + throw new UncheckedInterruptedException(e); } - }); - harryRunner.set(f); - Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); - }) - .runAfterNodeUpgrade((cluster, node) -> { - Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); // make sure harry executes in mixed mode - }) - .runAfterClusterUpgrade((cluster) -> { + } + if (!matched) + throw new AssertionError("Harry did not make enough progress within " + Duration.ofNanos(System.nanoTime() - nowNanos) + "; starting loops " + startingLoopCount + ", ending loops " + loops.get()); + }; + withRandom(rng -> { + new TestCase() + .nodes(3) + .nodesToUpgrade(1, 2, 3) + .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP) + .set(Constants.KEY_DTEST_FULL_STARTUP, true)) + .upgradesToCurrentFrom(v41) + .withUpgradeListener(listener) + .setup((cluster) -> { + SchemaSpec schema = new SchemaSpec(rng.next(), + 10_000, + "harry", "test_table", + asList(pk("pk1", asciiType), pk("pk2", int64Type)), + asList(ck("ck1", asciiType, false), ck("ck2", int64Type, false)), + asList(regularColumn("regular1", asciiType), regularColumn("regular2", int64Type)), + asList(staticColumn("static1", asciiType), staticColumn("static2", int64Type))); + cluster.schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3)); + cluster.schemaChange(schema.compile()); - // make sure we haven't persisted any events; - cluster.stream().forEach((i) -> { - Object[][] res = i.executeInternal(String.format("select * from %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, NAME)); - assertEquals(0, res.length); - }); + HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + hb -> InJvmDTestVisitExecutor.builder() + .retryPolicy(retry -> true) + .nodeSelector(lts -> { + while (true) + { + int node = rng.nextInt(0, cluster.size()) + 1; + if (listener.isDown(node)) + continue; + return node; + } + }) + .consistencyLevel(ConsistencyLevel.QUORUM) + .build(schema, hb, cluster)); - cluster.get(1).nodetoolResult("cms", "initialize").asserts().success(); - cluster.get(1).nodetoolResult("cms", "reconfigure", "3").asserts().success(); - cluster.schemaChange(withKeyspace("create table %s.xyz (id int primary key)")); - stopLatch.decrement(); - harryRunner.get().get(); - }).run(); + Generator pkIdxGen = Generators.int32(0, Math.min(10_000, schema.valueGenerators.ckPopulation())); + + executor.set(executorFactory().infiniteLoop("R/W Worload", + () -> { + history.insert(pkIdxGen.generate(rng)); + history.selectPartition(pkIdxGen.generate(rng)); + loops.incrementAndGet(); + }, UNSAFE)); + + awaitHarryProgress.run(); + }) + .runAfterNodeUpgrade((cluster, node) -> { + awaitHarryProgress.run(); // make sure harry executes in mixed mode + }) + .runAfterClusterUpgrade((cluster) -> { + + // make sure we haven't persisted any events; + cluster.stream().forEach((i) -> { + Object[][] res = i.executeInternal(String.format("select * from %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, NAME)); + assertEquals(0, res.length); + }); + + cluster.get(1).nodetoolResult("cms", "initialize").asserts().success(); + cluster.get(1).nodetoolResult("cms", "reconfigure", "3").asserts().success(); + cluster.schemaChange(withKeyspace("create table %s.xyz (id int primary key)")); + executor.get().shutdownNow(); + executor.get().awaitTermination(1, TimeUnit.MINUTES); + }).run(); + }); } - private static class Listener implements UpgradeListener, ClusterState + private static class Listener implements UpgradeListener { // only ever one node down here. public final AtomicInteger downNode = new AtomicInteger(0); @@ -142,7 +162,6 @@ public class ClusterMetadataUpgradeHarryTest extends UpgradeTestBase downNode.set(0); } - @Override public boolean isDown(int i) { return downNode.get() == i; diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java index ddddb57c09..8607c2551d 100644 --- a/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java @@ -18,120 +18,117 @@ 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.distributed.test.IntegrationTestBase; +import org.apache.cassandra.harry.SchemaSpec; 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.HistoryBuilderHelper; 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; +import org.apache.cassandra.harry.dsl.SingleOperationBuilder; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; public class RangeTombstoneBurnTest extends IntegrationTestBase { - private final long seed = 1; - private final int ITERATIONS = 5; - private final int STEPS_PER_ITERATION = 100; + private final int ITERATIONS = 10; + private final int STEPS_PER_ITERATION = 1000; @Test - public void rangeTombstoneBurnTest() throws Throwable + public void rangeTombstoneBurnTest() { - Supplier supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - - for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++) - { - SchemaSpec schema = supplier.get(); - beforeEach(); - SchemaSpec doubleWriteSchema = schema.cloneWithName(schema.keyspace, schema.keyspace + "_debug"); - - sut.schemaChange(schema.compile().cql()); - sut.schemaChange(doubleWriteSchema.compile().cql()); - - QueryModifyingSut sut = new QueryModifyingSut(this.sut, - schema.table, - doubleWriteSchema.table); - + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "range_tombstone", 100); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); cluster.get(1).nodetool("disableautocompaction"); + cluster.schemaChange(schema.compile()); + + int perIteration = Math.min(10, schema.valueGenerators.pkPopulation());; + int maxPartitions = Math.max(perIteration, schema.valueGenerators.pkPopulation()); for (int iteration = 0; iteration < ITERATIONS; iteration++) { - ModelChecker modelChecker = new ModelChecker<>(); - EntropySource entropySource = new JdkRandomEntropySource(iteration); - - int maxPartitionSize = entropySource.nextInt(1, 1 << entropySource.nextInt(5, 11)); - - int[] partitions = new int[10]; - for (int j = 0; j < partitions.length; j++) + Integer[] partitions = new Integer[perIteration]; + for (int j = 0; j < partitions.length && iteration * 10 < maxPartitions; 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(); + float deleteRowChance = rng.nextFloat(0.99f, 1.0f); + float deletePartitionChance = rng.nextFloat(0.999f, 1.0f); + float deleteColumnsChance = rng.nextFloat(0.95f, 1.0f); + float deleteRangeChance = rng.nextFloat(0.95f, 1.0f); + float flushChance = rng.nextFloat(0.999f, 1.0f); + int maxPartitionSize = Math.min(rng.nextInt(1, 1 << rng.nextInt(5, 11)), schema.valueGenerators.ckPopulation()); - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); + Generator partitionPicker = Generators.pick(partitions); + Generator rowPicker = Generators.int32(0, maxPartitionSize); + ModelChecker model = new ModelChecker<>(); + ReplayingHistoryBuilder historyBuilder = new ReplayingHistoryBuilder(schema.valueGenerators, + (hb) -> InJvmDTestVisitExecutor.builder().build(schema, hb, cluster)); - DataTracker tracker = new DefaultDataTracker(); - modelChecker.init(new ReplayingHistoryBuilder(seed, maxPartitionSize, STEPS_PER_ITERATION, new DefaultDataTracker(), sut, schema, rf, SystemUnderTest.ConsistencyLevel.ALL)) - .step((history, rng) -> { - int rowIdx = rng.nextInt(maxPartitionSize); - int partitionIdx = partitions[rng.nextInt(partitions.length)]; - history.visitPartition(partitionIdx).insert(rowIdx); - }) - .step((history, rng) -> rng.nextFloat() > deleteRowChance, - (history, rng) -> { - int partitionIdx = partitions[rng.nextInt(partitions.length)]; - history.visitPartition(partitionIdx).deleteRow(); - }) - .step((history, rng) -> rng.nextFloat() > deleteRowChance, - (history, rng) -> { - int partitionIdx = partitions[rng.nextInt(partitions.length)]; - history.visitPartition(partitionIdx).deleteColumns(); - }) - .step((history, rng) -> rng.nextFloat() > deletePartitionChance, - (history, rng) -> { - int partitionIdx = partitions[rng.nextInt(partitions.length)]; - history.visitPartition(partitionIdx).deletePartition(); - }) - .step((history, rng) -> rng.nextFloat() > flushChance, - (history, rng) -> { - cluster.get(1).nodetool("flush", schema.keyspace, schema.table); - flushes.incrementAndGet(); - }) - .step((history, rng) -> rng.nextFloat() > deleteRangeChance, - (history, rng) -> { - int partitionIdx = partitions[rng.nextInt(partitions.length)]; - history.visitPartition(partitionIdx).deleteRowSlice(); - }) - .step((history, rng) -> rng.nextFloat() > deleteRangeChance, - (history, rng) -> { - int row1 = rng.nextInt(maxPartitionSize); - int row2 = rng.nextInt(maxPartitionSize); - int partitionIdx = partitions[rng.nextInt(partitions.length)]; - history.visitPartition(partitionIdx).deleteRowRange(Math.min(row1, row2), - Math.max(row1, row2), - entropySource.nextBoolean(), - entropySource.nextBoolean()); - }) - .afterAll((history) -> { - // Sanity check - history.validate(new AgainstSutChecker(tracker, history.clock(), sut, schema, doubleWriteSchema), - partitions); - history.validate(partitions); - }) - .run(STEPS_PER_ITERATION, seed, entropySource); + model.init(historyBuilder) + .step((history, rng_) -> { + int pdIdx = partitionPicker.generate(rng); + history.insert(pdIdx, rowPicker.generate(rng)); + history.selectPartition(pdIdx); + }) + .step((history, rng_) -> rng.nextDouble() >= deleteRowChance, + (history, rng_) -> { + int pdIdx = partitionPicker.generate(rng); + history.deleteRow(pdIdx, rowPicker.generate(rng)); + history.selectPartition(pdIdx); + }) + .step((history, rng_) -> rng.nextDouble() >= deletePartitionChance, + (history, rng_) -> { + int pdIdx = partitionPicker.generate(rng); + history.deletePartition(pdIdx); + history.selectPartition(pdIdx); + }) + .step((history, rng_) -> rng.nextDouble() >= deleteColumnsChance, + (history, rng_) -> { + int pdIdx = partitionPicker.generate(rng); + HistoryBuilderHelper.deleteRandomColumns(schema, pdIdx, rowPicker.generate(rng), rng, history); + history.selectPartition(pdIdx); + }) + .step((history, rng_) -> rng.nextDouble() >= deleteRangeChance, + (history, rng_) -> { + int pdIdx = partitionPicker.generate(rng); + history.deleteRowRange(pdIdx, + rowPicker.generate(rng), + rowPicker.generate(rng), + rng.nextInt(schema.clusteringKeys.size()), + rng.nextBoolean(), + rng.nextBoolean() + ); + history.selectPartition(pdIdx); + }) + .step((history, rng_) -> { + int pdIdx = partitionPicker.generate(rng); + history.selectRow(pdIdx, rowPicker.generate(rng)); + }) + .step((history, rng_) -> { + int pdIdx = partitionPicker.generate(rng); + history.selectRowRange(pdIdx, + rowPicker.generate(rng), + rowPicker.generate(rng), + rng.nextInt(schema.clusteringKeys.size()), + rng.nextBoolean(), + rng.nextBoolean()); + }) + .step((history, rng_) -> rng.nextDouble() >= flushChance, + (history, rng_) -> { + history.custom(() -> cluster.get(1).nodetool("flush", schema.keyspace, schema.table), "FLUSH"); + }) + .exitCondition((history) -> { + if (historyBuilder.size() < STEPS_PER_ITERATION) + return false; + return true; + }) + .run(0, Long.MAX_VALUE, rng); } - } + }); } } \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/examples/RepairBurnTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RepairBurnTest.java index 19f2997d69..65b1bf4441 100644 --- a/test/distributed/org/apache/cassandra/fuzz/harry/examples/RepairBurnTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RepairBurnTest.java @@ -18,27 +18,21 @@ package org.apache.cassandra.fuzz.harry.examples; -import java.util.Arrays; -import java.util.Random; - import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.distributed.api.Feature; -import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase; +import org.apache.cassandra.distributed.test.IntegrationTestBase; +import org.apache.cassandra.harry.SchemaSpec; import org.apache.cassandra.harry.checker.ModelChecker; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; import org.apache.cassandra.harry.dsl.HistoryBuilder; -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.sut.injvm.InJvmSutBase; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.tracker.DefaultDataTracker; -import org.apache.cassandra.harry.visitors.ReplayingVisitor; +import org.apache.cassandra.harry.dsl.HistoryBuilderHelper; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; public class RepairBurnTest extends IntegrationTestBase { @@ -46,92 +40,45 @@ public class RepairBurnTest extends IntegrationTestBase public static void before() throws Throwable { init(3, - (cfg) -> InJvmSutBase.defaultConfig().accept(cfg.with(Feature.NETWORK, Feature.GOSSIP))); + (cfg) -> defaultConfig().accept(cfg.with(Feature.NETWORK, Feature.GOSSIP))); } - private final long seed = 1L; - @Test - public void repairBurnTest() throws Throwable + public void repairBurnTest() { - SchemaSpec schema = new SchemaSpec("repair_burn_test", - "test_overrides", - Arrays.asList( - ColumnSpec.pk("pk1", ColumnSpec.asciiType(4, 10)), - ColumnSpec.pk("pk2", ColumnSpec.int64Type), - ColumnSpec.pk("pk3", ColumnSpec.int64Type), - ColumnSpec.pk("pk4", ColumnSpec.asciiType(2, 10))), - Arrays.asList( - ColumnSpec.ck("ck1", ColumnSpec.asciiType(2, 0)), - ColumnSpec.ck("ck2", ColumnSpec.asciiType(2, 0)), - ColumnSpec.ck("ck3", ColumnSpec.int64Type), - ColumnSpec.ck("ck4", ColumnSpec.asciiType(4, 100)), - ColumnSpec.ck("ck5", ColumnSpec.asciiType(8, 100)) - ), - Arrays.asList( - ColumnSpec.regularColumn("regular1", ColumnSpec.asciiType(8, 100)), - ColumnSpec.regularColumn("regular2", ColumnSpec.asciiType(8, 100)), - ColumnSpec.regularColumn("regular3", ColumnSpec.asciiType(8, 100)) - ), - Arrays.asList( - ColumnSpec.staticColumn("static1", ColumnSpec.asciiType(8, 100)), - ColumnSpec.staticColumn("static2", ColumnSpec.asciiType(8, 100)), - ColumnSpec.staticColumn("static3", ColumnSpec.asciiType(8, 100)) - )); - - sut.schemaChange("CREATE KEYSPACE " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};"); - sut.schemaChange(schema.compile().cql()); - - ModelChecker modelChecker = new ModelChecker<>(); - DataTracker tracker = new DefaultDataTracker(); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(3); - int maxPartitionSize = 10; int partitions = 1000; + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "repair_burn", 1000); - modelChecker.init(new HistoryBuilder(seed, maxPartitionSize, 10, schema, rf)) - .step((history, rng) -> { - history.visitPartition(rng.nextInt(partitions), - (ps) -> { - Object[][] clusterings = new Object[maxPartitionSize][]; - for (int i = 0; i < clusterings.length; i++) - { - Object[] v = schema.ckGenerator.inflate(rng.next()); - for (int j = 0; j < v.length; j++) - { - if (rng.nextBoolean() && v[j] instanceof String) - { - v[j] = ""; - return; - } - } - clusterings[i] = v; - } - ps.overrideClusterings(clusterings); - }) - .insert(rng.nextInt(maxPartitionSize)); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), partitions))); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), maxPartitionSize)); + + ModelChecker modelChecker = new ModelChecker<>(); + + modelChecker.init(new HistoryBuilder(schema.valueGenerators)) + .step((history, rng_) -> HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history)) + .step((history, rng_) -> history.deleteRow(pkGen.generate(rng), ckGen.generate(rng))) + .exitCondition((history) -> { + if (history.size() < 10_000) + return false; + + history.custom(() -> cluster.get(1).nodetool("repair", "--full"), + "Repair"); + + for (Integer pkIdx : pkGen.generated()) + history.selectPartition(pkIdx); + + cluster.schemaChange(schema.compile()); + + InJvmDTestVisitExecutor.replay(InJvmDTestVisitExecutor.builder().build(schema, history, cluster), + history); + + return true; }) - .step((history, rng) -> { - history.visitPartition(rng.nextInt(partitions)) - .deleteRow(rng.nextInt(maxPartitionSize)); - }) - .exitCondition((history) -> { - if (history.size() < 10_000) - return false; - - ReplayingVisitor visitor = history.visitor(tracker, sut, SystemUnderTest.ConsistencyLevel.NODE_LOCAL); - visitor.replayAll(); - - cluster.get(1).nodetool("repair", "--full"); - - Model model = history.quiescentLocalChecker(tracker, sut); - - for (Long pd : history.visitedPds()) - model.validate(Query.selectAllColumns(history.schema(), pd, false)); - - return true; - }) - .run(Integer.MAX_VALUE, seed, new JdkRandomEntropySource(new Random(seed))); + .run(rng); + }); } } \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/gen/DataGeneratorsTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/gen/DataGeneratorsTest.java deleted file mode 100644 index 37aa5a5274..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/gen/DataGeneratorsTest.java +++ /dev/null @@ -1,504 +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.fuzz.harry.gen; - -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.gen.Bijections; -import org.apache.cassandra.harry.gen.Bytes; -import org.apache.cassandra.harry.gen.DataGenerators; -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.gen.Generator; -import org.apache.cassandra.harry.gen.StringBijection; - -public class DataGeneratorsTest -{ - private static final int RUNS = 100; - private static final EntropySource rand = EntropySource.forTests(1); - - @Test - public void testSingleTypeRoundTrip() - { - for (ColumnSpec.DataType dt : new ColumnSpec.DataType[]{ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.asciiType, - ColumnSpec.floatType, - ColumnSpec.doubleType}) - { - for (int i = 0; i < RUNS; i++) - { - DataGenerators.SinglePartKeyGenerator gen = new DataGenerators.SinglePartKeyGenerator(Collections.singletonList(ColumnSpec.ck("ck0", dt, false))); - long descriptor = rand.next(); - descriptor = gen.adjustEntropyDomain(descriptor); - Assert.assertEquals(descriptor, - gen.deflate(gen.inflate(descriptor))); - } - } - } - - @Test - public void testRequiredBytes() - { - testRequiredBytes(sizes(4, 1), - ColumnSpec.int32Type, ColumnSpec.int8Type); - testRequiredBytes(sizes(7, 1), - ColumnSpec.int64Type, ColumnSpec.int8Type); - testRequiredBytes(sizes(4, 4), - ColumnSpec.int32Type, ColumnSpec.int64Type); - testRequiredBytes(sizes(4, 4), - ColumnSpec.int32Type, ColumnSpec.int32Type); - testRequiredBytes(sizes(4, 3), - ColumnSpec.int32Type, ColumnSpec.floatType); - testRequiredBytes(sizes(4, 4), - ColumnSpec.int64Type, ColumnSpec.int64Type); - testRequiredBytes(sizes(4, 2, 2), - ColumnSpec.int64Type, ColumnSpec.int16Type, ColumnSpec.int32Type); - testRequiredBytes(sizes(6, 1, 1), - ColumnSpec.int64Type, ColumnSpec.int8Type, ColumnSpec.int8Type); - testRequiredBytes(sizes(4, 2, 2), - ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type); - testRequiredBytes(sizes(4, 2, 2), - ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type); - testRequiredBytes(sizes(1, 5, 2), - ColumnSpec.int8Type, ColumnSpec.asciiType, ColumnSpec.int64Type); - testRequiredBytes(sizes(1, 1, 6), - ColumnSpec.int8Type, ColumnSpec.int8Type, ColumnSpec.int64Type); - testRequiredBytes(sizes(2, 2, 2, 2), - ColumnSpec.int64Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type); - testRequiredBytes(sizes(1, 3, 2, 2), - ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type); - testRequiredBytes(sizes(1, 3, 2, 2), - ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type); - testRequiredBytes(sizes(1, 3, 2, 2), - ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type); - } - - private static void testRequiredBytes(int[] sizes, - ColumnSpec.DataType... types) - { - int sum = 0; - for (int size : sizes) - sum += size; - Assert.assertTrue(sum > 0); - Assert.assertTrue(sum <= 8); - - List> columns = new ArrayList<>(types.length); - for (int i = 0; i < types.length; i++) - columns.add(ColumnSpec.ck("r" + i, types[i], false)); - Assert.assertArrayEquals(columns.toString(), - sizes, - DataGenerators.requiredBytes(columns)); - } - - @Test - public void testSliceStitch() - { - for (int i = 2; i < 5; i++) - { - Iterator iter = permutations(i, - ColumnSpec.DataType.class, - ColumnSpec.int8Type, - ColumnSpec.asciiType, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int32Type, - ColumnSpec.floatType, - ColumnSpec.doubleType); - while (iter.hasNext()) - { - testSliceStitch(iter.next()); - } - } - } - - private static void testSliceStitch(ColumnSpec.DataType... types) - { - List> spec = new ArrayList<>(types.length); - for (int i = 0; i < types.length; i++) - spec.add(ColumnSpec.ck("r" + i, types[i], false)); - DataGenerators.MultiPartKeyGenerator gen = new DataGenerators.MultiPartKeyGenerator(spec); - - for (int i = 0; i < RUNS; i++) - { - long orig = gen.adjustEntropyDomain(rand.next()); - long[] sliced = gen.slice(orig); - long stitched = gen.stitch(sliced); - Assert.assertEquals(String.format("Orig: %s. Stitched: %s", - Long.toHexString(orig), - Long.toHexString(stitched)), - orig, stitched); - } - } - - @Test - public void testKeyGenerators() - { - for (int i = 1; i < 5; i++) - { - for (boolean asReversed : new boolean[]{ false, true }) - { - Iterator iter = permutations(i, - ColumnSpec.DataType.class, - ColumnSpec.int8Type, - ColumnSpec.asciiType, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType - ); - - while (iter.hasNext()) - { - ColumnSpec.DataType[] types = iter.next(); - try - { - testKeyGenerators(asReversed, types); - } - catch (Throwable t) - { - throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t); - } - } - } - } - } - - @Ignore - @Test // this one is mostly useful when the above test fails and you need a quicker turnaround - public void testSomeKeyGenerators() - { - Iterator iter = Collections.singletonList(new ColumnSpec.DataType[]{ ColumnSpec.int32Type, ColumnSpec.int32Type }).iterator(); - - while (iter.hasNext()) - { - ColumnSpec.DataType[] types = iter.next(); - try - { - testKeyGenerators(true, types); - testKeyGenerators(false, types); - } - catch (Throwable t) - { - throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t); - } - } - } - - static void testKeyGenerators(boolean reversed, ColumnSpec.DataType... types) - { - List> spec = new ArrayList<>(types.length); - for (int i = 0; i < types.length; i++) - spec.add(ColumnSpec.ck("r" + i, types[i], reversed)); - - DataGenerators.KeyGenerator keyGenerator = DataGenerators.createKeyGenerator(spec); - - for (int i = 0; i < RUNS; i++) - { - testKeyGenerators(rand.next(), rand.next(), keyGenerator); - // test some edge cases - testKeyGenerators(0, 0, keyGenerator); - testKeyGenerators(0xffffffffffffffffL, 0xffffffffffffffffL, keyGenerator); - testKeyGenerators(keyGenerator.minValue(), keyGenerator.maxValue(), keyGenerator); - testKeyGenerators(0, keyGenerator.minValue(), keyGenerator); - testKeyGenerators(0, keyGenerator.maxValue(), keyGenerator); - long descriptor = rand.next(); - testKeyGenerators(descriptor, 0, keyGenerator); - testKeyGenerators(descriptor, keyGenerator.minValue(), keyGenerator); - testKeyGenerators(descriptor, keyGenerator.maxValue(), keyGenerator); - testKeyGenerators(descriptor, 0xffffffffffffffffL, keyGenerator); - testKeyGenerators(descriptor, descriptor + 1, keyGenerator); - testKeyGenerators(descriptor, descriptor - 1, keyGenerator); - testKeyGenerators(descriptor, descriptor, keyGenerator); - } - - // Fixed prefix, tests sign inversion of subsequent values - if (types.length > 1) - { - - long pattern = Bytes.bytePatternFor(Long.BYTES - DataGenerators.requiredBytes(spec)[0]); - for (int i = 0; i < RUNS; i++) - { - long descriptor = rand.next(); - long descriptor2 = (descriptor & ~pattern) | (rand.next() & pattern); - testKeyGenerators(descriptor, descriptor2, keyGenerator); - } - - } - } - - static void testKeyGenerators(long descriptor1, long descriptor2, DataGenerators.KeyGenerator keyGenerator) - { - descriptor1 = keyGenerator.adjustEntropyDomain(descriptor1); - descriptor2 = keyGenerator.adjustEntropyDomain(descriptor2); - Object[] value1 = keyGenerator.inflate(descriptor1); - Object[] value2 = keyGenerator.inflate(descriptor2); - - assertDescriptorsEqual(descriptor1, - keyGenerator.deflate(value1)); - assertDescriptorsEqual(descriptor2, - keyGenerator.deflate(value2)); - - Assert.assertEquals(String.format("%s %s %s and %s %s %s have different order. ", - Arrays.toString(value1), - toSignString(compare(value1, value2, keyGenerator.columns)), - Arrays.toString(value2), - Long.toHexString(descriptor1), - toSignString(Long.compare(descriptor1, descriptor2)), - Long.toHexString(descriptor2)), - normalize(Long.compare(descriptor1, descriptor2)), - compare(value1, value2, keyGenerator.columns)); - } - - private static void assertDescriptorsEqual(long l, long r) - { - Assert.assertEquals(String.format("Expected %d (0x%s), but got %d (0x%s)", - l, Long.toHexString(l), r, Long.toHexString(r)), - l, r); - } - - @Test - public void int8GeneratorTest() - { - testInverse(Bijections.INT8_GENERATOR); - } - - @Test - public void int16GeneratorTest() - { - testInverse(Bijections.INT16_GENERATOR); - } - - @Test - public void int32GeneratorTest() - { - testInverse(Bijections.INT32_GENERATOR); - } - - @Test - public void int64GeneratorTest() - { - testInverse(Bijections.INT64_GENERATOR); - testOrderPreserving(Bijections.INT64_GENERATOR); - testInverse(new Bijections.ReverseBijection(Bijections.INT64_GENERATOR)); - testOrderPreserving(new Bijections.ReverseBijection(Bijections.INT64_GENERATOR), true); - } - - @Test - public void booleanGenTest() - { - testInverse(Bijections.BOOLEAN_GENERATOR); - testOrderPreserving(Bijections.BOOLEAN_GENERATOR); - } - - @Test - public void floatGeneratorTest() - { - testInverse(Bijections.FLOAT_GENERATOR); - testOrderPreserving(Bijections.FLOAT_GENERATOR, Float::compareTo); - } - - @Test - public void doubleGeneratorTest() - { - testInverse(Bijections.DOUBLE_GENERATOR); - testOrderPreserving(Bijections.DOUBLE_GENERATOR); - } - - - @Test - public void stringGenTest() - { - testInverse(new StringBijection()); - testOrderPreserving(new StringBijection()); - } - - public static void testInverse(Bijections.Bijection gen) - { - test(gen, - (v) -> Assert.assertEquals(gen.adjustEntropyDomain(v.descriptor), gen.deflate(v.value))); - } - - public static void testOrderPreserving(Bijections.Bijection gen) - { - testOrderPreserving(gen, false); - } - - public static void testOrderPreserving(Bijections.Bijection gen, boolean reverse) - { - test(gen, gen, - (v1, v2) -> { - long v1Descriptor = gen.adjustEntropyDomain(v1.descriptor); - long v2Descriptor = gen.adjustEntropyDomain(v2.descriptor); - Assert.assertEquals(String.format("%s (%s) and %s (%s) sort wrong", - v1.value, - Long.toHexString(v1Descriptor), - v2.value, - Long.toHexString(v2Descriptor)), - normalize(Long.compare(v1Descriptor, v2Descriptor)) * (reverse ? -1 : 1), - normalize(v1.value.compareTo(v2.value))); - }); - } - - public static void testOrderPreserving(Bijections.Bijection gen, Comparator comparator) - { - test(gen, gen, - (v1, v2) -> Assert.assertEquals(normalize(Long.compare(gen.adjustEntropyDomain(v1.descriptor), - gen.adjustEntropyDomain(v2.descriptor))), - normalize(comparator.compare(v1.value, v2.value)))); - } - - public static void test(Bijections.Bijection gen1, - Consumer> validate) - { - - for (int i = 0; i < RUNS; i++) - { - long descriptor1 = rand.next(); - validate.accept(new Generator.Value(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1)))); - } - } - - public static void test(Bijections.Bijection gen1, - Bijections.Bijection gen2, - BiConsumer, Generator.Value> validate) - { - - for (int i = 0; i < RUNS; i++) - { - long descriptor1 = rand.next(); - long descriptor2 = rand.next(); - validate.accept(new Generator.Value(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1))), - new Generator.Value(descriptor2, gen2.inflate(gen1.adjustEntropyDomain(descriptor2)))); - } - } - - public static int[] sizes(int... ints) - { - return ints; - } - - - public static String toSignString(int l) - { - if (l == 0) - return "="; - else if (l > 0) - return ">"; - return "<"; - } - public static int normalize(int l) - { - if (l == 0) - return 0; - if (l > 0) - return 1; - else - return -1; - } - - static int compare(Object[] a, Object[] b, List> spec) - { - assert a.length == b.length; - for (int i = 0; i < a.length; i++) - { - Comparable comparableA = (Comparable) a[i]; - Comparable comparableB = (Comparable) b[i]; - - int cmp = comparableA.compareTo(comparableB); - if (cmp != 0) - { - if (spec.get(i).isReversed()) - cmp *= -1; - - return cmp < 0 ? -1 : 1; - } - } - return 0; - } - - public static Iterator permutations(int size, Class klass, T... values) - { - int[] cursors = new int[size]; - return new Iterator() - { - int left = 0; - T[] next = fromCursors(); - - public boolean hasNext() - { - for (int i = cursors.length - 1; i >= 0; i--) - { - if (cursors[i] < values.length - 1) - return true; - } - return false; - } - - public T[] computeNext() - { - cursors[left]++; - - for (int i = 0; i < cursors.length; i++) - { - if (cursors[i] == values.length) - { - cursors[i] = 0; - cursors[i + 1]++; - } - } - - return fromCursors(); - } - - public T[] next() - { - if (next == null) - next = computeNext(); - - T[] ret = next; - next = null; - return ret; - } - - public T[] fromCursors() - { - T[] res = (T[]) Array.newInstance(klass, cursors.length); - for (int i = 0; i < cursors.length; i++) - res[i] = values[cursors[i]]; - return res; - } - }; - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/gen/EntropySourceTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/gen/EntropySourceTest.java deleted file mode 100644 index a2b53306d0..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/gen/EntropySourceTest.java +++ /dev/null @@ -1,158 +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.fuzz.harry.gen; - -import java.util.Random; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.gen.distribution.Distribution; -import org.apache.cassandra.harry.gen.rng.PCGFastPure; -import org.apache.cassandra.harry.gen.rng.PcgRSUFast; -import org.apache.cassandra.harry.model.OpSelectors; - -public class EntropySourceTest -{ - private static int RUNS = 100000; - - @Test - public void testScale() - { - Random rand = new Random(); - for (int cycle = 0; cycle < RUNS; cycle++) - { - int a = rand.nextInt(100); - int b = rand.nextInt(100); - while (a == b) - b = rand.nextInt(100); - - int min = Math.min(a, b); - int max = Math.max(a, b); - long[] cardinality = new long[max - min]; - for (int i = 0; i < 100000; i++) - { - long rnd = rand.nextLong(); - long scaled = Distribution.ScaledDistribution.scale(rnd, min, max); - cardinality[(int) scaled - min]++; - } - - for (long c : cardinality) - Assert.assertTrue(c > 0); - } - } - - @Test - public void testShuffleUnshuffle() - { - Random rnd = new Random(); - - for (int i = 1; i < RUNS; i++) - { - long l = rnd.nextLong(); - Assert.assertEquals(l, PCGFastPure.unshuffle(PCGFastPure.shuffle(l))); - } - } - - @Test - public void testImmutableRng() - { - int size = 5; - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - for (int stream = 1; stream < RUNS; stream++) - { - long[] generated = new long[size]; - for (int i = 0; i < size; i++) - generated[i] = rng.randomNumber(i, stream); - - Assert.assertEquals(0, rng.sequenceNumber(generated[0], stream)); - Assert.assertEquals(generated[1], rng.next(generated[0], stream)); - - for (int i = 1; i < size; i++) - { - Assert.assertEquals(generated[i], rng.next(generated[i - 1], stream)); - Assert.assertEquals(generated[i - 1], rng.prev(generated[i], stream)); - Assert.assertEquals(i, rng.sequenceNumber(generated[i], stream)); - } - } - } - - @Test - public void testSequenceNumber() - { - int size = 5; - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - for (int stream = 1; stream < RUNS; stream++) - { - for (int i = 0; i < size; i++) - Assert.assertEquals(i, rng.sequenceNumber(rng.randomNumber(i, stream), stream)); - } - } - - @Test - public void seekTest() - { - PcgRSUFast rand = new PcgRSUFast(1, 1); - long first = rand.next(); - long last = 0; - for (int i = 0; i < 10; i++) - last = rand.next(); - - rand.advance(-11); - Assert.assertEquals(first, rand.next()); - - rand.advance(9); - Assert.assertEquals(last, rand.next()); - Assert.assertEquals(first, rand.nextAt(0)); - Assert.assertEquals(last, rand.nextAt(10)); - Assert.assertEquals(-10, rand.distance(first)); - } - - @Test - public void shuffleUnshuffleTest() - { - Random rnd = new Random(); - for (int i = 0; i < RUNS; i++) - { - long a = rnd.nextLong(); - Assert.assertEquals(a, PCGFastPure.unshuffle(PCGFastPure.shuffle(a))); - } - } - - @Test - public void testIntBetween() - { - EntropySource rng = new PcgRSUFast(System.currentTimeMillis(), 0); - - int a = 0; - int b = 50; - int[] cardinality = new int[b - a]; - for (int i = 0; i < RUNS; i++) - { - int min = Math.min(a, b); - int max = Math.max(a, b); - cardinality[rng.nextInt(min, max - 1) - min]++; - } - - // Extremely improbable yet possible that some of the values won't be generated - for (int i = 0; i < cardinality.length; i++) - Assert.assertTrue(cardinality[i] > 0); - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/gen/SurjectionsTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/gen/SurjectionsTest.java deleted file mode 100644 index b749c4140a..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/gen/SurjectionsTest.java +++ /dev/null @@ -1,63 +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.fuzz.harry.gen; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.gen.rng.PcgRSUFast; - -public class SurjectionsTest -{ - private static int RUNS = 1000000; - - @Test - public void weightedTest() - { - int[] weights = new int[] {50, 40, 10}; - - Surjections.Surjection gen = Surjections.weighted(Surjections.weights(weights), - "a", "b", "c"); - - Map frequencies = new HashMap<>(); - EntropySource rng = new PcgRSUFast(System.currentTimeMillis(), 0); - - for (int i = 0; i < RUNS; i++) - { - String s = gen.inflate(rng.next()); - frequencies.compute(s, (s1, i1) -> { - if (i1 == null) - return 1; - else - return i1 + 1; - }); - } - - Assert.assertEquals(frequencies.get("a") / 10000, weights[0], 1); - Assert.assertEquals(frequencies.get("b") / 10000, weights[1], 1); - Assert.assertEquals(frequencies.get("c") / 10000, weights[2], 1); - } - - -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/QuickTheoriesAdapter.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/QuickTheoriesAdapter.java deleted file mode 100644 index ecb3513278..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/QuickTheoriesAdapter.java +++ /dev/null @@ -1,100 +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.fuzz.harry.integration; - -import org.apache.cassandra.harry.gen.Generator; -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.gen.rng.RngUtils; -import org.quicktheories.core.Gen; -import org.quicktheories.core.RandomnessSource; -import org.quicktheories.impl.Constraint; - -public class QuickTheoriesAdapter -{ - public static Gen convert(Generator generator) - { - return new Gen() - { - private final RandomnessSourceAdapter adapter = new RandomnessSourceAdapter<>(); - - public T generate(RandomnessSource randomnessSource) - { - return adapter.generate(randomnessSource, generator); - } - }; - } - - public static class RandomnessSourceAdapter implements EntropySource - { - private RandomnessSource rnd; - - public long next() - { - return rnd.next(Constraint.none()); - } - - public void seed(long seed) - { - throw new RuntimeException("Seed is not settable"); - } - - public EntropySource derive() - { - return new RandomnessSourceAdapter<>(); - } - - public int nextInt() - { - return RngUtils.asInt(next()); - } - - public int nextInt(int max) - { - return RngUtils.asInt(next(), max); - } - - public int nextInt(int min, int max) - { - return RngUtils.asInt(next(), min, max); - } - - public long nextLong(long min, long max) - { - return RngUtils.trim(next(), min, max); - } - - public float nextFloat() - { - return RngUtils.asFloat(next()); - } - - public boolean nextBoolean() - { - return RngUtils.asBoolean(next()); - } - - public T generate(RandomnessSource rnd, Generator generate) - { - this.rnd = rnd; - T value = generate.generate(this); - this.rnd = null; - return value; - } - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/ddl/SchemaGenTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/ddl/SchemaGenTest.java deleted file mode 100644 index c39421fb84..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/ddl/SchemaGenTest.java +++ /dev/null @@ -1,215 +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.fuzz.harry.integration.ddl; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.fuzz.harry.integration.QuickTheoriesAdapter; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Generator; -import org.apache.cassandra.harry.operations.CompiledStatement; - -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.util.TestRunner; -import org.apache.cassandra.cql3.CQLTester; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.utils.Pair; -import org.quicktheories.QuickTheory; -import org.quicktheories.core.Gen; - -import static org.quicktheories.generators.SourceDSL.integers; - -public class SchemaGenTest extends CQLTester -{ - private static final int CYCLES = 10; - - // TODO: compact storage tests - @Test - public void testSelectForwardAndReverseIteration() throws Throwable - { - Generator gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(1, 4) - .clusteringColumnCount(1, 10) - .regularColumnCount(0, 10) - .staticColumnCount(0, 10) - .generator(); - - TestRunner.test(gen, - schemaDefinition -> { - String tableDef = schemaDefinition.compile().cql(); - createTable(tableDef); - - try - { - CompiledStatement statement = Query.selectAllColumns(schemaDefinition, 1, false).toSelectStatement(); - execute(statement.cql(), statement.bindings()); - statement = Query.selectAllColumns(schemaDefinition, 1, true).toSelectStatement(); - execute(statement.cql(), statement.bindings()); - } - catch (Throwable t) - { - throw new AssertionError("Exception caught", t); - } - }); - } - - @Test - public void createTableRoundTrip() throws Throwable - { - Generator gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(1, 10) - .clusteringColumnCount(1, 10) - .regularColumnCount(0, 10) - .staticColumnCount(0, 10) - .generator(); - - TestRunner.test(gen, - schemaDefinition -> { - String tableDef = schemaDefinition.compile().cql(); - createTable(KEYSPACE, tableDef); - TableMetadata tableMetadata = Keyspace.open(KEYSPACE).getColumnFamilyStore(schemaDefinition.table).metadata.get(); - compareColumns(schemaDefinition.partitionKeys, tableMetadata.partitionKeyColumns()); - compareColumns(schemaDefinition.clusteringKeys, tableMetadata.clusteringColumns()); - compareColumns(schemaDefinition.regularColumns, tableMetadata.regularColumns()); - compareColumns(schemaDefinition.staticColumns, tableMetadata.staticColumns()); - }); - } - - @Test - public void testReverseComparator() - { - SchemaSpec spec = new SchemaSpec(KEYSPACE, "tbl1", - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), - ColumnSpec.pk("pk2", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true), - ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)), - Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType), - ColumnSpec.regularColumn("v2", ColumnSpec.asciiType), - ColumnSpec.regularColumn("v3", ColumnSpec.int64Type), - ColumnSpec.regularColumn("v4", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType), - ColumnSpec.staticColumn("static2", ColumnSpec.int64Type))); - - - String tableDef = spec.compile().cql(); - createTable(KEYSPACE, tableDef); - TableMetadata tableMetadata = Keyspace.open(KEYSPACE).getColumnFamilyStore(spec.table).metadata.get(); - compareColumns(spec.partitionKeys, tableMetadata.partitionKeyColumns()); - compareColumns(spec.clusteringKeys, tableMetadata.clusteringColumns()); - compareColumns(spec.regularColumns, tableMetadata.regularColumns()); - compareColumns(spec.staticColumns, tableMetadata.staticColumns()); - } - - - @Test - public void testSchemaGeneration() - { - Gen> ckCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create); - Gen> regCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create); -// Gen> staticCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create); - Gen> pkCounts = integers().between(1, 4).zip(integers().between(0, 6), Pair::create); - - Gen inputs = pkCounts.zip(ckCounts, regCounts, - (pks, cks, regs) -> - new SchemaGenerationInputs(pks.left, pks.left + pks.right, - cks.left, cks.left + cks.right, - regs.left, regs.left + regs.right)); - - Gen> schemaAndInputs = inputs.flatMap(input -> { - Generator gen = new SchemaGenerators.Builder("test") - .partitionKeyColumnCount(input.minPk, input.maxPk) - .clusteringColumnCount(input.minCks, input.maxCks) - .regularColumnCount(input.minRegs, input.maxRegs) - .generator(); - - return QuickTheoriesAdapter.convert(gen).map(schema -> Pair.create(input, schema)); - }); - - qt().forAll(schemaAndInputs) - .check(schemaAndInput -> { - SchemaGenerationInputs input = schemaAndInput.left; - SchemaSpec schema = schemaAndInput.right; - - return schema.partitionKeys.size() <= input.maxPk && schema.partitionKeys.size() >= input.minPk && - schema.clusteringKeys.size() <= input.maxCks && schema.clusteringKeys.size() >= input.minCks && - schema.regularColumns.size() <= input.maxRegs && schema.regularColumns.size() >= input.minRegs; - }); - } - - private static class SchemaGenerationInputs { - private final int minPk; - private final int maxPk; - private final int minCks; - private final int maxCks; - private final int minRegs; - private final int maxRegs; - - public SchemaGenerationInputs(int minPk, int maxPk, int minCks, int maxCks, int minRegs, int maxRegs) - { - this.minPk = minPk; - this.maxPk = maxPk; - this.minCks = minCks; - this.maxCks = maxCks; - this.minRegs = minRegs; - this.maxRegs = maxRegs; - } - } - - private static boolean compareColumns(Collection> expectedColl, Collection actualColl) - { - List> expectedSorted = new ArrayList<>(expectedColl); - expectedSorted.sort(Comparator.comparing(Object::toString)); - List actualSorted = new ArrayList<>(actualColl); - actualSorted.sort(Comparator.comparing(Object::toString)); - - Iterator> expectedIter = expectedSorted.iterator(); - Iterator actualIter = actualSorted.iterator(); - - while (expectedIter.hasNext() && actualIter.hasNext()) - { - ColumnSpec expected = expectedIter.next(); - ColumnMetadata actual = actualIter.next(); - - Assert.assertEquals(expected.kind.toString(), actual.kind.toString()); - Assert.assertEquals(expected.name, actual.name.toString()); - Assert.assertEquals(expected.type.toString(), actual.type.asCQL3Type().toString()); - } - - Assert.assertEquals(String.format("Collections %s and %s have different sizes", expectedColl, actualColl), - expectedIter.hasNext(), actualIter.hasNext()); - return true; - } - - public static QuickTheory qt() - { - return QuickTheory.qt() - .withExamples(CYCLES) - .withShrinkCycles(0); - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderIntegrationTest.java deleted file mode 100644 index bd0d2533a9..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderIntegrationTest.java +++ /dev/null @@ -1,173 +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.fuzz.harry.integration.dsl; - -import java.util.Random; -import java.util.function.Supplier; - -import org.junit.Test; - -import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase; -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.BatchVisitBuilder; -import org.apache.cassandra.harry.dsl.HistoryBuilder; -import org.apache.cassandra.harry.dsl.SingleOperationBuilder; -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.tracker.DataTracker; -import org.apache.cassandra.harry.tracker.DefaultDataTracker; -import org.apache.cassandra.harry.visitors.ReplayingVisitor; - -public class HistoryBuilderIntegrationTest extends IntegrationTestBase -{ - private static final long SEED = 1L; - private static final int STEPS_PER_ITERATION = 1_000; - private static final int MAX_PARTITIONS = 50; - - @Test - public void simpleDSLTest() throws Throwable - { - Supplier supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++) - { - SchemaSpec schema = supplier.get(); - DataTracker tracker = new DefaultDataTracker(); - beforeEach(); - sut.schemaChange(schema.compile().cql()); - - ModelChecker modelChecker = new ModelChecker<>(); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); - - int maxPartitionSize = 100; - modelChecker.init(new HistoryBuilder(SEED, maxPartitionSize, 10, schema, rf)) - .step((history) -> { - history.insert(); - }) - .step((history, rng) -> { - history.insert(rng.nextInt(maxPartitionSize)); - }) - .step((history, rng) -> { - int row = rng.nextInt(maxPartitionSize); - long[] vIdxs = new long[schema.regularColumns.size()]; - for (int j = 0; j < schema.regularColumns.size(); j++) - vIdxs[j] = rng.nextInt(20); - - history.insert(row, vIdxs); - }) - .step((history) -> { - history.deleteRow(); - }) - .step((history, rng) -> { - history.deleteRow(rng.nextInt(maxPartitionSize)); - }) - .step(SingleOperationBuilder::deletePartition) - .step(SingleOperationBuilder::deleteColumns) - .step(SingleOperationBuilder::deleteRowSlice) - .step((history) -> { - history.deleteRowRange(); - }) - .step((history, rng) -> { - history.deleteRowRange(rng.nextInt(maxPartitionSize), - rng.nextInt(maxPartitionSize), - rng.nextBoolean(), - rng.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(tracker, sut, SystemUnderTest.ConsistencyLevel.ALL); - visitor.replayAll(); - - if (historyBuilder.visitedPds().size() < MAX_PARTITIONS) - return false; - - Model model = historyBuilder.quiescentChecker(tracker, sut); - - for (Long pd : historyBuilder.visitedPds()) - model.validate(Query.selectAllColumns(historyBuilder.schema(), pd, false)); - - return true; - }) - .run(STEPS_PER_ITERATION, SEED, new JdkRandomEntropySource(new Random(SEED))); - } - } - - @Test - public void overrideCkTest() throws Throwable - { - Supplier supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - for (int schemaIdx = 0; schemaIdx < SchemaGenerators.DEFAULT_RUNS; schemaIdx++) - { - SchemaSpec schema = supplier.get(); - DataTracker tracker = new DefaultDataTracker(); - beforeEach(); - sut.schemaChange(schema.compile().cql()); - - ModelChecker modelChecker = new ModelChecker<>(); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); - - int maxPartitionSize = 10; - modelChecker.init(new HistoryBuilder(SEED, maxPartitionSize, 10, schema, rf)) - .beforeAll((history, rng) -> { - for (int i = 0; i < MAX_PARTITIONS; i++) - history.forPartition(i).ensureClustering(schema.ckGenerator.inflate(rng.next())); - }) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).insert()) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).insert(rng.nextInt(maxPartitionSize))) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).deleteRow()) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).deleteRow(rng.nextInt(maxPartitionSize))) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).deletePartition()) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).deleteColumns()) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).deleteRowRange()) - .step((history, rng) -> history.visitPartition(rng.nextInt(MAX_PARTITIONS)).deleteRowSlice()) - .exitCondition((history) -> { - ReplayingVisitor visitor = history.visitor(tracker, sut, SystemUnderTest.ConsistencyLevel.ALL); - visitor.replayAll(); - - if (history.visitedPds().size() < MAX_PARTITIONS) - return false; - - Model model = history.quiescentChecker(tracker, sut); - - for (Long pd : history.visitedPds()) - { - model.validate(Query.selectAllColumns(history.schema(), pd, false)); - model.validate(Query.selectAllColumnsWildcard(history.schema(), pd, false)); - } - - - return true; - }) - .run(STEPS_PER_ITERATION, SEED, new JdkRandomEntropySource(new Random(SEED))); - } - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderOverridesIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderOverridesIntegrationTest.java deleted file mode 100644 index a6cfb74cd8..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderOverridesIntegrationTest.java +++ /dev/null @@ -1,359 +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.fuzz.harry.integration.dsl; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Supplier; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase; -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.HistoryBuilder; -import org.apache.cassandra.harry.gen.Bijections; -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; -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.tracker.DataTracker; -import org.apache.cassandra.harry.tracker.DefaultDataTracker; -import org.apache.cassandra.harry.visitors.ReplayingVisitor; - -public class HistoryBuilderOverridesIntegrationTest extends IntegrationTestBase -{ - private static final long SEED = 1L; - - public static SchemaSpec SIMPLE_SCHEMA = new SchemaSpec("harry", - "test_overrides", - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType(4, 10)), - ColumnSpec.pk("pk2", ColumnSpec.int64Type), - ColumnSpec.pk("pk3", ColumnSpec.int64Type), - ColumnSpec.pk("pk4", ColumnSpec.asciiType(2, 10)) - ), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType(2, 0)), - ColumnSpec.ck("ck2", ColumnSpec.asciiType(2, 0)), - ColumnSpec.ck("ck3", ColumnSpec.int64Type), - ColumnSpec.ck("ck4", ColumnSpec.asciiType(4, 100)), - ColumnSpec.ck("ck5", ColumnSpec.asciiType(8, 100)) - ), - Arrays.asList(ColumnSpec.regularColumn("regular1", ColumnSpec.asciiType(8, 100)), - ColumnSpec.regularColumn("regular2", ColumnSpec.asciiType(8, 100)), - ColumnSpec.regularColumn("regular3", ColumnSpec.asciiType(8, 100)) - ), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType(8, 100)), - ColumnSpec.staticColumn("static2", ColumnSpec.asciiType(8, 100)), - ColumnSpec.staticColumn("static3", ColumnSpec.asciiType(8, 100)) - )); - @Test - public void simpleCkOverrideTest() - { - SchemaSpec schema = SIMPLE_SCHEMA; - - DataTracker tracker = new DefaultDataTracker(); - beforeEach(); - sut.schemaChange(schema.compile().cql()); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); - - HistoryBuilder history = new HistoryBuilder(SEED, 5, 10, schema, rf); - Object[] override = new Object[]{ "", "b", -1L, "c", "d" }; - history.forPartition(1).ensureClustering(override); - for (int i = 0; i < 5; i++) - history.visitPartition(1).insert(i); - - history.visitor(tracker, sut, SystemUnderTest.ConsistencyLevel.ALL).replayAll(); - - Object[][] res = sut.execute(Query.selectAllColumns(history.schema(), history.visitedPds().get(0), false).toSelectStatement(), - SystemUnderTest.ConsistencyLevel.ALL); - int found = 0; - for (Object[] row : res) - { - if (Arrays.equals(override, Arrays.copyOfRange(row, 4, 9))) - found++; - } - Assert.assertEquals("Should have mutated exactly one CK", found, 1); - - history.validateAll(tracker, sut); - } - - @Test - public void ckOverrideSortingTest() - { - for (boolean reverse : new boolean[]{ true, false }) - { - SchemaSpec schema = new SchemaSpec("harry", - "test_overrides" + (reverse ? "_reverse" : ""), - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType(4, 10))), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType(2, 0), reverse)), - Arrays.asList(ColumnSpec.regularColumn("regular1", ColumnSpec.asciiType(8, 100))), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType(8, 100)))); - - DataTracker tracker = new DefaultDataTracker(); - beforeEach(); - sut.schemaChange(schema.compile().cql()); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); - - int partitionSize = 10; - HistoryBuilder history = new HistoryBuilder(SEED, partitionSize, 10, schema, rf); - ReplayingVisitor visitor = history.visitor(tracker, sut, SystemUnderTest.ConsistencyLevel.ALL); - Set foundAt = new HashSet<>(); - for (int pdIdx = 0; pdIdx < 128; pdIdx++) - { - Object[] override = new Object[]{ Character.toString(pdIdx) }; - history.forPartition(pdIdx).ensureClustering(override); - for (int i = 0; i < partitionSize; i++) - history.visitPartition(pdIdx).insert(i); - - visitor.replayAll(); - long visitedPd = history.forPartition(pdIdx).pd(); - { - Object[][] res = sut.execute(Query.selectAllColumns(history.schema(), visitedPd, false).toSelectStatement(), - SystemUnderTest.ConsistencyLevel.ALL); - - int found = 0; - for (int i = 0; i < res.length; i++) - { - Object[] row = res[i]; - if (Arrays.equals(override, Arrays.copyOfRange(row, 1, 2))) - { - found++; - foundAt.add(i); - } - } - Assert.assertEquals("Should have mutated exactly one CK", found, 1); - } - history.validateAll(tracker, sut); - } - Assert.assertEquals(10, foundAt.size()); - } - } - - @Test - public void ckOverrideManySortingTest() - { - int counter = 0; - for (boolean reverse : new boolean[]{ true, false }) - { - for (ColumnSpec.DataType type : new ColumnSpec.DataType[]{ ColumnSpec.asciiType(2, 0), ColumnSpec.int64Type }) - { - SchemaSpec schema = new SchemaSpec("harry", - "test_overrides" + (counter++), - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType(4, 10))), - Arrays.asList(ColumnSpec.ck("ck1", type, reverse)), - Arrays.asList(ColumnSpec.regularColumn("regular1", ColumnSpec.asciiType(8, 100))), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType(8, 100)))); - - DataTracker tracker = new DefaultDataTracker(); - beforeEach(); - sut.schemaChange(schema.compile().cql()); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); - - int partitionSize = 10; - HistoryBuilder history = new HistoryBuilder(SEED, partitionSize, 10, schema, rf); - ReplayingVisitor visitor = history.visitor(tracker, sut, SystemUnderTest.ConsistencyLevel.ALL); - EntropySource rng = new JdkRandomEntropySource(SEED); - for (int pdIdx = 0; pdIdx < 100; pdIdx++) - { - Set overrides = new HashSet<>(); - for (int i = 0; i < 5; i++) - { - Object override = schema.clusteringKeys.get(0).generator().inflate(rng.next()); - try - { - history.forPartition(pdIdx).ensureClustering(new Object[]{ override }); - overrides.add(override); - } - catch (IllegalStateException t) - { - // could not override twice - } - } - - for (int i = 0; i < partitionSize; i++) - history.visitPartition(pdIdx).insert(i); - - visitor.replayAll(); - long visitedPd = history.forPartition(pdIdx).pd(); - { - Object[][] res = sut.execute(Query.selectAllColumns(history.schema(), visitedPd, false).toSelectStatement(), - SystemUnderTest.ConsistencyLevel.ALL); - - int found = 0; - for (int i = 0; i < res.length; i++) - { - Object[] row = res[i]; - Object v = row[1]; - if (overrides.contains(v)) - found++; - } - Assert.assertEquals("Should have mutated exactly one CK", found, overrides.size()); - } - history.validateAll(tracker, sut); - } - } - } - } - - @Test - public void ckOverrideWithDeleteTestSingleColumn() - { - Supplier supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - - int partitionSize = 5; - for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++) - { - SchemaSpec schema = supplier.get(); - beforeEach(); - - DataTracker tracker = new DefaultDataTracker(); - beforeEach(); - sut.schemaChange(schema.compile().cql()); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); - - HistoryBuilder history = new HistoryBuilder(SEED, partitionSize, 1, schema, rf); - ReplayingVisitor visitor = history.visitor(tracker, sut, SystemUnderTest.ConsistencyLevel.ALL); - - EntropySource rng = new JdkRandomEntropySource(SEED); - for (int i = 0; i < partitionSize; i++) - { - history.visitPartition(1, - (e) -> schema.ckGenerator.inflate(rng.next())) - .insert(i); - } - - for (int i = 0; i < partitionSize; i++) - { - history.visitPartition(1) - .deleteRow(i); - - visitor.replayAll(); - history.validateAll(tracker, sut); - } - } - } - - @Test - public void regularAndStaticOverrideTest() - { - for (ColumnSpec.DataType type : new ColumnSpec.DataType[]{ ColumnSpec.asciiType(2, 0), ColumnSpec.int64Type }) - { - SchemaSpec schema = new SchemaSpec("harry", - "test_overrides", - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType(4, 10))), - Arrays.asList(ColumnSpec.ck("ck1", type, false)), - Arrays.asList(ColumnSpec.regularColumn("regular1", ColumnSpec.asciiType(2, 2)), - ColumnSpec.regularColumn("regular2", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType(2, 2)), - ColumnSpec.staticColumn("static2", ColumnSpec.int64Type) - )); - - Map> reGenerators = new HashMap<>(); - reGenerators.put("regular1", ColumnSpec.asciiType(4, 4).generator()); - reGenerators.put("regular2", ColumnSpec.int64Type.generator()); - reGenerators.put("static1", ColumnSpec.asciiType(8, 4).generator()); - reGenerators.put("static2", ColumnSpec.int64Type.generator()); - - DataTracker tracker = new DefaultDataTracker(); - beforeEach(); - sut.schemaChange(schema.compile().cql()); - - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1); - - int partitionSize = 100; - HistoryBuilder history = new HistoryBuilder(SEED, partitionSize, 10, schema, rf); - ReplayingVisitor visitor = history.visitor(tracker, sut, SystemUnderTest.ConsistencyLevel.ALL); - EntropySource rng = new JdkRandomEntropySource(SEED); - - Map> perColumnOverrides = new HashMap<>(); - for (ColumnSpec column : schema.regularColumns) - { - perColumnOverrides.put(column.name, new HashSet<>()); - for (int i = 0; i < partitionSize; i++) - { - Object override = reGenerators.get(column.name).inflate(rng.next()); - history.valueOverrides().override(column.name, i, override); - perColumnOverrides.get(column.name).add(override); - } - } - - for (ColumnSpec column : schema.staticColumns) - { - perColumnOverrides.put(column.name, new HashSet<>()); - for (int i = 0; i < partitionSize; i++) - { - Object override = reGenerators.get(column.name).inflate(rng.next()); - history.valueOverrides().override(column.name, i, override); - perColumnOverrides.get(column.name).add(override); - } - } - for (int pdIdx = 0; pdIdx < 10; pdIdx++) - { - Map> results = new HashMap<>(); - for (ColumnSpec column : schema.regularColumns) - results.put(column.name, new HashSet<>()); - for (ColumnSpec column : schema.staticColumns) - results.put(column.name, new HashSet<>()); - - for (int i = 0; i < partitionSize; i++) - { - history.visitPartition(pdIdx) - .insert(i, - new long[]{ rng.nextInt(100), rng.nextInt(100) }, - new long[]{ rng.nextInt(100), rng.nextInt(100) }); - } - - visitor.replayAll(); - history.validateAll(tracker, sut); - - long visitedPd = history.forPartition(pdIdx).pd(); - Object[][] res = sut.execute(Query.selectAllColumns(history.schema(), visitedPd, false).toSelectStatement(), - SystemUnderTest.ConsistencyLevel.ALL); - - for (int i = 0; i < res.length; i++) - { - Object[] row = res[i]; - results.get("regular1").add(row[4]); - results.get("regular2").add(row[5]); - results.get("static1").add(row[2]); - results.get("static2").add(row[3]); - } - - for (Map.Entry> e : results.entrySet()) - { - for (Object o : e.getValue()) - { - Assert.assertTrue(String.format("Found a non-overriden value for %s: %s", e.getKey(), e.getValue()), - perColumnOverrides.get(e.getKey()).contains(o)); - } - } - } - } - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/generators/DataGeneratorsIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/generators/DataGeneratorsIntegrationTest.java deleted file mode 100644 index 1ce9994c09..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/generators/DataGeneratorsIntegrationTest.java +++ /dev/null @@ -1,179 +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.fuzz.harry.integration.generators; - -import java.util.Random; -import java.util.concurrent.CompletableFuture; - -import com.google.common.collect.Iterators; -import org.junit.Test; - -import org.apache.cassandra.harry.HarryHelper; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Generator; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.gen.distribution.Distribution; -import org.apache.cassandra.harry.model.NoOpChecker; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.harry.visitors.MutatingRowVisitor; -import org.apache.cassandra.harry.visitors.SingleValidator; -import org.apache.cassandra.harry.util.TestRunner; -import org.apache.cassandra.harry.visitors.Visitor; -import org.apache.cassandra.cql3.CQLTester; -import org.apache.cassandra.cql3.UntypedResultSet; -import org.apache.cassandra.distributed.impl.RowUtil; - -public class DataGeneratorsIntegrationTest extends CQLTester -{ - @Test - public void testTimestampTieResolution() - { - Random rng = new Random(1); - String ks = "test_timestamp_tie_resolution"; - createKeyspace(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", ks)); - int counter = 0; - for (ColumnSpec.DataType dataType : new ColumnSpec.DataType[]{ ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.asciiType, - ColumnSpec.floatType, - ColumnSpec.doubleType }) - { - - - String tbl = "table_" + (counter++); - createTable(String.format("CREATE TABLE %s.%s (pk int PRIMARY KEY, v %s)", - ks, tbl, - dataType)); - for (int i = 0; i < 10_000; i++) - { - long d1 = dataType.generator().adjustEntropyDomain(rng.nextLong()); - long d2 = dataType.generator().adjustEntropyDomain(rng.nextLong()); - for (long d : new long[]{ d1, d2 }) - { - execute(String.format("INSERT INTO %s.%s (pk, v) VALUES (?,?) USING TIMESTAMP 1", ks, tbl), - i, dataType.generator().inflate(d)); - } - - if (dataType.compareLexicographically(d1, d2) > 0) - assertRows(execute(String.format("SELECT v FROM %s.%s WHERE pk=?", ks, tbl), i), - row(dataType.generator().inflate(d1))); - else - assertRows(execute(String.format("SELECT v FROM %s.%s WHERE pk=?", ks, tbl), i), - row(dataType.generator().inflate(d2))); - } - } - } - - @Test - public void queryParseabilityTest() throws Throwable - { - Generator gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(2, 4) - .clusteringColumnCount(1, 4) - .regularColumnCount(1, 4) - .staticColumnCount(1, 4) - .generator(); - - TestRunner.test(gen, - (schema) -> { - try - { - schema.validate(); - } - catch (AssertionError e) - { - return; - } - createTable(schema.compile().cql()); - - - Configuration.ConfigurationBuilder builder = HarryHelper.defaultConfiguration() - .setDataTracker(new Configuration.NoOpDataTrackerConfiguration()) - .setSchemaProvider(new Configuration.FixedSchemaProviderConfiguration(schema)) - .setSUT(CqlTesterSut::new); - - for (OpSelectors.OperationKind kind : OpSelectors.OperationKind.values()) - { - Run run = builder - .setClusteringDescriptorSelector((rng, schema_) -> { - return new OpSelectors.DefaultDescriptorSelector(rng, - OpSelectors.columnSelectorBuilder().forAll(schema_).build(), - OpSelectors.OperationSelector.weighted(Surjections.weights(100), kind), - new Distribution.ConstantDistribution(2), - 100); - }) - .build() - .createRun(); - - Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new); - for (int lts = 0; lts < 100; lts++) - visitor.visit(); - - SingleValidator validator = new SingleValidator(100, run, NoOpChecker::new); - for (int lts = 0; lts < 10; lts++) - validator.visit(lts); - } - }); - } - - public class CqlTesterSut implements SystemUnderTest - { - public boolean isShutdown() - { - return false; - } - - public void shutdown() - { - } - - public void schemaChange(String statement) - { - createTable(statement); - } - - public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) - { - try - { - UntypedResultSet res = DataGeneratorsIntegrationTest.this.execute(statement, bindings); - if (res == null) - return new Object[][]{}; - - return Iterators.toArray(RowUtil.toIter(res), Object[].class); - } - catch (Throwable throwable) - { - throw new RuntimeException(throwable); - } - } - - public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) - { - return CompletableFuture.completedFuture(execute(statement, cl, bindings)); - } - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ConcurrentQuiescentCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ConcurrentQuiescentCheckerIntegrationTest.java deleted file mode 100644 index 192629b026..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ConcurrentQuiescentCheckerIntegrationTest.java +++ /dev/null @@ -1,71 +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.fuzz.harry.integration.model; - -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; - -import org.junit.Test; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.tracker.LockingDataTracker; -import org.apache.cassandra.harry.runner.Runner; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.harry.visitors.QueryLogger; -import org.apache.cassandra.harry.visitors.RandomPartitionValidator; - -import static org.apache.cassandra.harry.core.Configuration.VisitorPoolConfiguration.pool; -import static java.util.Arrays.asList; - -public class ConcurrentQuiescentCheckerIntegrationTest extends ModelTestBase -{ - @Test - public void testConcurrentReadWriteWorkload() throws Throwable - { - Supplier supplier = SchemaGenerators.progression(1); - int writeThreads = 2; - int readThreads = 2; - - for (int i = 0; i < SchemaGenerators.GENERATORS_COUNT; i++) - { - SchemaSpec schema = supplier.get(); - - Configuration config = configuration(i, schema) - .setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};") - .setCreateSchema(true) - .setDropSchema(true) - .setDataTracker(LockingDataTracker::new) - .build(); - - Runner.concurrent(config, - asList(pool("Writer", writeThreads, MutatingVisitor::new), - pool("Reader", readThreads, (run) -> new RandomPartitionValidator(run, modelConfiguration(), QueryLogger.NO_OP))), - 2, TimeUnit.MINUTES) - .run(); - } - } - - @Override - protected Configuration.ModelConfiguration modelConfiguration() - { - return new Configuration.QuiescentCheckerConfig(); - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/InJVMTokenAwareExecutorTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/InJVMTokenAwareExecutorTest.java deleted file mode 100644 index bd7927a13e..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/InJVMTokenAwareExecutorTest.java +++ /dev/null @@ -1,98 +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.fuzz.harry.integration.model; - -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.QuiescentChecker; -import org.apache.cassandra.harry.model.RepairingLocalStateValidator; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.sut.injvm.InJVMTokenAwareVisitExecutor; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.sut.injvm.InJvmSutBase; -import org.apache.cassandra.harry.runner.Runner; -import org.apache.cassandra.harry.runner.UpToLtsRunner; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.Feature; - -public class InJVMTokenAwareExecutorTest extends IntegrationTestBase -{ - private static final Logger logger = LoggerFactory.getLogger(InJVMTokenAwareExecutorTest.class); - - @BeforeClass - public static void before() throws Throwable - { - cluster = Cluster.build() - .withNodes(5) - .withConfig((cfg) -> InJvmSutBase.defaultConfig().accept(cfg.with(Feature.GOSSIP, Feature.NETWORK))) - .createWithoutStarting(); - cluster.setUncaughtExceptionsFilter(t -> { - logger.error("Caught exception, reporting during shutdown. Ignoring.", t); - return true; - }); - cluster.startup(); - cluster = init(cluster); - sut = new InJvmSut(cluster); - } - - @Override - @Before - public void beforeEach() - { - cluster.schemaChange("DROP KEYSPACE IF EXISTS harry"); - cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};"); - } - - @Test - public void testRepair() throws Throwable - { - Supplier schemaGen = SchemaGenerators.progression(1); - for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++) - { - SchemaSpec schema = schemaGen.get(); - Configuration.ConfigurationBuilder builder = sharedConfiguration(cnt, schema); - - Configuration configuration = builder.build(); - Run run = configuration.createRun(); - run.sut.schemaChange(run.schemaSpec.compile().cql()); - - Runner.chain(configuration, - UpToLtsRunner.factory(MutatingVisitor.factory(InJVMTokenAwareVisitExecutor.factory(new Configuration.MutatingRowVisitorConfiguration(), - SystemUnderTest.ConsistencyLevel.NODE_LOCAL, - new TokenPlacementModel.SimpleReplicationFactor(3))), - 10_000, 2, TimeUnit.SECONDS), - Runner.single(RepairingLocalStateValidator.factoryForTests(5, QuiescentChecker::new))) - .run(); - } - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/IntegrationTestBase.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/IntegrationTestBase.java deleted file mode 100644 index 7a6a926bed..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/IntegrationTestBase.java +++ /dev/null @@ -1,122 +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.fuzz.harry.integration.model; - -import java.util.function.Consumer; -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.distributed.api.IInstanceConfig; -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 - { - init(1, InJvmSutBase.defaultConfig()); - } - - protected static void init(int nodes, Consumer cfg) throws Throwable - { - cluster = Cluster.build() - .withNodes(nodes) - .withConfig(cfg) - .createWithoutStarting(); - cluster.setUncaughtExceptionsFilter(t -> { - logger.error("Caught exception, reporting during shutdown. Ignoring.", t); - return true; - }); - cluster.startup(); - cluster = init(cluster); - sut = new InJvmSut(cluster); - } - @AfterClass - public static void afterClass() - { - sut.shutdown(); - } - - @Before - public void beforeEach() - { - cluster.schemaChange("DROP KEYSPACE IF EXISTS harry"); - cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};"); - } - - private static long seed = 0; - public static Supplier sharedConfiguration() - { - Supplier specGenerator = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - return () -> { - SchemaSpec schemaSpec = specGenerator.get(); - return sharedConfiguration(seed, schemaSpec); - }; - } - - public static Configuration.CDSelectorConfigurationBuilder sharedCDSelectorConfiguration() - { - return new Configuration.CDSelectorConfigurationBuilder() - .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2)) - .setMaxPartitionSize(100) - .setOperationKindWeights(new Configuration.OperationKindSelectorBuilder() - .addWeight(OpSelectors.OperationKind.DELETE_ROW, 1) - .addWeight(OpSelectors.OperationKind.DELETE_COLUMN, 1) - .addWeight(OpSelectors.OperationKind.DELETE_RANGE, 1) - .addWeight(OpSelectors.OperationKind.DELETE_SLICE, 1) - .addWeight(OpSelectors.OperationKind.DELETE_PARTITION, 1) - .addWeight(OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS, 5) - .addWeight(OpSelectors.OperationKind.INSERT_WITH_STATICS, 20) - .addWeight(OpSelectors.OperationKind.INSERT, 20) - .addWeight(OpSelectors.OperationKind.UPDATE_WITH_STATICS, 25) - .addWeight(OpSelectors.OperationKind.UPDATE, 25) - .build()); - } - - public static Configuration.ConfigurationBuilder sharedConfiguration(long seed, SchemaSpec schema) - { - return new Configuration.ConfigurationBuilder().setSeed(seed) - .setClock(() -> new OffsetClock(100000)) - .setCreateSchema(true) - .setTruncateTable(false) - .setDropSchema(true) - .setSchemaProvider((seed1, sut) -> schema) - .setClusteringDescriptorSelector(sharedCDSelectorConfiguration().build()) - .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(2, 200)) - .setSUT(() -> sut); - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/MockSchema.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/MockSchema.java deleted file mode 100644 index 8d7475be18..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/MockSchema.java +++ /dev/null @@ -1,117 +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.fuzz.harry.integration.model; - -import java.util.Arrays; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.util.BitSet; - -public class MockSchema -{ - public static final String KEYSPACE = "harry"; - public static final Surjections.Surjection columnMaskSelector1; - public static final Surjections.Surjection columnMaskSelector2; - public static final Surjections.Surjection compactColumnMaskSelector; - - public static final SchemaSpec tbl1; - public static final SchemaSpec tbl2; - public static final SchemaSpec compact_schema; - - static - { - columnMaskSelector1 = Surjections.pick(BitSet.create(0b0000111, 7), - BitSet.create(0b1110000, 7), - BitSet.create(0b1111111, 7)); - - columnMaskSelector2 = Surjections.pick(BitSet.create(0b11, 2), - BitSet.create(0b01, 2), - BitSet.create(0b10, 2)); - - compactColumnMaskSelector = Surjections.pick(BitSet.create(1, 1)); - - tbl1 = new SchemaSpec(KEYSPACE, "tbl1", - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), - ColumnSpec.pk("pk2", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false), - ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)), - Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type), - ColumnSpec.regularColumn("v2", ColumnSpec.int64Type), - // TODO: test boolean values; before that - submit a PR to Cassandra where we add boolean to ByteBufferUtil#objectToBytes - ColumnSpec.regularColumn("v3", ColumnSpec.int32Type), - ColumnSpec.regularColumn("v4", ColumnSpec.asciiType), - ColumnSpec.regularColumn("v5", ColumnSpec.int64Type), - ColumnSpec.regularColumn("v6", ColumnSpec.floatType), - ColumnSpec.regularColumn("v7", ColumnSpec.doubleType)), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType), - ColumnSpec.staticColumn("static2", ColumnSpec.int64Type))); - - tbl2 = new SchemaSpec(KEYSPACE, "tbl2", - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), - ColumnSpec.pk("pk2", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true)), - Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type), - ColumnSpec.regularColumn("v2", ColumnSpec.asciiType)), - Arrays.asList(ColumnSpec.regularColumn("static1", ColumnSpec.int32Type), - ColumnSpec.regularColumn("static2", ColumnSpec.asciiType))); - - compact_schema = new SchemaSpec(KEYSPACE, "tbl3", - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), - ColumnSpec.pk("pk2", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true), - ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)), - Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType)), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType))) - .withCompactStorage(); - } - - public static SchemaSpec randomSchema(String keyspace, String table, long seed) - { - return new SchemaGenerators.Builder(keyspace, () -> table) - .partitionKeySpec(1, 4, -// ColumnSpec.int8Type, -// ColumnSpec.int16Type, -// ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType(4, 10)) - .clusteringKeySpec(1, 4, -// ColumnSpec.int8Type, -// ColumnSpec.int16Type, -// ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType(4, 10)) - .regularColumnSpec(2, 10, - ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType(5, 10)) - .surjection() - .inflate(seed); - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ModelTestBase.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ModelTestBase.java deleted file mode 100644 index 2776aea930..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ModelTestBase.java +++ /dev/null @@ -1,138 +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.fuzz.harry.integration.model; - -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.runner.Runner; -import org.apache.cassandra.harry.runner.UpToLtsRunner; -import org.apache.cassandra.harry.visitors.MutatingRowVisitor; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.harry.visitors.SingleValidator; - -public abstract class ModelTestBase extends IntegrationTestBase -{ - private final int ITERATIONS = 20_000; - - void negativeTest(Function corrupt, BiConsumer validate) throws Throwable - { - Supplier supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++) - { - SchemaSpec schema = supplier.get(); - negativeTest(corrupt, validate, i, schema); - } - } - - void negativeIntegrationTest(Configuration.RunnerConfiguration runnerConfig) throws Throwable - { - Supplier supplier = SchemaGenerators.progression(1); - for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++) - { - SchemaSpec schema = supplier.get(); - Configuration.ConfigurationBuilder builder = configuration(i, schema); - - builder.setClock(new Configuration.ApproximateClockConfiguration((int) TimeUnit.MINUTES.toMillis(10), - 1, TimeUnit.SECONDS)) - .setCreateSchema(false) - .setDropSchema(false) - .setRunner(runnerConfig); - - Configuration config = builder.build(); - Runner runner = config.createRunner(); - - Run run = runner.getRun(); - beforeEach(); - run.sut.schemaChange(run.schemaSpec.compile().cql()); - runner.run(); - } - } - - protected abstract Configuration.ModelConfiguration modelConfiguration(); - - protected SingleValidator validator(Run run) - { - return new SingleValidator(100, run , modelConfiguration()); - } - - public Configuration.ConfigurationBuilder configuration(long seed, SchemaSpec schema) - { - return sharedConfiguration(seed, schema); - } - - void negativeTest(Function corrupt, BiConsumer validate, int counter, SchemaSpec schemaSpec) throws Throwable - { - Configuration config = configuration(counter, schemaSpec) - .setCreateSchema(true) - .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", - schemaSpec.keyspace, cluster.size())) - .setDropSchema(true) - .build(); - - Run run = config.createRun(); - - new Runner.ChainRunner(run, config, - Arrays.asList(writer(ITERATIONS, 2, TimeUnit.MINUTES), - (r, c) -> new Runner.SingleVisitRunner(r, c, Collections.singletonList(this::validator)) { - @Override - public void runInternal() - { - if (!corrupt.apply(run)) - { - System.out.println("Could not corrupt"); - return; - } - try - { - super.runInternal(); - throw new ShouldHaveThrownException(); - } - catch (Throwable t) - { - validate.accept(t, run); - } - } - })).run(); - } - - public static Configuration.RunnerConfiguration writer(long iterations, int runtime, TimeUnit timeUnit) - { - return (run, config) -> { - return new UpToLtsRunner(run, config, - Collections.singletonList((r_) -> new MutatingVisitor(r_, MutatingRowVisitor::new)), - iterations, - runtime, timeUnit); - }; - } - - public static class ShouldHaveThrownException extends AssertionError - { - - } -} - diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorNegativeTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorNegativeTest.java deleted file mode 100644 index 11c65e4ad9..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorNegativeTest.java +++ /dev/null @@ -1,157 +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.fuzz.harry.integration.model; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.function.Supplier; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.corruptor.AddExtraRowCorruptor; -import org.apache.cassandra.harry.corruptor.ChangeValueCorruptor; -import org.apache.cassandra.harry.corruptor.HideRowCorruptor; -import org.apache.cassandra.harry.corruptor.HideValueCorruptor; -import org.apache.cassandra.harry.corruptor.QueryResponseCorruptor; -import org.apache.cassandra.harry.corruptor.ShowValueCorruptor; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.QuiescentChecker; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.harry.visitors.MutatingRowVisitor; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; -import org.apache.cassandra.harry.visitors.Visitor; - -import static org.apache.cassandra.harry.corruptor.QueryResponseCorruptor.SimpleQueryResponseCorruptor; - -@RunWith(Parameterized.class) -public class QuerySelectorNegativeTest extends IntegrationTestBase -{ - private final int CYCLES = 1000; - - private final Random rnd = new Random(); - - private final QueryResponseCorruptorFactory corruptorFactory; - - public QuerySelectorNegativeTest(QueryResponseCorruptorFactory corruptorFactory) - { - this.corruptorFactory = corruptorFactory; - } - - @Parameterized.Parameters - public static Collection source() - { - return Arrays.asList((run) -> new SimpleQueryResponseCorruptor(run.schemaSpec, - run.clock, - ChangeValueCorruptor::new), - (run) -> new SimpleQueryResponseCorruptor(run.schemaSpec, - run.clock, - HideValueCorruptor::new), - (run) -> new SimpleQueryResponseCorruptor(run.schemaSpec, - run.clock, - ShowValueCorruptor::new), - (run) -> new SimpleQueryResponseCorruptor(run.schemaSpec, - run.clock, - HideRowCorruptor::new), - (run) -> new AddExtraRowCorruptor(run.schemaSpec, - run.clock, - run.tracker, - run.descriptorSelector) - ); - } - - interface QueryResponseCorruptorFactory - { - QueryResponseCorruptor create(Run run); - } - - @Test - public void selectRows() - { - Map stats = new HashMap<>(); - Supplier gen = sharedConfiguration(); - - int rounds = SchemaGenerators.DEFAULT_RUNS; - int failureCounter = 0; - outer: - for (int counter = 0; counter < rounds; counter++) - { - beforeEach(); - Configuration config = gen.get() - .setClusteringDescriptorSelector(sharedCDSelectorConfiguration() - .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2)) - .setMaxPartitionSize(2000) - .build()) - .build(); - Run run = config.createRun(); - run.sut.schemaChange(run.schemaSpec.compile().cql()); - System.out.println(run.schemaSpec.compile().cql()); - - Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new); - Model model = new QuiescentChecker(run); - - QueryResponseCorruptor corruptor = this.corruptorFactory.create(run); - - for (int i = 0; i < CYCLES; i++) - visitor.visit(); - - while (true) - { - long verificationLts = rnd.nextInt(1000); - QueryGenerator queryGen = new QueryGenerator(run.schemaSpec, - run.pdSelector, - run.descriptorSelector, - run.rng); - - QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, queryGen); - Query query = querySelector.inflate(verificationLts, counter); - - model.validate(query); - - if (!corruptor.maybeCorrupt(query, run.sut)) - continue; - - try - { - model.validate(query); - Assert.fail("Should've failed"); - } - catch (Throwable t) - { - // expected - failureCounter++; - stats.compute(query.queryKind, (Query.QueryKind kind, Integer cnt) -> cnt == null ? 1 : (cnt + 1)); - continue outer; - } - } - } - - Assert.assertTrue(String.format("Seen only %d failures", failureCounter), - failureCounter > (rounds * 0.8)); - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorTest.java deleted file mode 100644 index 39fe2c89c5..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorTest.java +++ /dev/null @@ -1,163 +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.fuzz.harry.integration.model; - -import java.util.HashSet; -import java.util.Set; -import java.util.function.Supplier; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.QuiescentChecker; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.harry.visitors.MutatingRowVisitor; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; -import org.apache.cassandra.harry.visitors.Visitor; - -import static org.apache.cassandra.harry.gen.DataGenerators.NIL_DESCR; - -public class QuerySelectorTest extends IntegrationTestBase -{ - private static int CYCLES = 300; - - @Test - public void basicQuerySelectorTest() - { - Supplier schemaGen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++) - { - beforeEach(); - SchemaSpec schemaSpec = schemaGen.get(); - int partitionSize = 200; - - int[] fractions = new int[schemaSpec.clusteringKeys.size()]; - int last = partitionSize; - for (int i = fractions.length - 1; i >= 0; i--) - { - fractions[i] = last; - last = last / 2; - } - - Configuration config = sharedConfiguration(cnt, schemaSpec) - .setClusteringDescriptorSelector(sharedCDSelectorConfiguration() - .setMaxPartitionSize(partitionSize) - .setFractions(fractions) - .build()) - .build(); - - Run run = config.createRun(); - run.sut.schemaChange(run.schemaSpec.compile().cql()); - - Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new); - - for (int i = 0; i < CYCLES; i++) - visitor.visit(); - - QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run); - - for (int i = 0; i < CYCLES; i++) - { - Query query = querySelector.inflate(i, i); - - Object[][] results = run.sut.execute(query.toSelectStatement(), SystemUnderTest.ConsistencyLevel.QUORUM); - Set matchingClusterings = new HashSet<>(); - for (Object[] row : results) - { - long cd = SelectHelper.resultSetToRow(run.schemaSpec, - run.clock, - row).cd; - matchingClusterings.add(cd); - } - - // the simplest test there can be: every row that is in the partition and was returned by the query, - // has to "match", every other row has to be a non-match - CompiledStatement selectPartition = SelectHelper.select(run.schemaSpec, run.pdSelector.pd(i, schemaSpec)); - Object[][] partition = run.sut.execute(selectPartition, SystemUnderTest.ConsistencyLevel.QUORUM); - for (Object[] row : partition) - { - long cd = SelectHelper.resultSetToRow(run.schemaSpec, - run.clock, - row).cd; - - // Skip static clustering - if (cd == NIL_DESCR) - continue; - - boolean expected = matchingClusterings.contains(cd); - boolean actual = query.matchCd(cd); - Assert.assertEquals(String.format("Mismatch for clustering: %d. Expected: %s. Actual: %s.\nQuery: %s", - cd, expected, actual, query.toSelectStatement()), - expected, - actual); - } - } - } - } - - @Test - public void querySelectorModelTest() - { - Supplier gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++) - { - SchemaSpec schemaSpec = gen.get(); - int[] fractions = new int[schemaSpec.clusteringKeys.size()]; - int partitionSize = 200; - int last = partitionSize; - for (int i = fractions.length - 1; i >= 0; i--) - { - fractions[i] = last; - last = last / 2; - } - - Configuration config = sharedConfiguration(cnt, schemaSpec) - .setClusteringDescriptorSelector(sharedCDSelectorConfiguration() - .setMaxPartitionSize(partitionSize) - .setFractions(fractions) - .build()) - .build(); - Run run = config.createRun(); - run.sut.schemaChange(run.schemaSpec.compile().cql()); - Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new); - - for (int i = 0; i < CYCLES; i++) - visitor.visit(); - - QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run); - Model model = new QuiescentChecker(run); - - long verificationLts = 10; - for (int i = 0; i < CYCLES; i++) - { - Query query = querySelector.inflate(verificationLts, i); - model.validate(query); - } - } - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentCheckerIntegrationTest.java deleted file mode 100644 index e88dd30d7e..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentCheckerIntegrationTest.java +++ /dev/null @@ -1,224 +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.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.selectAllColumns(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.selectAllColumns(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.selectAllColumns(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.selectAllColumns(run.schemaSpec, - run.pdSelector.pd(CORRUPT_LTS, run.schemaSpec), - false), - run.sut); - }, - (t, run) -> { - String expected = "Returned row state doesn't match the one predicted by the model"; - String expected2 = "Timestamps in the row state don't match ones predicted by the model"; - - if (t.getMessage() != null && - (t.getMessage().contains(expected) || t.getMessage().contains(expected2))) - return; - - throw new AssertionError(String.format("Exception string mismatch.\nExpected error: %s.\nActual error: %s", expected, t.getMessage()), - t); - }); - } - - @Override - protected Configuration.ModelConfiguration modelConfiguration() - { - return new Configuration.QuiescentCheckerConfig(); - } - - public Configuration.ConfigurationBuilder configuration(long seed, SchemaSpec schema) - { - return super.configuration(seed, schema) - .setClusteringDescriptorSelector(sharedCDSelectorConfiguration() - .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2)) - .setMaxPartitionSize(100) - .build()); - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentLocalStateCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentLocalStateCheckerIntegrationTest.java deleted file mode 100644 index 2ebfaa55cd..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentLocalStateCheckerIntegrationTest.java +++ /dev/null @@ -1,79 +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.fuzz.harry.integration.model; - -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; - -import org.junit.BeforeClass; -import org.junit.Test; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.sut.injvm.InJvmSutBase; -import org.apache.cassandra.harry.sut.injvm.QuiescentLocalStateChecker; -import org.apache.cassandra.harry.runner.Runner; -import org.apache.cassandra.harry.visitors.AllPartitionsValidator; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.Feature; - -public class QuiescentLocalStateCheckerIntegrationTest extends ModelTestBase -{ - @BeforeClass - public static void before() throws Throwable - { - cluster = init(Cluster.build() - .withNodes(2) - .withConfig(InJvmSutBase.defaultConfig().andThen((cfg) -> cfg.with(Feature.GOSSIP))) - .start()); - sut = new InJvmSut(cluster); - } - - @Test - public void testQuiescentLocalStateChecker() throws Throwable - { - Supplier supplier = SchemaGenerators.progression(1); - for (int i = 0; i < SchemaGenerators.GENERATORS_COUNT; i++) - { - SchemaSpec schema = supplier.get(); - - Configuration config = configuration(i, schema) - .setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};") - .setCreateSchema(true) - .setDropSchema(true) - .build(); - - Runner.chain(config, - Runner.sequential(MutatingVisitor::new, 2, TimeUnit.SECONDS), - Runner.single(AllPartitionsValidator.factoryForTests(1, QuiescentLocalStateChecker.factory(new TokenPlacementModel.SimpleReplicationFactor(1))))) - .run(); - break; - } - } - - @Override - protected Configuration.ModelConfiguration modelConfiguration() - { - return new Configuration.QuiescentCheckerConfig(); - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ReconcilerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ReconcilerIntegrationTest.java deleted file mode 100644 index 707fc70ca0..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ReconcilerIntegrationTest.java +++ /dev/null @@ -1,93 +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.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.dsl.ValueDescriptorIndexGenerator; -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 static final long SEED = 1; - - @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[]{ ValueDescriptorIndexGenerator.UNSET, ValueDescriptorIndexGenerator.UNSET }, - new long[]{ 1, 1 }); - historyBuilder.validate(1); - - historyBuilder.visitPartition(2).insert(2, - new long[]{ 1, 1 }, - new long[]{ 1, 1 }); - 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[]{ 1, 1 }); - 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[]{ 1, 1 }); - historyBuilder.visitPartition(2).deleteRowRange(1, 3, true, true); - historyBuilder.validate(2); - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/reconciler/SimpleReconcilerTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/reconciler/SimpleReconcilerTest.java deleted file mode 100644 index 3f3d4135e1..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/reconciler/SimpleReconcilerTest.java +++ /dev/null @@ -1,341 +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.fuzz.harry.integration.model.reconciler; - -import java.util.*; - -import org.junit.Test; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.model.QuiescentChecker; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.model.reconciler.PartitionState; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.WriteHelper; -import org.apache.cassandra.harry.util.BitSet; -import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase; - -public class SimpleReconcilerTest extends IntegrationTestBase -{ - public static Surjections.Surjection defaultSchemaSpecGen(String ks, String table) - { - return new SchemaGenerators.Builder(ks, () -> table) - .partitionKeySpec(1, 3, - ColumnSpec.int64Type, - ColumnSpec.asciiType(5, 256)) - .clusteringKeySpec(1, 3, - ColumnSpec.int64Type, - ColumnSpec.asciiType(2, 3), - ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), - ColumnSpec.ReversedType.getInstance(ColumnSpec.asciiType(2, 3))) - .regularColumnSpec(50, 50, - ColumnSpec.int64Type, - ColumnSpec.asciiType(5, 256)) - .staticColumnSpec(50, 50, - ColumnSpec.int64Type, - ColumnSpec.asciiType(4, 256)) - .surjection(); - } - - @Test - public void testStatics() throws Throwable - { - int rowsPerPartition = 50; - SchemaSpec schema = defaultSchemaSpecGen("harry", "tbl").inflate(1); - Configuration config = sharedConfiguration(1, schema).build(); - Run run = config.createRun(); - SyntheticTest test = new SyntheticTest(run.rng, schema); - beforeEach(); - cluster.schemaChange(schema.compile().cql()); - - ModelState state = new ModelState(new HashMap<>()); - InJvmSut sut = (InJvmSut) run.sut; - Random rng = new Random(1); - - int partitionIdx = 0; - - for (int i = 0; i < 100; i++) - { - BitSet subset = BitSet.allUnset(schema.allColumns.size()); - for (int j = 0; j < subset.size(); j++) - { - if (rng.nextBoolean()) - subset.set(j); - } - if (!isValidSubset(schema.allColumns, subset)) - continue; - int pdIdx = partitionIdx++; - long pd = test.pd(pdIdx); - - for (int j = 0; j < 10; j++) - { - int cdIdx = rng.nextInt(rowsPerPartition); - long cd = test.cd(pdIdx, cdIdx); - - long[] vds = run.descriptorSelector.descriptors(pd, cd, state.lts, 0, schema.regularColumns, - schema.regularColumnsMask(), - subset, - schema.regularColumnsOffset); - long[] sds = run.descriptorSelector.descriptors(pd, cd, state.lts, 0, schema.staticColumns, - schema.staticColumnsMask, - subset, - schema.staticColumnsOffset); - - CompiledStatement statement = WriteHelper.inflateUpdate(schema, pd, cd, vds, sds, run.clock.rts(state.lts)); - sut.cluster.coordinator(1).execute(statement.cql(), ConsistencyLevel.QUORUM, statement.bindings()); - - PartitionState partitionState = state.state.get(pd); - if (partitionState == null) - { - partitionState = new PartitionState(pd, -1, schema); - state.state.put(pd, partitionState); - } - - partitionState.writeStaticRow(sds, state.lts); - partitionState.write(cd, vds, state.lts, true); - - state.lts++; - } - } - - // Validate that all partitions correspond to our expectations - for (Long pd : state.state.keySet()) - { - ArrayList clusteringDescriptors = new ArrayList<>(state.state.get(pd).rows().keySet()); - - // TODO: allow sub-selection - // Try different column subsets - for (int i = 0; i < 10; i++) - { - BitSet bitset = BitSet.allUnset(schema.allColumns.size()); - for (int j = 0; j < bitset.size(); j++) - { - if (rng.nextBoolean()) - bitset.set(j); - } - Set> subset = i == 0 ? null : subset(schema.allColumns, bitset); - if (subset != null && !isValidSubset(schema.allColumns, bitset)) - continue; - - int a = rng.nextInt(clusteringDescriptors.size()); - long cd1tmp = clusteringDescriptors.get(a); - long cd2tmp; - int b; - while (true) - { - b = rng.nextInt(clusteringDescriptors.size()); - long tmp = clusteringDescriptors.get(b); - if (tmp != cd1tmp) - { - cd2tmp = tmp; - break; - } - } - - long cd1 = Math.min(cd1tmp, cd2tmp); - long cd2 = Math.max(cd1tmp, cd2tmp); - - for (boolean reverse : new boolean[]{ true, false }) - { - Query query; - - query = Query.selectAllColumns(schema, pd, reverse); - - QuiescentChecker.validate(schema, - run.tracker, - subset, - state.state.get(pd), - SelectHelper.execute(sut, run.clock, query, subset), - query); - - query = Query.singleClustering(schema, pd, cd1, false); - QuiescentChecker.validate(schema, - run.tracker, - subset, - state.state.get(pd).apply(query), - SelectHelper.execute(sut, run.clock, query, subset), - query); - - for (boolean isGt : new boolean[]{ true, false }) - { - for (boolean isEquals : new boolean[]{ true, false }) - { - try - { - query = Query.clusteringSliceQuery(schema, pd, cd1, rng.nextLong(), isGt, isEquals, reverse); - } - catch (IllegalArgumentException impossibleQuery) - { - continue; - } - - QuiescentChecker.validate(schema, - run.tracker, - subset, - state.state.get(pd).apply(query), - SelectHelper.execute(sut, run.clock, query, subset), - query); - } - } - - for (boolean isMinEq : new boolean[]{ true, false }) - { - for (boolean isMaxEq : new boolean[]{ true, false }) - { - try - { - query = Query.clusteringRangeQuery(schema, pd, cd1, cd2, rng.nextLong(), isMinEq, isMaxEq, reverse); - } - catch (IllegalArgumentException impossibleQuery) - { - continue; - } - QuiescentChecker.validate(schema, - run.tracker, - subset, - state.state.get(pd).apply(query), - SelectHelper.execute(sut, run.clock, query, subset), - query); - } - } - } - } - } - } - - public static boolean isValidSubset(List> superset, BitSet bitSet) - { - boolean hasRegular = false; - for (int i = 0; i < superset.size(); i++) - { - ColumnSpec column = superset.get(i); - if (column.kind == ColumnSpec.Kind.PARTITION_KEY && !bitSet.isSet(i)) - return false; - if (column.kind == ColumnSpec.Kind.CLUSTERING && !bitSet.isSet(i)) - return false; - if (column.kind == ColumnSpec.Kind.REGULAR && bitSet.isSet(i)) - hasRegular = true; - } - - return hasRegular; - } - - public static Set> subset(List> superset, BitSet bitSet) - { - Set> subset = new HashSet<>(); - for (int i = 0; i < superset.size(); i++) - { - if (bitSet.isSet(i)) - subset.add(superset.get(i)); - } - - return subset; - } - - public static Set> randomSubset(List> superset, Random e) - { - Set> set = new HashSet<>(); - boolean hadRegular = false; - for (ColumnSpec v : superset) - { - // TODO: allow selecting without partition and clustering key, too - if (e.nextBoolean() || v.kind == ColumnSpec.Kind.CLUSTERING || v.kind == ColumnSpec.Kind.PARTITION_KEY) - { - set.add(v); - hadRegular |= v.kind == ColumnSpec.Kind.REGULAR; - } - } - - // TODO: this is an oversimplification and a workaround for "Invalid restrictions on clustering columns since the UPDATE statement modifies only static columns" - if (!hadRegular) - return randomSubset(superset, e); - - return set; - } - - public static BitSet subsetToBitset(List superset, Set subset) - { - BitSet bitSet = new BitSet.BitSet64Bit(superset.size()); - for (int i = 0; i < superset.size(); i++) - { - if (subset.contains(superset.get(i))) - bitSet.set(i); - } - return bitSet; - } - - public static class ModelState - { - public long lts = 0; - public final Map state; - - public ModelState(Map state) - { - this.state = state; - } - } - - public static class SyntheticTest // TODO: horrible name - { - private static long PD_STREAM = System.nanoTime(); - private final OpSelectors.PureRng rng; - private final SchemaSpec schema; - - public SyntheticTest(OpSelectors.PureRng rng, SchemaSpec schema) - { - this.schema = schema; - this.rng = rng; - } - - public long pd(int pdIdx) - { - long pd = this.rng.randomNumber(pdIdx + 1, PD_STREAM); - long adjusted = schema.adjustPdEntropy(pd); - assert adjusted == pd : "Partition descriptors not utilising all entropy bits are not supported."; - return pd; - } - - public long pdIdx(long pd) - { - return this.rng.sequenceNumber(pd, PD_STREAM) - 1; - } - - public long cd(int pdIdx, int cdIdx) - { - long cd = this.rng.randomNumber(cdIdx + 1, pd(pdIdx)); - long adjusted = schema.adjustCdEntropy(cd); - assert adjusted == cd : "Clustering descriptors not utilising all entropy bits are not supported."; - return cd; - } - - public long cdIdx(long pd) - { - return this.rng.sequenceNumber(pd, PD_STREAM) - 1; - } - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/op/RowVisitorTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/op/RowVisitorTest.java deleted file mode 100644 index 5978f1e877..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/integration/op/RowVisitorTest.java +++ /dev/null @@ -1,90 +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.fuzz.harry.integration.op; - -import java.util.function.Supplier; - -import org.junit.Before; -import org.junit.Test; - -import org.apache.cassandra.harry.core.MetricReporter; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.gen.distribution.Distribution; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.clock.OffsetClock; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.visitors.GeneratingVisitor; -import org.apache.cassandra.harry.visitors.MutatingRowVisitor; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.cql3.CQLTester; - -import static org.apache.cassandra.harry.model.OpSelectors.DefaultDescriptorSelector.DEFAULT_OP_SELECTOR; - -public class RowVisitorTest extends CQLTester -{ - @Before - public void beforeTest() throws Throwable { - super.beforeTest(); - schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", SchemaGenerators.DEFAULT_KEYSPACE_NAME)); - } - - @Test - public void rowWriteGeneratorTest() - { - Supplier specGenerator = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - EntropySource rand = EntropySource.forTests(6371747244598697093L); - - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - - OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10); - - for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++) - { - SchemaSpec schema = specGenerator.get(); - createTable(schema.compile().cql()); - - OpSelectors.DescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng, - new OpSelectors.ColumnSelectorBuilder().forAll(schema) - .build(), - DEFAULT_OP_SELECTOR, - new Distribution.ScaledDistribution(1, 30), - 100); - - Run run = new Run(rng, - new OffsetClock(10000), - pdSelector, - descriptorSelector, - schema, - DataTracker.NO_OP, - SystemUnderTest.NO_OP, - MetricReporter.NO_OP); - - long[] descriptors = new long[4]; - for (int j = 0; j < descriptors.length; j++) - descriptors[j] = rand.next(); - - // some improvement wont hurt here - new GeneratingVisitor(run, new MutatingVisitor(run, MutatingRowVisitor::new)).visit(); - } - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/model/ApproximateClockTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/model/ApproximateClockTest.java deleted file mode 100644 index 4751b4789c..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/model/ApproximateClockTest.java +++ /dev/null @@ -1,180 +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.fuzz.harry.model; - -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.LockSupport; -import java.util.concurrent.locks.ReentrantLock; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.harry.clock.ApproximateClock; - -public class ApproximateClockTest -{ - @Test - public void approximateClockTest() throws InterruptedException - { - ConcurrentHashMap m = new ConcurrentHashMap<>(); - ConcurrentHashMap inverse = new ConcurrentHashMap<>(); - TimeUnit timeUnit = TimeUnit.MILLISECONDS; - int duration = 1000; - int concurrency = 5; - long maxTicks = timeUnit.toMicros(duration) / (4 * concurrency); - - ApproximateClock clock = new ApproximateClock(duration, timeUnit); - - ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1); - ExecutorService executor = Executors.newFixedThreadPool(concurrency); - final Lock lock = new ReentrantLock(); - AtomicReference throwable = new AtomicReference(); - final Condition signalError = lock.newCondition(); - lock.lock(); - for (int i = 0; i < concurrency; i++) - { - executor.submit(() -> { - try - { - int sleepCnt = 0; - - while (!executor.isShutdown() && !Thread.currentThread().isInterrupted()) - { - sleepCnt++; - if (sleepCnt >= maxTicks) - { - LockSupport.parkNanos(timeUnit.toNanos(duration)); - sleepCnt = 0; - } - - if (executor.isShutdown() || Thread.currentThread().isInterrupted()) - return; - - long lts = clock.nextLts(); - - // Make sure to test "history" path - if (lts % 10000 == 0) - { - scheduledExecutor.schedule(() -> { - try - { - long rts = clock.rts(lts); - Assert.assertNull(m.put(lts, rts)); - Assert.assertNull(inverse.put(rts, lts)); - } - catch (Throwable t) - { - throwable.set(t); - signalError.signalAll(); - t.printStackTrace(); - } - }, 2 * duration, timeUnit); - continue; - } - - try - { - long rts = clock.rts(lts); - Assert.assertNull(m.put(lts, rts)); - Assert.assertNull(inverse.put(rts, lts)); - } - catch (Throwable t) - { - throwable.set(t); - signalError.signalAll(); - } - } - } - catch (Throwable t) - { - throwable.set(t); - signalError.signalAll(); - t.printStackTrace(); - } - }); - } - signalError.await(10, TimeUnit.SECONDS); - lock.unlock(); - executor.shutdown(); - Assert.assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS)); - scheduledExecutor.shutdown(); - Assert.assertTrue(scheduledExecutor.awaitTermination(10, TimeUnit.SECONDS)); - Throwable t = throwable.get(); - if (t != null) - throw new AssertionError("Caught an exception while executing", t); - - Assert.assertEquals(m.size(), inverse.size()); - Iterator> iter = m.entrySet().iterator(); - - Map.Entry previous = iter.next(); - while (iter.hasNext()) - { - if (previous == null) - { - previous = iter.next(); - continue; - } - - Map.Entry current = iter.next(); - long lts = current.getKey(); - long rts = current.getValue(); - Assert.assertEquals(String.format("%s and %s sort wrong", previous, current), - Long.compare(previous.getKey(), current.getKey()), - Long.compare(previous.getValue(), current.getValue())); - - Assert.assertEquals(clock.rts(lts), rts); - Assert.assertEquals(clock.lts(rts), lts); - previous = current; - } - } - - @Test - public void approximateClockInvertibilityTest() - { - ConcurrentHashMap m = new ConcurrentHashMap<>(); - TimeUnit timeUnit = TimeUnit.MILLISECONDS; - int duration = 100; - int cycles = 10_000; - ApproximateClock clock = new ApproximateClock(duration, timeUnit); - - for (long i = 0; i < cycles; i++) - { - long lts = clock.nextLts(); - Assert.assertEquals(lts, i); - long rts = clock.rts(lts); - Assert.assertNull(m.put(lts, rts)); - } - - for (Map.Entry entry : m.entrySet()) - { - Assert.assertEquals(entry.getKey(), - Long.valueOf(clock.lts(entry.getValue()))); - } - } - -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/model/OpSelectorsTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/model/OpSelectorsTest.java deleted file mode 100644 index e9984c1ef3..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/model/OpSelectorsTest.java +++ /dev/null @@ -1,418 +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.fuzz.harry.model; - -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Supplier; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.harry.core.MetricReporter; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.gen.distribution.Distribution; -import org.apache.cassandra.harry.clock.OffsetClock; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.visitors.LtsVisitor; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.harry.visitors.OperationExecutor; -import org.apache.cassandra.harry.util.BitSet; -import org.apache.cassandra.harry.visitors.VisitExecutor; - -public class OpSelectorsTest -{ - private static int RUNS = 1000; - private static SchemaSpec SCHEMA = new SchemaSpec("ks", "tbl", - Collections.singletonList(ColumnSpec.pk("pk", ColumnSpec.int64Type)), - Collections.singletonList(ColumnSpec.pk("ck", ColumnSpec.int64Type)), - Collections.emptyList(), Collections.emptyList()); - - @Test - public void testRowDataDescriptorSupplier() - { - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - SchemaSpec schema = new SchemaSpec("ks", "tbl1", - Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), - ColumnSpec.pk("pk2", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false), - ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)), - Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type), - ColumnSpec.regularColumn("v2", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType), - ColumnSpec.staticColumn("static2", ColumnSpec.int64Type))); - OpSelectors.DefaultDescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng, - new OpSelectors.ColumnSelectorBuilder().forAll(schema) - .build(), - OpSelectors.DefaultDescriptorSelector.DEFAULT_OP_SELECTOR, - new Distribution.ScaledDistribution(2, 10), - 50); - - OpSelectors.PdSelector pdSupplier = new OpSelectors.DefaultPdSelector(rng, - 100, - 100); - - for (int lts = 0; lts < RUNS; lts++) - { - long pd = pdSupplier.pd(lts, schema); - - int opsPerLts = descriptorSelector.operationsPerLts(lts); - for (int opId = 0; opId < opsPerLts; opId++) - { - long cd = descriptorSelector.cd(pd, lts, opId, schema); - Assert.assertEquals(opId, descriptorSelector.opId(pd, lts, cd)); - Assert.assertTrue(descriptorSelector.isCdVisitedBy(pd, lts, cd)); - for (int col = 0; col < 10; col++) - { - long vd = descriptorSelector.vd(pd, cd, lts, opId, col); - } - } - } - } - - @Test - public void pdSelectorSymmetryTest() - { - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - Supplier gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - SchemaSpec schema = gen.get(); - - for (long[] positions : new long[][]{ { 0, Long.MAX_VALUE }, { 100, Long.MAX_VALUE }, { 1000, Long.MAX_VALUE } }) - { - for (int repeats = 2; repeats <= 1000; repeats++) - { - for (int windowSize = 2; windowSize <= 10; windowSize++) - { - OpSelectors.DefaultPdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, windowSize, repeats, positions[0], positions[1]); - - Map> m = new HashMap<>(); - final long maxLts = 10_000; - for (long lts = 0; lts <= maxLts; lts++) - { - long pd = pdSelector.pd(lts, schema); - m.computeIfAbsent(pd, (k) -> new ArrayList<>()).add(lts); - } - - for (Long pd : m.keySet()) - { - long currentLts = pdSelector.minLtsFor(pd); - List predicted = new ArrayList<>(); - while (currentLts <= maxLts && currentLts >= 0) - { - predicted.add(currentLts); - currentLts = pdSelector.nextLts(currentLts); - } - Assert.assertEquals(m.get(pd), predicted); - } - - - } - } - } - } - - @Test - public void pdSelectorTest() - { - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - int cycles = 10000; - for (long[] positions : new long[][]{ { 0, Long.MAX_VALUE }, { 100, Long.MAX_VALUE }, { 1000, Long.MAX_VALUE } }) - { - for (int repeats = 2; repeats <= 1000; repeats++) - { - for (int windowSize = 2; windowSize <= 10; windowSize++) - { - OpSelectors.DefaultPdSelector pdSupplier = new OpSelectors.DefaultPdSelector(rng, windowSize, repeats, positions[0], positions[1]); - long[] pds = new long[cycles]; - for (int i = 0; i < cycles; i++) - { - long pd = pdSupplier.pd(i, SCHEMA); - pds[i] = pd; - Assert.assertEquals(pdSupplier.positionFor(i), pdSupplier.positionForPd(pd)); - } - - Set noNext = new HashSet<>(); - for (int i = 0; i < cycles; i++) - { - long nextLts = pdSupplier.nextLts(i); - Assert.assertFalse(noNext.contains(pds[i])); - if (nextLts == -1) - { - noNext.add(nextLts); - } - else if (nextLts < cycles) - { - Assert.assertEquals(pds[(int) nextLts], pdSupplier.pd(i, SCHEMA)); - } - } - - Set noPrev = new HashSet<>(); - for (int i = cycles - 1; i >= 0; i--) - { - long prevLts = pdSupplier.prevLts(i); - Assert.assertFalse(noPrev.contains(pds[i])); - if (prevLts == -1) - { - noPrev.add(prevLts); - } - else if (prevLts >= 0) - { - Assert.assertEquals(pds[(int) prevLts], pdSupplier.pd(i, SCHEMA)); - } - } - - Set seen = new HashSet<>(); - for (int i = 0; i < cycles; i++) - { - long pd = pdSupplier.pd(i, SCHEMA); - if (!seen.contains(pd)) - { - Assert.assertEquals(i, pdSupplier.minLtsAt(pdSupplier.positionFor(i))); - seen.add(pd); - } - } - - for (int i = 0; i < cycles; i++) - { - long pd = pdSupplier.pd(i, SCHEMA); - long maxLts = pdSupplier.maxLtsFor(pd); - Assert.assertEquals(-1, pdSupplier.nextLts(maxLts)); - Assert.assertEquals(pdSupplier.pd(i, SCHEMA), pdSupplier.pd(maxLts, SCHEMA)); - } - } - } - } - } - - @Test - public void ckSelectorTest() - { - Supplier gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER); - for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++) - ckSelectorTest(gen.get()); - } - - public void ckSelectorTest(SchemaSpec schema) - { - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10); - OpSelectors.DescriptorSelector ckSelector = new OpSelectors.DefaultDescriptorSelector(rng, - new OpSelectors.ColumnSelectorBuilder().forAll(schema, Surjections.pick(schema.regularColumnsMask())).build(), - OpSelectors.OperationSelector.weighted(Surjections.weights(10, 10, 40, 40), - OpSelectors.OperationKind.DELETE_ROW, - OpSelectors.OperationKind.DELETE_COLUMN, - OpSelectors.OperationKind.INSERT, - OpSelectors.OperationKind.UPDATE), - new Distribution.ConstantDistribution(10), - 10); - - Map> partitionMap = new HashMap<>(); - CompiledStatement compiledStatement = new CompiledStatement(""); - BiConsumer consumer = (pd, cd) -> { - partitionMap.compute(pd, (pk, list) -> { - if (list == null) - list = new HashSet<>(); - list.add(cd); - return list; - }); - }; - - Run run = new Run(rng, - new OffsetClock(0), - pdSelector, - ckSelector, - schema, - DataTracker.NO_OP, - SystemUnderTest.NO_OP, - MetricReporter.NO_OP); - - LtsVisitor visitor = new MutatingVisitor(run, - (r) -> new OperationExecutor() - { - public CompiledStatement insert(VisitExecutor.WriteOp op) - { - consumer.accept(op.pd(), op.cd()); - return compiledStatement; - } - - public CompiledStatement update(VisitExecutor.WriteOp op) - { - consumer.accept(op.pd(), op.cd()); - return compiledStatement; - } - - public CompiledStatement insertWithStatics(VisitExecutor.WriteStaticOp op) - { - consumer.accept(op.pd(), op.cd()); - return compiledStatement; - } - - public CompiledStatement updateWithStatics(VisitExecutor.WriteStaticOp op) - { - consumer.accept(op.pd(), op.cd()); - return compiledStatement; - } - - public CompiledStatement deleteColumn(VisitExecutor.DeleteColumnsOp op) - { - consumer.accept(op.pd(), op.cd()); - return compiledStatement; - } - - public CompiledStatement deleteColumnWithStatics(VisitExecutor.DeleteColumnsOp op) - { - consumer.accept(op.pd(), op.cd()); - return compiledStatement; - } - - public CompiledStatement deleteRow(VisitExecutor.DeleteRowOp op) - { - consumer.accept(op.pd(), op.cd()); - return compiledStatement; - } - - public CompiledStatement deletePartition(VisitExecutor.DeleteOp op) - { - // ignore - return compiledStatement; - } - - public CompiledStatement deleteRange(VisitExecutor.DeleteOp op) - { - // ignore - return compiledStatement; - } - - public CompiledStatement deleteSlice(VisitExecutor.DeleteOp op) - { - // ignore - return compiledStatement; - } - }); - - for (int lts = 0; lts < 1000; lts++) - visitor.visit(); - - for (Collection value : partitionMap.values()) - Assert.assertEquals(10, value.size()); - } - - @Test - public void hierarchicalDescriptorSelector() - { - SchemaSpec schema = new SchemaSpec("ks", "tbl1", - Collections.singletonList(ColumnSpec.pk("pk1", ColumnSpec.asciiType)), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType), - ColumnSpec.ck("ck2", ColumnSpec.asciiType), - ColumnSpec.ck("ck3", ColumnSpec.asciiType)), - Collections.singletonList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType)), - Collections.emptyList()); - - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - OpSelectors.DescriptorSelector ckSelector = new OpSelectors.HierarchicalDescriptorSelector(rng, - new int[] {10, 20}, - OpSelectors.columnSelectorBuilder().forAll(schema, Surjections.pick(BitSet.allUnset(0))).build(), - OpSelectors.OperationSelector.weighted(Surjections.weights(10, 10, 40, 40), - OpSelectors.OperationKind.DELETE_ROW, - OpSelectors.OperationKind.DELETE_COLUMN, - OpSelectors.OperationKind.INSERT, - OpSelectors.OperationKind.UPDATE), - new Distribution.ConstantDistribution(10), - 100); - - Set ck1 = new TreeSet<>(); - Set ck2 = new TreeSet<>(); - Set ck3 = new TreeSet<>(); - for (int i = 0; i < 1000; i++) - { - long[] part = schema.ckGenerator.slice(ckSelector.cd(0, i, 0, schema)); - ck1.add(part[0]); - ck2.add(part[1]); - ck3.add(part[2]); - } - Assert.assertEquals(10, ck1.size()); - Assert.assertEquals(20, ck2.size()); - Assert.assertEquals(100, ck3.size()); - } - - @Test - public void testWeights() - { - Map config = new EnumMap<>(OpSelectors.OperationKind.class); - config.put(OpSelectors.OperationKind.DELETE_RANGE, 1); - config.put(OpSelectors.OperationKind.DELETE_SLICE, 1); - config.put(OpSelectors.OperationKind.DELETE_ROW, 1); - config.put(OpSelectors.OperationKind.DELETE_COLUMN, 1); - config.put(OpSelectors.OperationKind.DELETE_PARTITION, 1); - config.put(OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS, 1); - config.put(OpSelectors.OperationKind.UPDATE, 500); - config.put(OpSelectors.OperationKind.INSERT, 500); - config.put(OpSelectors.OperationKind.UPDATE_WITH_STATICS, 500); - config.put(OpSelectors.OperationKind.INSERT_WITH_STATICS, 500); - - int[] weights = new int[config.size()]; - for (int i = 0; i < config.values().size(); i++) - weights[i] = config.get(OpSelectors.OperationKind.values()[i]); - OpSelectors.OperationSelector selector = OpSelectors.OperationSelector.weighted(Surjections.weights(weights), - OpSelectors.OperationKind.values()); - - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1); - OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10); - OpSelectors.DescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng, - null, - selector, - new Distribution.ConstantDistribution(10), - 100); - - EnumMap m = new EnumMap(OpSelectors.OperationKind.class); - for (int lts = 0; lts < 1000000; lts++) - { - int total = descriptorSelector.operationsPerLts(lts); - long pd = pdSelector.pd(lts, SCHEMA); - for (int opId = 0; opId < total; opId++) - { - m.compute(descriptorSelector.operationType(pd, lts, opId), - (OpSelectors.OperationKind k, Integer old) -> { - if (old == null) return 1; - else return old + 1; - }); - } - } - - for (OpSelectors.OperationKind l : OpSelectors.OperationKind.values()) - { - for (OpSelectors.OperationKind r : OpSelectors.OperationKind.values()) - { - if (l != r) - { - Assert.assertEquals(m.get(l) * 1.0 / m.get(r), - config.get(l) * 1.0 / config.get(r), - (config.get(l) * 1.0 / config.get(r)) * 0.10); - } - } - } - } -} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/operations/RelationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/operations/RelationTest.java deleted file mode 100644 index 975a87a13e..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/operations/RelationTest.java +++ /dev/null @@ -1,204 +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.fuzz.harry.operations; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.fuzz.harry.gen.DataGeneratorsTest; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; -import org.apache.cassandra.harry.util.BitSet; - -public class RelationTest -{ - private static int RUNS = 50; - - @Test - public void testKeyGenerators() - { - for (int size = 1; size < 5; size++) - { - Iterator iter = DataGeneratorsTest.permutations(size, - ColumnSpec.DataType.class, - ColumnSpec.int8Type, - ColumnSpec.asciiType, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType - ); - while (iter.hasNext()) - { - ColumnSpec.DataType[] types = iter.next(); - List> spec = new ArrayList<>(types.length); - for (int i = 0; i < types.length; i++) - spec.add(ColumnSpec.ck("r" + i, types[i], false)); - - SchemaSpec schemaSpec = new SchemaSpec("ks", - "tbl", - Collections.singletonList(ColumnSpec.pk("pk", ColumnSpec.int64Type)), - spec, - Collections.emptyList(), - Collections.emptyList()); - - long[] cds = new long[RUNS]; - - int[] fractions = new int[schemaSpec.clusteringKeys.size()]; - int last = cds.length; - for (int i = fractions.length - 1; i >= 0; i--) - { - fractions[i] = last; - last = last / 2; - } - - for (int i = 0; i < cds.length; i++) - { - long cd = OpSelectors.HierarchicalDescriptorSelector.cd(i, fractions, schemaSpec, new OpSelectors.PCGFast(1L), 1L); - cds[i] = schemaSpec.adjustPdEntropy(cd); - } - Arrays.sort(cds); - - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1L); - - // TODO: replace with mocks? - QueryGenerator querySelector = new QueryGenerator(schemaSpec, - new OpSelectors.PdSelector() - { - protected long pd(long lts) - { - return lts; - } - - public long nextLts(long lts) - { - throw new RuntimeException("not implemented"); - } - - public long prevLts(long lts) - { - throw new RuntimeException("not implemented"); - } - - public long maxLtsFor(long pd) - { - throw new RuntimeException("not implemented"); - } - - public long minLtsAt(long position) - { - throw new RuntimeException("not implemented"); - } - - - public long minLtsFor(long pd) - { - throw new RuntimeException("not implemented"); - } - - public long maxPosition(long maxLts) - { - throw new RuntimeException("not implemented"); - } - }, - new OpSelectors.DescriptorSelector() - { - public int operationsPerLts(long lts) - { - throw new RuntimeException("not implemented"); - } - - public int maxPartitionSize() - { - throw new RuntimeException("not implemented"); - } - - public boolean isCdVisitedBy(long pd, long lts, long cd) - { - throw new RuntimeException("not implemented"); - } - - protected long cd(long pd, long lts, long opId) - { - throw new RuntimeException("not implemented"); - } - - public long randomCd(long pd, long entropy) - { - return Math.abs(rng.prev(entropy)) % cds.length; - } - - protected long vd(long pd, long cd, long lts, long opId, int col) - { - throw new RuntimeException("not implemented"); - } - - public OpSelectors.OperationKind operationType(long pd, long lts, long opId) - { - throw new RuntimeException("not implemented"); - } - - public BitSet columnMask(long pd, long lts, long opId, OpSelectors.OperationKind opType) - { - throw new RuntimeException("not implemented"); - } - - public long opId(long pd, long lts, long cd) - { - return 0; - } - }, - rng); - - QueryGenerator.TypedQueryGenerator gen = new QueryGenerator.TypedQueryGenerator(rng, querySelector); - - try - { - for (int i = 0; i < RUNS; i++) - { - Query query = gen.inflate(i, 0); - for (int j = 0; j < cds.length; j++) - { - long cd = schemaSpec.ckGenerator.adjustEntropyDomain(cds[i]); - // the only thing we care about here is that query - Assert.assertEquals(String.format("Error caught while running a query %s with cd %d", - query, cd), - Query.simpleMatch(query, cd), - query.matchCd(cd)); - } - } - } - catch (Throwable t) - { - throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t); - } - } - } - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/runner/LockingDataTrackerTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/runner/LockingDataTrackerTest.java deleted file mode 100644 index 94b9f2e131..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/runner/LockingDataTrackerTest.java +++ /dev/null @@ -1,133 +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.fuzz.harry.runner; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.concurrent.ExecutorFactory; -import org.apache.cassandra.concurrent.Interruptible; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.runner.Runner; -import org.apache.cassandra.harry.tracker.LockingDataTracker; -import org.apache.cassandra.utils.concurrent.WaitQueue; - -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; - -public class LockingDataTrackerTest -{ - @Test - public void testDataTracker() throws Throwable - { - SchemaSpec schemaSpec = SchemaGenerators.defaultSchemaSpecGen("test").inflate(1L); - OpSelectors.PureRng rng = new OpSelectors.PCGFast(1L); - OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 5, 2); - LockingDataTracker tracker = new LockingDataTracker(pdSelector, schemaSpec); - - AtomicReference excluded = new AtomicReference<>(State.UNLOCKED); - AtomicInteger readers = new AtomicInteger(0); - AtomicInteger writers = new AtomicInteger(0); - - WaitQueue queue = WaitQueue.newWaitQueue(); - WaitQueue.Signal interrupt = queue.register(); - List errors = new CopyOnWriteArrayList<>(); - - long lts = 1; - long pd = pdSelector.pd(lts, schemaSpec); - int parallelism = 2; - for (int i = 0; i < parallelism; i++) - { - ExecutorFactory.Global.executorFactory().infiniteLoop("write-" + i, Runner.wrapInterrupt(state -> { - try - { - if (state == Interruptible.State.NORMAL) - { - tracker.beginModification(lts); - Assert.assertEquals(0, readers.get()); - writers.incrementAndGet(); - excluded.updateAndGet((prev) -> { - assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_WRITE) : prev; - return State.LOCKED_FOR_WRITE; - }); - Assert.assertEquals(0, readers.get()); - excluded.updateAndGet((prev) -> { - assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_WRITE) : prev; - return State.UNLOCKED; - }); - Assert.assertEquals(0, readers.get()); - writers.decrementAndGet(); - tracker.endModification(lts); - } - } - catch (Throwable t) - { - t.printStackTrace(); - throw t; - } - }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); - } - - for (int i = 0; i < parallelism; i++) - { - ExecutorFactory.Global.executorFactory().infiniteLoop("read-" + i, Runner.wrapInterrupt(state -> { - try - { - if (state == Interruptible.State.NORMAL) - { - tracker.beginValidation(pd); - Assert.assertEquals(0, writers.get()); - readers.incrementAndGet(); - excluded.updateAndGet((prev) -> { - assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_READ) : prev; - return State.LOCKED_FOR_READ; - }); - Assert.assertEquals(0, writers.get()); - excluded.updateAndGet((prev) -> { - assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_READ) : prev; - return State.UNLOCKED; - }); - Assert.assertEquals(0, writers.get()); - readers.decrementAndGet(); - tracker.endValidation(pd); - } - } - catch (Throwable t) - { - t.printStackTrace(); - throw t; - } - }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); - } - - interrupt.await(1, TimeUnit.MINUTES); - if (!errors.isEmpty()) - Runner.mergeAndThrow(errors); - } - enum State { UNLOCKED, LOCKED_FOR_READ, LOCKED_FOR_WRITE } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/util/RangesTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/util/RangesTest.java deleted file mode 100644 index 38231ee249..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/harry/util/RangesTest.java +++ /dev/null @@ -1,102 +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.fuzz.harry.util; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Random; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.harry.util.DescriptorRanges; - -public class RangesTest -{ - - @Test - public void simpleRangesTest() - { - List list = Arrays.asList(inclusiveRange(10, 20, 10), - inclusiveRange(40, 50, 10), - inclusiveRange(60, 70, 10), - inclusiveRange(80, 90, 10)); - Collections.shuffle(list); - DescriptorRanges ranges = new DescriptorRanges(list); - - Assert.assertTrue(ranges.isShadowed(10, 5)); - Assert.assertFalse(ranges.isShadowed(10, 20)); - Assert.assertFalse(ranges.isShadowed(15, 20)); - Assert.assertTrue(ranges.isShadowed(49, 5)); - Assert.assertFalse(ranges.isShadowed(55, 5)); - Assert.assertFalse(ranges.isShadowed(50, 20)); - Assert.assertTrue(ranges.isShadowed(90, 9)); - } - - @Test - public void randomizedRangesTest() - { - for (int i = 0; i < 1000; i++) - _randomizedRangesTest(); - } - - private void _randomizedRangesTest() - { - List rangesList = new ArrayList<>(); - Random rnd = new Random(); - for (int i = 0; i < 100; i++) - { - long a = rnd.nextInt(1000); - long b = rnd.nextInt(1000); - DescriptorRanges.DescriptorRange range = new DescriptorRanges.DescriptorRange(Math.min(a, b), - Math.max(a,b), - rnd.nextBoolean(), - rnd.nextBoolean(), - rnd.nextInt(1000)); - rangesList.add(range); - } - DescriptorRanges ranges = new DescriptorRanges(rangesList); - - for (int i = 0; i < 10000; i++) - { - long descriptor = rnd.nextLong(); - long ts = rnd.nextInt(1000); - Assert.assertEquals(matchLinear(rangesList, descriptor, ts), - ranges.isShadowed(descriptor, ts)); - - } - } - - public boolean matchLinear(List ranges, long descriptor, long ts) - { - for (DescriptorRanges.DescriptorRange range : ranges) - { - if (range.contains(descriptor, ts)) - return true; - } - return false; - } - - public DescriptorRanges.DescriptorRange inclusiveRange(long start, long end, long ts) - { - return new DescriptorRanges.DescriptorRange(start, end, true, true, 10); - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java index f5f5ef0488..c6e8db78b1 100644 --- a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java @@ -22,6 +22,8 @@ import java.util.concurrent.Callable; import org.junit.Assert; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Constants; @@ -30,13 +32,19 @@ 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.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.harry.SchemaSpec; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.HistoryBuilderHelper; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.execution.RingAwareInJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.metrics.TCMMetrics; import org.apache.cassandra.net.Verb; import org.apache.cassandra.tcm.Epoch; @@ -46,15 +54,18 @@ import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAf import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; public class ConsistentBootstrapTest extends FuzzTestBase { + private static final Logger logger = LoggerFactory.getLogger(ConsistentBootstrapTest.class); private static int WRITES = 500; @Test public void bootstrapFuzzTest() throws Throwable { - IInvokableInstance cmsInstance = null; + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "bootstrap_fuzz", 1000); + IInvokableInstance forShutdown = null; try (Cluster cluster = builder().withNodes(3) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) @@ -63,53 +74,69 @@ public class ConsistentBootstrapTest extends FuzzTestBase .set("metadata_snapshot_frequency", 5)) .start()) { - cmsInstance = cluster.get(1); + IInvokableInstance cmsInstance = cluster.get(1); + forShutdown = cmsInstance; waitForCMSToQuiesce(cluster, cmsInstance); - 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(harry.schema().compile().cql(), ConsistencyLevel.ALL); - waitForCMSToQuiesce(cluster, cluster.get(1)); - Runnable writeAndValidate = () -> { - System.out.println("Starting write phase..."); - for (int i = 0; i < WRITES; i++) - harry.insert(); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), 1000))); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); - System.out.println("Starting validate phase..."); - harry.validateAll(harry.quiescentLocalChecker()); - }; - writeAndValidate.run(); + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + Runnable writeAndValidate = () -> { + for (int i = 0; i < WRITES; i++) + HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history); - IInstanceConfig config = cluster.newInstanceConfig() - .set("auto_bootstrap", true) - .set(Constants.KEY_DTEST_FULL_STARTUP, true); - IInvokableInstance newInstance = cluster.bootstrap(config); + for (int pk : pkGen.generated()) + history.selectPartition(pk); + }; - // Prime the CMS node to pause before the finish join event is committed - Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareJoin.FinishJoin); - new Thread(() -> newInstance.startup()).start(); - pending.call(); + history.customThrowing(() -> { + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", KEYSPACE)); + cluster.schemaChange(schema.compile()); + waitForCMSToQuiesce(cluster, cmsInstance); + }, "Setup"); - writeAndValidate.run(); + writeAndValidate.run(); - // Make sure there can be only one FinishJoin in flight - waitForCMSToQuiesce(cluster, cmsInstance); - // set expectation of finish join & retrieve the sequence when it gets committed - Callable bootstrapVisible = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareJoin.FinishJoin && r.isSuccess()); + history.customThrowing(() -> { + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + IInvokableInstance newInstance = cluster.bootstrap(config); - // wait for the cluster to all witness the finish join event - unpauseCommits(cmsInstance); - waitForCMSToQuiesce(cluster, bootstrapVisible.call()); + // Prime the CMS node to pause before the finish join event is committed + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareJoin.FinishJoin); + new Thread(() -> newInstance.startup()).start(); + pending.call(); + }, "Start boostrap"); - writeAndValidate.run(); + writeAndValidate.run(); + + history.customThrowing(() -> { + // Make sure there can be only one FinishJoin in flight + waitForCMSToQuiesce(cluster, cmsInstance); + // set expectation of finish join & retrieve the sequence when it gets committed + Callable bootstrapVisible = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareJoin.FinishJoin && r.isSuccess()); + + // wait for the cluster to all witness the finish join event + unpauseCommits(cmsInstance); + waitForCMSToQuiesce(cluster, bootstrapVisible.call()); + }, "Finish bootstrap"); + writeAndValidate.run(); + + RingAwareInJvmDTestVisitExecutor.replay(RingAwareInJvmDTestVisitExecutor.builder() + .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(2)) + .consistencyLevel(ConsistencyLevel.ALL) + .build(schema, history, cluster), + history); + }); } catch (Throwable t) { - if (cmsInstance != null) - unpauseCommits(cmsInstance); + if (forShutdown != null) + unpauseCommits(forShutdown); throw t; } } @@ -117,7 +144,9 @@ public class ConsistentBootstrapTest extends FuzzTestBase @Test public void coordinatorIsBehindTest() throws Throwable { - IInvokableInstance cmsInstance = null; + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "coordinator_is_behind", 1000); + + IInvokableInstance forShutdown = null; try (Cluster cluster = builder().withNodes(3) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) @@ -126,109 +155,122 @@ public class ConsistentBootstrapTest extends FuzzTestBase .set("metadata_snapshot_frequency", 5)) .start()) { - cmsInstance = cluster.get(1); + IInvokableInstance cmsInstance = cluster.get(1); + forShutdown = cmsInstance; waitForCMSToQuiesce(cluster, cmsInstance); - ReplayingHistoryBuilder harry = HarryHelper.dataGen(new InJvmSut(cluster, () -> 2, (t) -> false) - { - public Object[][] execute(String statement, ConsistencyLevel cl, int coordinator, int pagesize, Object... bindings) - { - try - { - return super.execute(statement, cl, coordinator, pagesize, bindings); - } - catch (Throwable t) - { - // Avoid retries - return new Object[][]{}; - } - } - }, - new TokenPlacementModel.SimpleReplicationFactor(3), - SystemUnderTest.ConsistencyLevel.ALL); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); - cluster.coordinator(1).execute(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", HarryHelper.KEYSPACE), - ConsistencyLevel.ALL); - cluster.coordinator(1).execute(harry.schema().compile().cql(), ConsistencyLevel.ALL); - waitForCMSToQuiesce(cluster, cluster.get(1)); + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); - cluster.filters().verbs(Verb.TCM_REPLICATION.id, - Verb.TCM_FETCH_CMS_LOG_RSP.id, - Verb.TCM_FETCH_PEER_LOG_RSP.id, - Verb.TCM_CURRENT_EPOCH_REQ.id) - .to(2) - .drop() - .on(); + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", KEYSPACE)); + cluster.schemaChange(schema.compile()); + waitForCMSToQuiesce(cluster, cluster.get(1)); - IInstanceConfig config = cluster.newInstanceConfig() - .set("auto_bootstrap", true) - .set(Constants.KEY_DTEST_FULL_STARTUP, true) - .set("progress_barrier_default_consistency_level", "NODE_LOCAL"); - IInvokableInstance newInstance = cluster.bootstrap(config); + cluster.filters().verbs(Verb.TCM_REPLICATION.id, + Verb.TCM_FETCH_CMS_LOG_RSP.id, + Verb.TCM_FETCH_PEER_LOG_RSP.id, + Verb.TCM_CURRENT_EPOCH_REQ.id) + .to(2) + .drop() + .on(); - // 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]; - for (int i = 1; i <= 4; i++) - metricCounts[i - 1] = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindPlacements.getCount()); - Thread thread = new Thread(() -> newInstance.startup()); - thread.start(); - pending.call(); + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true) + .set("progress_barrier_default_consistency_level", "NODE_LOCAL"); + IInvokableInstance newInstance = cluster.bootstrap(config); + // Prime the CMS node to pause before the finish join event is committed + long[] metricCounts = new long[4]; + for (int i = 1; i <= 4; i++) + metricCounts[i - 1] = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindPlacements.getCount()); - boolean triggered = false; - long[] markers = new long[4]; - outer: - for (int i = 0; i < 20; i++) - { - for (int n = 0; n < 4; n++) - markers[n] = cluster.get(n + 1).logs().mark(); + DataTracker tracker = new DataTracker.SequentialDataTracker(); + RingAwareInJvmDTestVisitExecutor executor = RingAwareInJvmDTestVisitExecutor.builder() + .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(2)) + .nodeSelector(i -> 2) + .consistencyLevel(ConsistencyLevel.ALL) + .build(schema, + tracker, + new QuiescentChecker(schema.valueGenerators, tracker, history), + cluster); - try + Thread startup = new Thread(() -> newInstance.startup()); + + history.customThrowing(() -> { + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareJoin.MidJoin); + startup.start(); + pending.call(); + }, "Startup"); + + long[] markers = new long[4]; + history.custom(() -> { + for (int n = 0; n < 4; n++) + markers[n] = cluster.get(n + 1).logs().mark(); + }, "Start grep"); + + outer: + for (int i = 0; i < history.valueGenerators().pkPopulation(); i++) { - harry.insert(); - } - catch (Throwable t) - { - // ignore - } - for (int n = 0; n < markers.length; n++) - { - if ((n + 1) == 2) // skip 2nd node - continue; - - if (!cluster.get(n + 1) - .logs() - .grep(markers[n], "Routing is correct, but coordinator needs to catch-up") - .getResult() - .isEmpty()) + long pd = history.valueGenerators().pkGen().descriptorAt(i); + for (TokenPlacementModel.Replica replica : executor.getReplicasFor(pd)) { - triggered = true; - break outer; + if (cluster.get(1).config().broadcastAddress().toString().contains(replica.node().id())) + { + HistoryBuilderHelper.insertRandomData(schema, i, ckGen.generate(rng), rng, history); + break outer; + } } } - } - Assert.assertTrue("Should have triggered routing exception on the replica", triggered); - boolean metricTriggered = false; - for (int i = 1; i <= 4; i++) - { - long prevMetric = metricCounts[i - 1]; - long newMetric = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindPlacements.getCount()); - if (newMetric - prevMetric > 0) - { - metricTriggered = true; - break; - } - } - Assert.assertTrue("Metric CoordinatorBehindRing should have been bumped by at least one replica", metricTriggered); - cluster.filters().reset(); - unpauseCommits(cmsInstance); - thread.join(); + + history.customThrowing(() -> { + boolean triggered = false; + for (int n = 0; n < markers.length; n++) + { + if ((n + 1) == 2) // skip 2nd node + continue; + + if (!cluster.get(n + 1) + .logs() + .grep(markers[n], "Routing is correct, but coordinator needs to catch-up") + .getResult() + .isEmpty()) + { + triggered = true; + break; + } + } + + Assert.assertTrue("Should have triggered routing exception on the replica", triggered); + boolean metricTriggered = false; + for (int i = 1; i <= 4; i++) + { + long prevMetric = metricCounts[i - 1]; + long newMetric = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindPlacements.getCount()); + if (newMetric - prevMetric > 0) + { + metricTriggered = true; + break; + } + } + Assert.assertTrue("Metric CoordinatorBehindRing should have been bumped by at least one replica", metricTriggered); + + cluster.filters().reset(); + unpauseCommits(cmsInstance); + startup.join(); + }, "Validate triggered"); + + for (Visit visit : history) + executor.execute(visit); + }); } catch (Throwable t) { - if (cmsInstance != null) - unpauseCommits(cmsInstance); + if (forShutdown != null) + unpauseCommits(forShutdown); throw t; } } diff --git a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java index 8c98eb2e2f..218575e7bf 100644 --- a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java @@ -32,16 +32,19 @@ 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.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.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; 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.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.execution.RingAwareInJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.tcm.Epoch; @@ -52,6 +55,7 @@ import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAf import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; import static org.junit.Assert.assertFalse; public class ConsistentLeaveTest extends FuzzTestBase @@ -61,67 +65,82 @@ public class ConsistentLeaveTest extends FuzzTestBase @Test public void decommissionTest() throws Throwable { - IInvokableInstance cmsInstance = null; + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "decomission", 1000); + IInvokableInstance forShutdown = null; try (Cluster cluster = builder().withNodes(3) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) .appendConfig(c -> c.with(Feature.NETWORK)) .start()) { - cmsInstance = cluster.get(1); + IInvokableInstance cmsInstance = cluster.get(1); + forShutdown = cmsInstance; + IInvokableInstance leavingInstance = cluster.get(2); waitForCMSToQuiesce(cluster, cmsInstance); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), 1000))); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); - 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(harry.schema().compile().cql(), ConsistencyLevel.ALL); - waitForCMSToQuiesce(cluster, cmsInstance); + HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + (hb) -> RingAwareInJvmDTestVisitExecutor.builder() + .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(2)) + .consistencyLevel(ConsistencyLevel.ALL) + .retryPolicy(InJvmDTestVisitExecutor.RetryPolicy.RETRY_ON_TIMEOUT) + .nodeSelector(lts -> 1) + .build(schema, hb, cluster)); + Runnable writeAndValidate = () -> { + for (int i = 0; i < WRITES; i++) + history.insert(pkGen.generate(rng), ckGen.generate(rng)); - Runnable writeAndValidate = () -> { - System.out.println("Starting write phase..."); - for (int i = 0; i < WRITES; i++) - harry.insert(); + for (int pk : pkGen.generated()) + history.selectPartition(pk); + }; - System.out.println("Starting validate phase..."); - harry.validateAll(harry.quiescentLocalChecker()); - }; - writeAndValidate.run(); + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", KEYSPACE)); + cluster.schemaChange(schema.compile()); + waitForCMSToQuiesce(cluster, cmsInstance); - // 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(); - pending.call(); + writeAndValidate.run(); - waitForCMSToQuiesce(cluster, cmsInstance); - assertGossipStatus(cluster, leavingInstance.config().num(), "LEAVING"); + history.customThrowing(() -> { + // 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(); + pending.call(); - writeAndValidate.run(); + waitForCMSToQuiesce(cluster, cmsInstance); + assertGossipStatus(cluster, leavingInstance.config().num(), "LEAVING"); + }, "Start Leave"); - // Make sure there can be only one FinishLeave in flight - waitForCMSToQuiesce(cluster, cmsInstance); - // set expectation of finish leave & retrieve the sequence when it gets committed - Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); - Callable finishedLeaving = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareLeave.FinishLeave && r.isSuccess()); - unpauseCommits(cmsInstance); - Epoch nextEpoch = finishedLeaving.call(); - Assert.assertEquals(String.format("Epoch %s should have immediately superseded epoch %s.", nextEpoch, currentEpoch), - nextEpoch.getEpoch(), currentEpoch.getEpoch() + 1); + writeAndValidate.run(); - // wait for the cluster to all witness the finish join event - waitForCMSToQuiesce(cluster, nextEpoch); + history.customThrowing(() -> { + // Make sure there can be only one FinishLeave in flight + waitForCMSToQuiesce(cluster, cmsInstance); + // set expectation of finish leave & retrieve the sequence when it gets committed + Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); + Callable finishedLeaving = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareLeave.FinishLeave && r.isSuccess()); + unpauseCommits(cmsInstance); + Epoch nextEpoch = finishedLeaving.call(); + Assert.assertEquals(String.format("Epoch %s should have immediately superseded epoch %s.", nextEpoch, currentEpoch), + nextEpoch.getEpoch(), currentEpoch.getEpoch() + 1); - assertGossipStatus(cluster, leavingInstance.config().num(), "LEFT"); + // wait for the cluster to all witness the finish join event + waitForCMSToQuiesce(cluster, nextEpoch); - writeAndValidate.run(); + assertGossipStatus(cluster, leavingInstance.config().num(), "LEFT"); + }, "Finish leave"); + + writeAndValidate.run(); + }); } catch (Throwable t) { - if (cmsInstance != null) - unpauseCommits(cmsInstance); + if (forShutdown != null) + unpauseCommits(forShutdown); throw t; } } diff --git a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java index 4ee7bc6487..8fcac18f04 100644 --- a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java @@ -19,28 +19,32 @@ package org.apache.cassandra.fuzz.ring; import java.util.List; -import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.google.common.util.concurrent.Uninterruptibles; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.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.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; 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.execution.RingAwareInJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.tcm.Epoch; @@ -50,82 +54,101 @@ import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAf import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class ConsistentMoveTest extends FuzzTestBase { + private static final Logger logger = LoggerFactory.getLogger(ConsistentMoveTest.class); private static int WRITES = 500; @Test public void moveTest() throws Throwable { - IInvokableInstance cmsInstance = null; + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "move", 1000); + + IInvokableInstance forShutdown = null; try (Cluster cluster = builder().withNodes(3) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) .appendConfig(c -> c.with(Feature.NETWORK)) .start()) { - cmsInstance = cluster.get(1); + IInvokableInstance cmsInstance = cluster.get(1); + forShutdown = cmsInstance; IInvokableInstance movingInstance = cluster.get(2); waitForCMSToQuiesce(cluster, cmsInstance); - 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(harry.schema().compile().cql(), ConsistencyLevel.ALL); - waitForCMSToQuiesce(cluster, cmsInstance); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), 1000))); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); - Runnable writeAndValidate = () -> { - System.out.println("Starting write phase..."); - for (int i = 0; i < WRITES; i++) - harry.insert(); + HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + (hb) -> RingAwareInJvmDTestVisitExecutor.builder() + .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(2)) + .consistencyLevel(ConsistencyLevel.ALL) + .build(schema, hb, cluster)); + Runnable writeAndValidate = () -> { + for (int i = 0; i < WRITES; i++) + history.insert(pkGen.generate(rng), ckGen.generate(rng)); - System.out.println("Starting validate phase..."); - harry.validateAll(harry.quiescentLocalChecker()); - }; - writeAndValidate.run(); + for (int pk : pkGen.generated()) + history.selectPartition(pk); + }; + history.custom(() -> { + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", KEYSPACE)); + cluster.schemaChange(schema.compile()); + waitForCMSToQuiesce(cluster, cmsInstance); + }, "Setup"); - // Make sure there can be only one FinishLeave in flight - waitForCMSToQuiesce(cluster, cmsInstance); + writeAndValidate.run(); - Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareMove.FinishMove); - new Thread(() -> { - Random rng = new Random(1); - movingInstance.runOnInstance(() -> StorageService.instance.move(Long.toString(rng.nextLong()))); - }).start(); - pending.call(); + history.customThrowing(() -> { + // Make sure there can be only one FinishLeave in flight + waitForCMSToQuiesce(cluster, cmsInstance); - assertGossipStatus(cluster, movingInstance.config().num(), "MOVING"); + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareMove.FinishMove); + new Thread(() -> { + logger.info("Executing move..."); + movingInstance.acceptsOnInstance((Long token) -> { + StorageService.instance.move(Long.toString(token)); + }).accept(rng.next()); + }).start(); + pending.call(); - // wait for the cluster to all witness the finish join event - Callable finishedMoving = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareMove.FinishMove && r.isSuccess()); - unpauseCommits(cmsInstance); - Epoch nextEpoch = finishedMoving.call(); - waitForCMSToQuiesce(cluster, nextEpoch); + assertGossipStatus(cluster, movingInstance.config().num(), "MOVING"); - // TODO: rewrite the test to check only PENDING ranges. - writeAndValidate.run(); + // wait for the cluster to all witness the finish join event + Callable finishedMoving = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareMove.FinishMove && r.isSuccess()); + unpauseCommits(cmsInstance); + Epoch nextEpoch = finishedMoving.call(); + waitForCMSToQuiesce(cluster, nextEpoch); + }, "Move"); - int clusterSize = cluster.size(); - List endpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.config().broadcastAddress())).collect(Collectors.toList()); - cluster.forEach(inst -> inst.runOnInstance(() -> { - for (int i = 1; i <= clusterSize; i++) - { - String gossipStatus = Gossiper.instance.getApplicationState(endpoints.get(i - 1), ApplicationState.STATUS_WITH_PORT); - assertTrue(endpoints.get(i - 1) + ": " + gossipStatus, - gossipStatus.contains("NORMAL")); - } - })); + // TODO: rewrite the test to check only PENDING ranges. + writeAndValidate.run(); + + history.custom(() -> { + int clusterSize = cluster.size(); + List endpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.config().broadcastAddress())).collect(Collectors.toList()); + cluster.forEach(inst -> inst.runOnInstance(() -> { + for (int i = 1; i <= clusterSize; i++) + { + String gossipStatus = Gossiper.instance.getApplicationState(endpoints.get(i - 1), ApplicationState.STATUS_WITH_PORT); + assertTrue(endpoints.get(i - 1) + ": " + gossipStatus, + gossipStatus.contains("NORMAL")); + } + })); + }, "Finish"); + }); } catch (Throwable t) { - if (cmsInstance != null) - unpauseCommits(cmsInstance); + if (forShutdown != null) + unpauseCommits(forShutdown); throw t; } } diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/MultiNodeSAITest.java b/test/distributed/org/apache/cassandra/fuzz/sai/MultiNodeSAITest.java index a86472b99a..9ca536921d 100644 --- a/test/distributed/org/apache/cassandra/fuzz/sai/MultiNodeSAITest.java +++ b/test/distributed/org/apache/cassandra/fuzz/sai/MultiNodeSAITest.java @@ -18,88 +18,10 @@ package org.apache.cassandra.fuzz.sai; -import org.junit.Before; -import org.junit.BeforeClass; - -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.test.sai.SAIUtil; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.sut.injvm.InJvmSutBase; - -import static org.apache.cassandra.distributed.api.Feature.GOSSIP; -import static org.apache.cassandra.distributed.api.Feature.NETWORK; - -public class MultiNodeSAITest extends SingleNodeSAITest +public class MultiNodeSAITest extends MultiNodeSAITestBase { - /** - * Chosing a fetch size has implications for how well this test will excercise paging, short-read protection, and - * other important parts of the distributed query apparatus. This should be set low enough to ensure a significant - * number of queries during validation page, but not too low that more expesive queries time out and fail the test. - */ - private static final int FETCH_SIZE = 10; - - @BeforeClass - public static void before() throws Throwable + public MultiNodeSAITest() { - cluster = Cluster.build() - .withNodes(2) - // At lower fetch sizes, queries w/ hundreds or thousands of matches can take a very long time. - .withConfig(InJvmSutBase.defaultConfig().andThen(c -> c.set("range_request_timeout", "180s") - .set("read_request_timeout", "180s") - .set("native_transport_timeout", "180s") - .set("slow_query_log_timeout", "180s") - .with(GOSSIP).with(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 - public Object[][] execute(String cql, ConsistencyLevel cl, Object[] bindings) - { - // The goal here is to make replicas as out of date as possible, modulo the efforts of repair - // and read-repair in the test itself. - if (cql.contains("SELECT")) - return super.execute(cql, ConsistencyLevel.ALL, FETCH_SIZE, bindings); - return super.execute(cql, ConsistencyLevel.NODE_LOCAL, bindings); - } - }; - } - - @Before - public void beforeEach() - { - cluster.schemaChange("DROP KEYSPACE IF EXISTS harry"); - cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};"); - } - - @Override - protected void flush(SchemaSpec schema) - { - cluster.get(1).nodetool("flush", schema.keyspace, schema.table); - cluster.get(2).nodetool("flush", schema.keyspace, schema.table); - } - - @Override - protected void repair(SchemaSpec schema) - { - cluster.get(1).nodetool("repair", schema.keyspace); - } - - @Override - protected void compact(SchemaSpec schema) - { - cluster.get(1).nodetool("compact", schema.keyspace); - cluster.get(2).nodetool("compact", schema.keyspace); - } - - @Override - protected void waitForIndexesQueryable(SchemaSpec schema) - { - SAIUtil.waitForIndexQueryable(cluster, schema.keyspace); + super(); } } \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/MultiNodeSAITestBase.java b/test/distributed/org/apache/cassandra/fuzz/sai/MultiNodeSAITestBase.java new file mode 100644 index 0000000000..045cfc81d9 --- /dev/null +++ b/test/distributed/org/apache/cassandra/fuzz/sai/MultiNodeSAITestBase.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.fuzz.sai; + +import org.junit.Before; +import org.junit.BeforeClass; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.test.sai.SAIUtil; +import org.apache.cassandra.harry.SchemaSpec; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + +public abstract class MultiNodeSAITestBase extends SingleNodeSAITestBase +{ + public MultiNodeSAITestBase() + { + super(); + } + + @BeforeClass + public static void before() throws Throwable + { + cluster = Cluster.build() + .withNodes(2) + // At lower fetch sizes, queries w/ hundreds or thousands of matches can take a very long time. + .withConfig(defaultConfig().andThen(c -> c.set("range_request_timeout", "180s") + .set("read_request_timeout", "180s") + .set("write_request_timeout", "180s") + .set("native_transport_timeout", "180s") + .set("slow_query_log_timeout", "180s") + .with(GOSSIP).with(NETWORK))) + .createWithoutStarting(); + cluster.startup(); + cluster = init(cluster); + } + + @Before + public void beforeEach() + { + cluster.schemaChange("DROP KEYSPACE IF EXISTS harry"); + cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};"); + } + + @Override + protected void flush(SchemaSpec schema) + { + cluster.forEach(i -> i.nodetool("flush", schema.keyspace)); + } + + @Override + protected void repair(SchemaSpec schema) + { + cluster.get(1).nodetool("repair", schema.keyspace); + } + + @Override + protected void compact(SchemaSpec schema) + { + cluster.forEach(i -> i.nodetool("compact", schema.keyspace)); + } + + @Override + protected void waitForIndexesQueryable(SchemaSpec schema) + { + SAIUtil.waitForIndexQueryable(cluster, schema.keyspace); + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/PagingSingleNodeSAITest.java b/test/distributed/org/apache/cassandra/fuzz/sai/PagingSingleNodeSAITest.java deleted file mode 100644 index f11a7cc3b6..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/sai/PagingSingleNodeSAITest.java +++ /dev/null @@ -1,54 +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.fuzz.sai; - -import org.junit.BeforeClass; - -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.sut.injvm.InJvmSutBase; - -public class PagingSingleNodeSAITest extends SingleNodeSAITest -{ - private static final int FETCH_SIZE = 10; - - @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) { - @Override - public Object[][] execute(String cql, ConsistencyLevel cl, Object[] bindings) - { - if (cql.contains("SELECT")) - return super.execute(cql, cl, FETCH_SIZE, bindings); - return super.execute(cql, cl, bindings); - } - }; - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/PagingStaticsTortureTest.java b/test/distributed/org/apache/cassandra/fuzz/sai/PagingStaticsTortureTest.java deleted file mode 100644 index dc81a277cd..0000000000 --- a/test/distributed/org/apache/cassandra/fuzz/sai/PagingStaticsTortureTest.java +++ /dev/null @@ -1,54 +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.fuzz.sai; - -import org.junit.BeforeClass; - -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.sut.injvm.InJvmSutBase; - -public class PagingStaticsTortureTest extends StaticsTortureTest -{ - private static final int FETCH_SIZE = 10; - - @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) { - @Override - public Object[][] execute(String cql, ConsistencyLevel cl, Object[] bindings) - { - if (cql.contains("SELECT")) - return super.execute(cql, cl, FETCH_SIZE, bindings); - return super.execute(cql, cl, bindings); - } - }; - } -} diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITest.java b/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITest.java index bbe4ce8398..37a9b1c184 100644 --- a/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITest.java +++ b/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITest.java @@ -18,290 +18,10 @@ package org.apache.cassandra.fuzz.sai; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.junit.Test; - -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase; -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.gen.EntropySource; -import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; -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.model.reconciler.Reconciler; -import org.apache.cassandra.harry.operations.FilteringQuery; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.Relation; -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 SingleNodeSAITest extends IntegrationTestBase +public class SingleNodeSAITest extends SingleNodeSAITestBase { - private static final int RUNS = 1; - - private static final int OPERATIONS_PER_RUN = 30_000; - private static final int REPAIR_SKIP = OPERATIONS_PER_RUN / 2; - private static final int FLUSH_SKIP = OPERATIONS_PER_RUN / 7; - private static final int VALIDATION_SKIP = OPERATIONS_PER_RUN / 100; - - private static final int NUM_PARTITIONS = OPERATIONS_PER_RUN / 1000; - protected static final int MAX_PARTITION_SIZE = 10_000; - private static final int UNIQUE_CELL_VALUES = 5; - - long seed = 1; - - @Test - public void basicSaiTest() + public SingleNodeSAITest() { - CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.setInt(6); - SchemaSpec schema = new SchemaSpec(KEYSPACE, "tbl1", - Arrays.asList(ColumnSpec.ck("pk1", ColumnSpec.int64Type), - ColumnSpec.ck("pk2", ColumnSpec.asciiType(4, 100)), - ColumnSpec.ck("pk3", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType(4, 100)), - ColumnSpec.ck("ck2", ColumnSpec.asciiType, true), - ColumnSpec.ck("ck3", ColumnSpec.int64Type)), - Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType(40, 100)), - ColumnSpec.regularColumn("v2", ColumnSpec.int64Type), - ColumnSpec.regularColumn("v3", ColumnSpec.int64Type)), - List.of(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType(40, 100)))) - .withCompactionStrategy("LeveledCompactionStrategy"); - - sut.schemaChange(schema.compile().cql()); - sut.schemaChange(schema.cloneWithName(schema.keyspace, schema.table + "_debug").compile().cql()); - sut.schemaChange(String.format("CREATE INDEX %s_sai_idx ON %s.%s (%s) USING 'sai' ", - schema.regularColumns.get(0).name, - schema.keyspace, - schema.table, - schema.regularColumns.get(0).name)); - sut.schemaChange(String.format("CREATE INDEX %s_sai_idx ON %s.%s (%s) USING 'sai';", - schema.regularColumns.get(1).name, - schema.keyspace, - schema.table, - schema.regularColumns.get(1).name)); - sut.schemaChange(String.format("CREATE INDEX %s_sai_idx ON %s.%s (%s) USING 'sai';", - schema.regularColumns.get(2).name, - schema.keyspace, - schema.table, - schema.regularColumns.get(2).name)); - sut.schemaChange(String.format("CREATE INDEX %s_sai_idx ON %s.%s (%s) USING 'sai';", - schema.staticColumns.get(0).name, - schema.keyspace, - schema.table, - schema.staticColumns.get(0).name)); - - waitForIndexesQueryable(schema); - - DataTracker tracker = new DefaultDataTracker(); - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(cluster.size()); - ReplayingHistoryBuilder history = new ReplayingHistoryBuilder(seed, - MAX_PARTITION_SIZE, - MAX_PARTITION_SIZE, - tracker, - sut, - schema, - rf, - SystemUnderTest.ConsistencyLevel.QUORUM); - - for (int run = 0; run < RUNS; run++) - { - logger.info("Starting run {}/{}...", run + 1, RUNS); - EntropySource random = new JdkRandomEntropySource(run); - - // Populate the array of possible values for all operations in the run: - long[] values = new long[UNIQUE_CELL_VALUES]; - for (int i = 0; i < values.length; i++) - values[i] = random.next(); - - for (int i = 0; i < OPERATIONS_PER_RUN; i++) - { - int partitionIndex = random.nextInt(0, NUM_PARTITIONS); - - history.visitPartition(partitionIndex) - .insert(random.nextInt(MAX_PARTITION_SIZE), - new long[] { random.nextBoolean() ? DataGenerators.UNSET_DESCR : values[random.nextInt(values.length)], - random.nextBoolean() ? DataGenerators.UNSET_DESCR : values[random.nextInt(values.length)], - random.nextBoolean() ? DataGenerators.UNSET_DESCR : values[random.nextInt(values.length)] }, - new long[] { random.nextBoolean() ? DataGenerators.UNSET_DESCR : values[random.nextInt(values.length)] }); - - if (random.nextFloat() > 0.99f) - { - int row1 = random.nextInt(MAX_PARTITION_SIZE); - int row2 = random.nextInt(MAX_PARTITION_SIZE); - history.visitPartition(partitionIndex).deleteRowRange(Math.min(row1, row2), Math.max(row1, row2), - random.nextBoolean(), random.nextBoolean()); - } - else if (random.nextFloat() > 0.999f) - { - history.visitPartition(partitionIndex).deleteRowSlice(); - } - - if (random.nextFloat() > 0.995f) - { - history.visitPartition(partitionIndex).deleteColumns(); - } - - if (random.nextFloat() > 0.9995f) - { - history.visitPartition(partitionIndex).deletePartition(); - } - - if (i % REPAIR_SKIP == 0) - { - logger.debug("Repairing/flushing after operation {}...", i); - repair(schema); - } - else if (i % FLUSH_SKIP == 0) - { - logger.debug("Flushing after operation {}...", i); - flush(schema); - } - - if (i % VALIDATION_SKIP != 0) - continue; - - logger.debug("Validating partition at index {} after operation {} in run {}...", partitionIndex, i, run + 1); - - for (int j = 0; j < 10; j++) - { - List relations = new ArrayList<>(); - - // For one text column and 2 numeric columns, we can use between 1 and 5 total relations. - int num = random.nextInt(1, 5); - - List> pick = new ArrayList<>(); - //noinspection ArraysAsListWithZeroOrOneArgument - pick.add(new ArrayList<>(Arrays.asList(Relation.RelationKind.EQ))); // text column supports only EQ - pick.add(new ArrayList<>(Arrays.asList(Relation.RelationKind.EQ, Relation.RelationKind.GT, Relation.RelationKind.LT))); - pick.add(new ArrayList<>(Arrays.asList(Relation.RelationKind.EQ, Relation.RelationKind.GT, Relation.RelationKind.LT))); - - if (random.nextFloat() > 0.75f) - { - relations.addAll(Query.clusteringSliceQuery(schema, - partitionIndex, - random.next(), - random.next(), - random.nextBoolean(), - random.nextBoolean(), - false).relations); - } - - for (int k = 0; k < num; k++) - { - int column = random.nextInt(schema.regularColumns.size()); - Relation.RelationKind relationKind = pickKind(random, pick, column); - - if (relationKind != null) - relations.add(Relation.relation(relationKind, - schema.regularColumns.get(column), - values[random.nextInt(values.length)])); - } - - if (random.nextFloat() > 0.7f) - { - relations.add(Relation.relation(Relation.RelationKind.EQ, - schema.staticColumns.get(0), - values[random.nextInt(values.length)])); - } - - long pd = history.pdSelector().pdAtPosition(partitionIndex); - FilteringQuery query = new FilteringQuery(pd, false, relations, schema); - Reconciler reconciler = new Reconciler(history.pdSelector(), schema, history::visitor); - Set> columns = new HashSet<>(schema.allColumns); - - PartitionState modelState = reconciler.inflatePartitionState(pd, tracker, query).filter(query); - - if (modelState.rows().size() > 0) - logger.debug("Model contains {} matching rows for query {}.", modelState.rows().size(), query); - - try - { - QuiescentChecker.validate(schema, - tracker, - columns, - modelState, - SelectHelper.execute(sut, history.clock(), query), - query); - - // Run the query again to see if the first execution caused an issue via read-repair: - QuiescentChecker.validate(schema, - tracker, - columns, - modelState, - SelectHelper.execute(sut, history.clock(), query), - query); - } - catch (Throwable t) - { - logger.debug("Partition index = {}, run = {}, j = {}, i = {}", partitionIndex, run, j, i); - - Query partitionQuery = Query.selectAllColumns(schema, pd, false); - QuiescentChecker.validate(schema, - tracker, - columns, - reconciler.inflatePartitionState(pd, tracker, partitionQuery), - SelectHelper.execute(sut, history.clock(), partitionQuery), - partitionQuery); - logger.debug("Partition state agrees. Throwing original error..."); - - throw t; - } - } - } - - if (run + 1 < RUNS) - { - logger.debug("Forcing compaction at the end of run {}...", run + 1); - compact(schema); - } - } + super(); } - - protected void flush(SchemaSpec schema) - { - cluster.get(1).nodetool("flush", schema.keyspace, schema.table); - } - - protected void compact(SchemaSpec schema) - { - cluster.get(1).nodetool("compact", schema.keyspace); - } - - protected void repair(SchemaSpec schema) - { - // Repair is nonsensical for a single node, but a repair does flush first, so do that at least. - cluster.get(1).nodetool("flush", schema.keyspace, schema.table); - } - - protected void waitForIndexesQueryable(SchemaSpec schema) {} - - private static Relation.RelationKind pickKind(EntropySource random, List> options, int column) - { - Relation.RelationKind kind = null; - - if (!options.get(column).isEmpty()) - { - List possible = options.get(column); - int chosen = random.nextInt(possible.size()); - kind = possible.remove(chosen); - - if (kind == Relation.RelationKind.EQ) - possible.clear(); // EQ precludes LT and GT - else - possible.remove(Relation.RelationKind.EQ); // LT GT preclude EQ - } - - return kind; - } -} \ No newline at end of file +} diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITestBase.java b/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITestBase.java new file mode 100644 index 0000000000..b14fa6908a --- /dev/null +++ b/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITestBase.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.sai; + + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import com.google.common.collect.Streams; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.HistoryBuilderHelper; +import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.SingleOperationBuilder.IdxRelation; + +// TODO: "WITH OPTIONS = {'case_sensitive': 'false', 'normalize': 'true', 'ascii': 'true'};", +public abstract class SingleNodeSAITestBase extends TestBaseImpl +{ + private static final int OPERATIONS_PER_RUN = 30_000; + private static final int REPAIR_SKIP = OPERATIONS_PER_RUN / 2; + private static final int FLUSH_SKIP = OPERATIONS_PER_RUN / 7; + private static final int COMPACTION_SKIP = OPERATIONS_PER_RUN / 11; + + private static final int NUM_PARTITIONS = OPERATIONS_PER_RUN / 1000; + protected static final int MAX_PARTITION_SIZE = 10_000; + private static final int UNIQUE_CELL_VALUES = 5; + + protected static final Logger logger = LoggerFactory.getLogger(SingleNodeSAITest.class); + protected static Cluster cluster; + + protected SingleNodeSAITestBase() + { + } + + @BeforeClass + public static void before() throws Throwable + { + init(1, + // At lower fetch sizes, queries w/ hundreds or thousands of matches can take a very long time. + defaultConfig().andThen(c -> c.set("range_request_timeout", "180s") + .set("read_request_timeout", "180s") + .set("write_request_timeout", "180s") + .set("native_transport_timeout", "180s") + .set("slow_query_log_timeout", "180s") + .with(GOSSIP).with(NETWORK)) + ); + } + + protected static void init(int nodes, Consumer cfg) throws Throwable + { + cluster = Cluster.build() + .withNodes(nodes) + .withConfig(cfg) + .createWithoutStarting(); + cluster.startup(); + cluster = init(cluster); + } + @AfterClass + public static void afterClass() + { + cluster.close(); + } + + @Before + public void beforeEach() + { + cluster.schemaChange("DROP KEYSPACE IF EXISTS harry"); + cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};"); + } + + @Test + public void simplifiedSaiTest() + { + withRandom(rng -> basicSaiTest(rng, SchemaGenerators.trivialSchema("harry", "simplified", 1000).generate(rng))); + } + + @Test + public void basicSaiTest() + { + Generator schemaGen = schemaGenerator(); + withRandom(rng -> { + basicSaiTest(rng, schemaGen.generate(rng)); + }); + } + + private void basicSaiTest(EntropySource rng, SchemaSpec schema) + { + Set usedPartitions = new HashSet<>(); + logger.info(schema.compile()); + + Generator globalPkGen = Generators.int32(0, Math.min(NUM_PARTITIONS, schema.valueGenerators.pkPopulation())); + Generator ckGen = Generators.int32(0, schema.valueGenerators.ckPopulation()); + + CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.setInt(100); + beforeEach(); + cluster.forEach(i -> i.nodetool("disableautocompaction")); + + cluster.schemaChange(schema.compile()); + cluster.schemaChange(schema.compile().replace(schema.keyspace + "." + schema.table, + schema.keyspace + ".debug_table")); + Streams.concat(schema.clusteringKeys.stream(), + schema.regularColumns.stream(), + schema.staticColumns.stream()) + .forEach(column -> { + cluster.schemaChange(String.format("CREATE INDEX %s_sai_idx ON %s.%s (%s) USING 'sai' ", + column.name, + schema.keyspace, + schema.table, + column.name)); + }); + + waitForIndexesQueryable(schema); + + HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + (hb) -> InJvmDTestVisitExecutor.builder() + .pageSizeSelector(pageSizeSelector(rng)) + .consistencyLevel(consistencyLevelSelector()) + .doubleWriting(schema, hb, cluster, "debug_table")); + List partitions = new ArrayList<>(); + for (int j = 0; j < 5; j++) + { + int picked = globalPkGen.generate(rng); + if (usedPartitions.contains(picked)) + continue; + partitions.add(picked); + } + + usedPartitions.addAll(partitions); + if (partitions.isEmpty()) + return; + + Generator pkGen = Generators.pick(partitions); + for (int i = 0; i < OPERATIONS_PER_RUN; i++) + { + int partitionIndex = pkGen.generate(rng); + HistoryBuilderHelper.insertRandomData(schema, partitionIndex, ckGen.generate(rng), rng, 0.5d, history); + + if (rng.nextFloat() > 0.99f) + { + int row1 = ckGen.generate(rng); + int row2 = ckGen.generate(rng); + history.deleteRowRange(partitionIndex, + Math.min(row1, row2), + Math.max(row1, row2), + rng.nextInt(schema.clusteringKeys.size()), + rng.nextBoolean(), + rng.nextBoolean()); + } + + if (rng.nextFloat() > 0.995f) + HistoryBuilderHelper.deleteRandomColumns(schema, partitionIndex, ckGen.generate(rng), rng, history); + + if (rng.nextFloat() > 0.9995f) + history.deletePartition(partitionIndex); + + if (i % REPAIR_SKIP == 0) + history.custom(() -> repair(schema), "Repair"); + else if (i % FLUSH_SKIP == 0) + history.custom(() -> flush(schema), "Flush"); + else if (i % COMPACTION_SKIP == 0) + history.custom(() -> compact(schema), "Compact"); + + if (i > 0 && i % 1000 == 0) + { + for (int j = 0; j < 5; j++) + { + List regularRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.regularColumns.size(), + column -> Math.min(schema.valueGenerators.regularPopulation(column), UNIQUE_CELL_VALUES)); + List staticRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.staticColumns.size(), + column -> Math.min(schema.valueGenerators.staticPopulation(column), UNIQUE_CELL_VALUES)); + history.select(pkGen.generate(rng), + HistoryBuilderHelper.generateClusteringRelations(rng, schema.clusteringKeys.size(), ckGen).toArray(new IdxRelation[0]), + regularRelations.toArray(new IdxRelation[regularRelations.size()]), + staticRelations.toArray(new IdxRelation[staticRelations.size()])); + } + } + } + } + + protected Generator schemaGenerator() + { + SchemaSpec.OptionsBuilder builder = SchemaSpec.optionsBuilder().compactionStrategy("LeveledCompactionStrategy"); + return SchemaGenerators.schemaSpecGen(KEYSPACE, "basic_sai", MAX_PARTITION_SIZE, builder); + } + + protected void flush(SchemaSpec schema) + { + cluster.get(1).nodetool("flush", schema.keyspace, schema.table); + } + + protected void compact(SchemaSpec schema) + { + cluster.get(1).nodetool("compact", schema.keyspace); + } + + protected void repair(SchemaSpec schema) + { + // Repair is nonsensical for a single node, but a repair does flush first, so do that at least. + cluster.get(1).nodetool("flush", schema.keyspace, schema.table); + } + + protected void waitForIndexesQueryable(SchemaSpec schema) {} + + public static Consumer defaultConfig() + { + return (cfg) -> { + cfg.set("row_cache_size", "50MiB") + .set("index_summary_capacity", "50MiB") + .set("counter_cache_size", "50MiB") + .set("key_cache_size", "50MiB") + .set("file_cache_size", "50MiB") + .set("index_summary_capacity", "50MiB") + .set("memtable_heap_space", "128MiB") + .set("memtable_offheap_space", "128MiB") + .set("memtable_flush_writers", 1) + .set("concurrent_compactors", 1) + .set("concurrent_reads", 5) + .set("concurrent_writes", 5) + .set("compaction_throughput_mb_per_sec", 10) + .set("hinted_handoff_enabled", false); + }; + } + + protected InJvmDTestVisitExecutor.ConsistencyLevelSelector consistencyLevelSelector() + { + return visit -> { + if (visit.selectOnly) + return ConsistencyLevel.ALL; + + // The goal here is to make replicas as out of date as possible, modulo the efforts of repair + // and read-repair in the test itself. + return ConsistencyLevel.NODE_LOCAL; + + }; + } + + protected InJvmDTestVisitExecutor.PageSizeSelector pageSizeSelector(EntropySource rng) + { + // Chosing a fetch size has implications for how well this test will excercise paging, short-read protection, and + // other important parts of the distributed query apparatus. This should be set low enough to ensure a significant + // number of queries during validation page, but not too low that more expesive queries time out and fail the test. + return lts -> rng.nextInt(1, 20); + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java b/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java index e68c204d23..07844b045e 100644 --- a/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java @@ -18,40 +18,30 @@ package org.apache.cassandra.fuzz.sai; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; import org.junit.Test; import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase; -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.distributed.test.IntegrationTestBase; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.SchemaSpec; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; -import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; -import org.apache.cassandra.harry.model.AgainstSutChecker; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.FilteringQuery; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.Relation; -import org.apache.cassandra.harry.sut.DoubleWritingSut; -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; +import org.apache.cassandra.harry.util.BitSet; + +import static org.apache.cassandra.harry.dsl.HistoryBuilderHelper.generateClusteringRelations; +import static org.apache.cassandra.harry.dsl.HistoryBuilderHelper.generateValueRelations; +import static org.apache.cassandra.harry.dsl.SingleOperationBuilder.IdxRelation; public class StaticsTortureTest extends IntegrationTestBase { - private static final long SEED = 1; private static final int MAX_PARTITION_SIZE = 10_000; private static final int NUM_PARTITIONS = 100; private static final int UNIQUE_CELL_VALUES = 5; @@ -61,206 +51,139 @@ public class StaticsTortureTest extends IntegrationTestBase { CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.setInt(6); int idx = 0; - staticsTortureTest(Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType(4, 100)), - ColumnSpec.ck("ck2", ColumnSpec.asciiType), - ColumnSpec.ck("ck3", ColumnSpec.int64Type)), - idx++); - - for (boolean b1 : new boolean[]{ true, false }) - for (boolean b2 : new boolean[]{ true, false }) - for (boolean b3 : new boolean[]{ true, false }) + for (boolean b1 : new boolean[]{ false, true }) + for (boolean b2 : new boolean[]{ false, true }) + for (boolean b3 : new boolean[]{ false, true }) { - staticsTortureTest(Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType(4, 100), b1), - ColumnSpec.ck("ck2", ColumnSpec.asciiType, b2), - ColumnSpec.ck("ck3", ColumnSpec.int64Type, b3)), + staticsTortureTest(Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, Generators.ascii(4, 1000), b1), + ColumnSpec.ck("ck2", ColumnSpec.asciiType, Generators.ascii(4, 1000), b2), + ColumnSpec.ck("ck3", ColumnSpec.int64Type, Generators.int64(), b3)), idx++); } } public void staticsTortureTest(List> cks, int idx) { - SchemaSpec schema = new SchemaSpec(KEYSPACE, "tbl" + idx, - Arrays.asList(ColumnSpec.ck("pk1", ColumnSpec.int64Type), - ColumnSpec.ck("pk2", ColumnSpec.asciiType(4, 100)), - ColumnSpec.ck("pk3", ColumnSpec.int64Type)), + SchemaSpec schema = new SchemaSpec(idx, + 10_000, + KEYSPACE, + "tbl" + idx, + Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.int64Type, Generators.int64()), + ColumnSpec.pk("pk2", ColumnSpec.asciiType, Generators.ascii(4, 1000)), + ColumnSpec.pk("pk3", ColumnSpec.int64Type, Generators.int64())), cks, - Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType(40, 100)), + Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType, Generators.ascii(4, 1000)), ColumnSpec.regularColumn("v2", ColumnSpec.int64Type), ColumnSpec.regularColumn("v3", ColumnSpec.int64Type)), - List.of(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType(40, 100)), - ColumnSpec.staticColumn("s2", ColumnSpec.int64Type), - ColumnSpec.staticColumn("s3", ColumnSpec.asciiType(40, 100)) - )); + Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType, Generators.ascii(4, 1000)), + ColumnSpec.staticColumn("s2", ColumnSpec.int64Type), + ColumnSpec.staticColumn("s3", ColumnSpec.asciiType, Generators.ascii(4, 1000)))); - sut.schemaChange(schema.compile().cql()); - SchemaSpec debugSchema = schema.cloneWithName(schema.keyspace, schema.table + "_debug"); - sut.schemaChange(schema.cloneWithName(schema.keyspace, schema.table + "_debug").compile().cql()); - sut.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai' " + + + cluster.schemaChange(schema.compile()); + cluster.get(1).nodetool("disableautocompaction"); + cluster.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai' " + "WITH OPTIONS = {'case_sensitive': 'false', 'normalize': 'true', 'ascii': 'true'};", schema.table, schema.regularColumns.get(0).name, schema.keyspace, schema.table, schema.regularColumns.get(0).name)); - sut.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", + cluster.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", schema.table, schema.regularColumns.get(1).name, schema.keyspace, schema.table, schema.regularColumns.get(1).name)); - sut.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", + cluster.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", schema.table, schema.regularColumns.get(2).name, schema.keyspace, schema.table, schema.regularColumns.get(2).name)); - sut.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", + cluster.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", schema.table, schema.staticColumns.get(0).name, schema.keyspace, schema.table, schema.staticColumns.get(0).name)); - sut.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", + cluster.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", schema.table, schema.staticColumns.get(1).name, schema.keyspace, schema.table, schema.staticColumns.get(1).name)); - sut.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", + cluster.schemaChange(String.format("CREATE INDEX %s_%s_sai_idx ON %s.%s (%s) USING 'sai';", schema.table, schema.staticColumns.get(2).name, schema.keyspace, schema.table, schema.staticColumns.get(2).name)); - DataTracker tracker = new DefaultDataTracker(); - TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(cluster.size()); - ReplayingHistoryBuilder history = new ReplayingHistoryBuilder(SEED + idx, - MAX_PARTITION_SIZE, - 100, - tracker, - new DoubleWritingSut(sut, - new QueryModifyingSut(sut, - schema.keyspace + "." + schema.table, - debugSchema.keyspace + "." + debugSchema.table)), - schema, - rf, - SystemUnderTest.ConsistencyLevel.QUORUM); + Generator regularColumnBitSet = Generators.bitSet(schema.regularColumns.size()); + Generator staticColumnBitSet = Generators.bitSet(schema.staticColumns.size()); EntropySource rng = new JdkRandomEntropySource(1l); - long[] values = new long[UNIQUE_CELL_VALUES]; - for (int i = 0; i < values.length; i++) - values[i] = rng.next(); + + ReplayingHistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, hb -> { + return InJvmDTestVisitExecutor.builder().pageSizeSelector(i -> rng.nextInt(1, 10)).build(schema, hb, cluster); + }); + for (int i = 0; i < NUM_PARTITIONS; i++) { - history.visitPartition(i) - .insert(1, - new long[]{ rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)] + history.insert(i, rng.nextInt(5), + new int[]{ rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES) }, - new long[]{ rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)] - }); - history.visitPartition(i) - .insert(5, - new long[]{ rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)] + new int[]{ rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES) + }); + history.insert(i,rng.nextInt(5), + new int[]{ rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES) }, - new long[]{ rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)], - rng.nextBoolean() ? DataGenerators.UNSET_DESCR : values[rng.nextInt(values.length)] - }); + new int[]{ rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES), + rng.nextBoolean() ? MagicConstants.UNSET_IDX : rng.nextInt(UNIQUE_CELL_VALUES) + }); if (rng.nextFloat() > 0.9f) { - history.visitPartition(i) - .deleteRowRange(rng.nextInt(5), rng.nextInt(5), rng.nextBoolean(), rng.nextBoolean()); + history.deleteRowRange(i, rng.nextInt(5), rng.nextInt(5), + rng.nextInt(1, schema.clusteringKeys.size()), + rng.nextBoolean(), rng.nextBoolean()); } if (rng.nextFloat() > 0.9f) { - history.visitPartition(i) - .deleteColumns(); + history.deleteColumns(i, rng.nextInt(5), regularColumnBitSet.generate(rng), staticColumnBitSet.generate(rng)); } if (i % 50 == 0) cluster.get(1).nodetool("flush", schema.keyspace, schema.table); + + if (i % 100 == 0) + cluster.get(1).nodetool("compact", schema.keyspace, schema.table); } - Model model = new AgainstSutChecker(tracker, history.clock(), sut, schema, schema.cloneWithName(schema.keyspace, debugSchema.table)) { - @Override - protected List executeOnDebugSchema(Query query) - { - CompiledStatement s2 = query.toSelectStatement(doubleWriteTable.allColumnsSet, true) - .withSchema(schema.keyspace, schema.table, doubleWriteTable.keyspace, doubleWriteTable.table) - .withFiltering(); - return SelectHelper.execute(sut, clock, s2, schema); - } - }; - + Generator ckIdxGen = Generators.int32(0, MAX_PARTITION_SIZE); for (int pdx = 0; pdx < NUM_PARTITIONS; pdx++) { - long pd = history.pdSelector().pdAtPosition(pdx); - history.pdSelector().pdAtPosition(1); - for (int i1 = 0; i1 < values.length; i1++) - for (int i2 = 0; i2 < values.length; i2++) - for (int i3 = 0; i3 < values.length; i3++) - { - long[] descriptors = new long[]{ values[i1], values[i2], values[i3], - values[i1], values[i2], values[i3] }; - List relations = new ArrayList<>(); - Stream.concat(schema.regularColumns.stream(), - schema.staticColumns.stream()) - .forEach(new Consumer<>() - { - int counter = 0; - - @Override - public void accept(ColumnSpec column) - { - if (rng.nextBoolean()) - return; - - if (column.type.toString().equals(ColumnSpec.int64Type.toString())) - { - if (rng.nextBoolean()) - { - relations.add(Relation.relation(Relation.RelationKind.EQ, - column, - descriptors[counter])); - } - else - { - Relation.relation(rng.nextBoolean() ? Relation.RelationKind.LT : Relation.RelationKind.GT, - column, - descriptors[counter]); - } - } - else - { - Relation.relation(Relation.RelationKind.EQ, - column, - descriptors[counter]); - } - - counter++; - } - }); - - // Without partition key - model.validate(new FilteringQuery(-1, false, relations, schema) - { - @Override - public CompiledStatement toSelectStatement() - { - return SelectHelper.select(schemaSpec, null, schemaSpec.allColumnsSet, relations, reverse, true); - } - }); - model.validate(new FilteringQuery(pd, false, relations, schema)); - } + for (int i = 0; i < 10; i++) + { + List ckRelations = generateClusteringRelations(rng, schema.clusteringKeys.size(), ckIdxGen); + List regularRelations = generateValueRelations(rng, schema.regularColumns.size(), + column -> Math.min(schema.valueGenerators.regularPopulation(column), MAX_PARTITION_SIZE)); + List staticRelations = generateValueRelations(rng, schema.staticColumns.size(), + column -> Math.min(schema.valueGenerators.staticPopulation(column), MAX_PARTITION_SIZE)); + history.select(pdx, + ckRelations.toArray(new IdxRelation[0]), + regularRelations.toArray(new IdxRelation[0]), + staticRelations.toArray(new IdxRelation[0])); + } } } } \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java index 11fce664a1..6f83ae0013 100644 --- a/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java @@ -18,12 +18,18 @@ package org.apache.cassandra.fuzz.topology; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; - import javax.annotation.Nullable; +import com.google.common.base.Throwables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import accord.utils.Gen; import accord.utils.Property; import accord.utils.Property.Command; @@ -32,16 +38,30 @@ import accord.utils.Property.SimpleCommand; import accord.utils.RandomSource; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.IInstanceConfig; -import org.apache.cassandra.harry.HarryHelper; +import org.apache.cassandra.exceptions.RequestTimeoutException; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; 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.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.utils.AssertionUtils; +import org.assertj.core.api.Condition; import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; public class HarryTopologyMixupTest extends TopologyMixupTestBase { + protected static final Condition TIMEOUT_CHECKER = AssertionUtils.isInstanceof(RequestTimeoutException.class); + private static final Logger logger = LoggerFactory.getLogger(HarryTopologyMixupTest.class); + + public HarryTopologyMixupTest() + { + } + @Override protected Gen> stateGen() { @@ -61,49 +81,73 @@ public class HarryTopologyMixupTest extends TopologyMixupTestBase 0) { - // do one last read just to make sure we validate the data... - var harry = state.schemaSpec.harry; - harry.validateAll(harry.quiescentLocalChecker()); + for (Integer pkIdx : state.schema.pkGen.generated()) + state.schema.harry.selectPartition(pkIdx); } } - private static Spec createSchemaSpec(RandomSource rs, Cluster cluster) + private static BiFunction createSchemaSpec() { - ReplayingHistoryBuilder harry = HarryHelper.dataGen(rs.nextLong(), - new InJvmSut(cluster), - new TokenPlacementModel.SimpleReplicationFactor(3), - SystemUnderTest.ConsistencyLevel.ALL); - cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", HarryHelper.KEYSPACE)); - var schema = harry.schema(); - cluster.schemaChange(schema.compile().cql()); - waitForCMSToQuiesce(cluster, cluster.get(1)); - return new Spec(harry); + return (rs, cluster) -> { + EntropySource rng = new JdkRandomEntropySource(rs.nextLong()); + Generator schemaGen = SchemaGenerators.schemaSpecGen("harry", "table", 1000);; + SchemaSpec schema = schemaGen.generate(rng); + + HistoryBuilder harry = new ReplayingHistoryBuilder(schema.valueGenerators, + hb -> { + InJvmDTestVisitExecutor.Builder builder = InJvmDTestVisitExecutor.builder(); + return builder.nodeSelector(new InJvmDTestVisitExecutor.NodeSelector() + { + private final AtomicLong cnt = new AtomicLong(); + + @Override + public int select(long lts) + { + for (int i = 0; i < 42; i++) + { + int selected = (int) (cnt.getAndIncrement() % cluster.size() + 1); + if (!cluster.get(selected).isShutdown()) + return selected; + } + throw new IllegalStateException("Unable to find an alive instance"); + } + }) + .retryPolicy(t -> { + t = Throwables.getRootCause(t); + if (!TIMEOUT_CHECKER.matches(t)) + return false; + return false; + }) + .build(schema, hb, cluster); + }); + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", schema.keyspace)); + cluster.schemaChange(schema.compile()); + waitForCMSToQuiesce(cluster, cluster.get(1)); + return new Spec(harry, schema); + }; } - private static BiFunction, Command, Void, ?>> cqlOperations(Spec spec) + private static class HarryCommand extends SimpleCommand> { - class HarryCommand extends SimpleCommand> + HarryCommand(Function, String> name, Consumer> fn) { - HarryCommand(Function, String> name, Consumer> fn) - { - super(name, fn); - } - - @Override - public PreCheckResult checkPreconditions(State state) - { - int clusterSize = state.topologyHistory.up().length; - return clusterSize >= 3 ? PreCheckResult.Ok : PreCheckResult.Ignore; - } + super(name, fn); } + + @Override + public PreCheckResult checkPreconditions(State state) + { + int clusterSize = state.topologyHistory.up().length; + return clusterSize >= 3 ? PreCheckResult.Ok : PreCheckResult.Ignore; + } + } + + private static CommandGen cqlOperations(Spec spec) + { Command, Void, ?> insert = new HarryCommand(state -> "Harry Insert" + state.commandNamePostfix(), state -> { spec.harry.insert(); ((HarryState) state).numInserts++; }); - Command, Void, ?> validateAll = new HarryCommand(state -> "Harry Validate All" + state.commandNamePostfix(), state -> { - spec.harry.validateAll(spec.harry.quiescentLocalChecker()); - ((HarryState) state).numInserts = 0; - }); return (rs, state) -> { HarryState harryState = (HarryState) state; TopologyHistory history = state.topologyHistory; @@ -111,43 +155,68 @@ public class HarryTopologyMixupTest extends TopologyMixupTestBase 0 && rs.decide(0.2))) // 20% of the time do reads - return validateAll; + return validateAll(state); return insert; }; } - public static class Spec implements TopologyMixupTestBase.SchemaSpec + private static Command, Void, ?> validateAll(State state) { - private final ReplayingHistoryBuilder harry; + Spec spec = state.schema; + List, Void, ?>> reads = new ArrayList<>(); - public Spec(ReplayingHistoryBuilder harry) + for (Integer pkIdx : spec.pkGen.generated()) + { + long pd = spec.harry.valueGenerators().pkGen().descriptorAt(pkIdx); + reads.add(new HarryCommand(s -> String.format("Harry Validate pd=%d%s", pd, state.commandNamePostfix()), s -> spec.harry.selectPartition(pkIdx))); + } + reads.add(new HarryCommand(s -> "Reset Harry Write State" + state.commandNamePostfix(), s -> ((HarryState) s).numInserts = 0)); + return Property.multistep(reads); + } + + public static class Spec implements Schema + { + private final Generators.TrackingGenerator pkGen; + private final HistoryBuilder harry; + private final SchemaSpec schema; + + public Spec(HistoryBuilder harry, SchemaSpec schema) { this.harry = harry; + this.schema = schema; + this.pkGen = Generators.tracking(Generators.int32(0, schema.valueGenerators.pkPopulation())); } @Override - public String name() + public String table() { - return harry.schema().table; + return schema.table; } @Override - public String keyspaceName() + public String keyspace() { - return HarryHelper.KEYSPACE; + return schema.keyspace; + } + + @Override + public String createSchema() + { + return schema.compile(); } } - public static class HarryState extends State + public class HarryState extends State { private long generation; private int numInserts = 0; + public HarryState(RandomSource rs) { - super(rs, HarryTopologyMixupTest::createSchemaSpec, HarryTopologyMixupTest::cqlOperations); + super(rs, createSchemaSpec(), HarryTopologyMixupTest::cqlOperations); } @Override @@ -156,4 +225,4 @@ public class HarryTopologyMixupTest extends TopologyMixupTestBase * {@code for id in $(seq 0 15); do sudo ifconfig lo0 alias "127.0.0.$id"; done;} */ -public abstract class TopologyMixupTestBase extends TestBaseImpl +public abstract class TopologyMixupTestBase extends TestBaseImpl { private static final Logger logger = LoggerFactory.getLogger(TopologyMixupTestBase.class); @@ -98,9 +117,9 @@ public abstract class TopologyMixupTestBase> REMOVE_TYPE_DISTRIBUTION = Gens.enums().allMixedDistribution(RemoveType.class); private static final Gen> CONF_GEN = new ConfigGenBuilder() - // jvm-dtest hard codes this partitioner in its APIs, so overriding will break the test - .withPartitionerGen(null) - .build(); + // jvm-dtest hard codes this partitioner in its APIs, so overriding will break the test + .withPartitionerGen(null) + .build(); // common commands private Command, Void, ?> repairCommand(int toCoordinate) { - return new SimpleCommand<>(state -> "nodetool repair " + state.schemaSpec.keyspaceName() + ' ' + state.schemaSpec.name() + " from node" + toCoordinate + state.commandNamePostfix(), - state -> state.cluster.get(toCoordinate).nodetoolResult("repair", state.schemaSpec.keyspaceName(), state.schemaSpec.name()).asserts().success()); + return new SimpleCommand<>(state -> "nodetool repair " + state.schema.keyspace() + ' ' + state.schema.table() + " from node" + toCoordinate + state.commandNamePostfix(), + state -> state.cluster.get(toCoordinate).nodetoolResult("repair", state.schema.keyspace(), state.schema.table(), "--force").asserts().success()); + } + + private static Command, Void, ?> repairCommand(int toCoordinate, String ks, String... tables) { + return new SimpleCommand<>(state -> "nodetool repair " + ks + (tables.length == 0 ? "" : " " + Arrays.asList(tables)) + " from node" + toCoordinate + state.commandNamePostfix(), + state -> { + if (tables.length == 0) { + state.cluster.get(toCoordinate).nodetoolResult("repair", ks, "--force").asserts().success(); + return; + } + List args = new ArrayList<>(3 + tables.length); + args.add("repair"); + args.add(ks); + args.addAll(Arrays.asList(tables)); + args.add("--force"); + state.cluster.get(toCoordinate).nodetoolResult(args.toArray(String[]::new)).asserts().success(); + }); } private Command, Void, ?> waitForCMSToQuiesce() { - return new SimpleCommand<>(state -> "Waiting for CMS to Quiesce" + state.commandNamePostfix(), - state -> ClusterUtils.waitForCMSToQuiesce(state.cluster, state.cmsGroup)); + return new Property.StateOnlyCommand<>() + { + private Epoch maxEpoch = null; + @Override + public String detailed(State state) + { + if (maxEpoch == null) + maxEpoch = ClusterUtils.maxEpoch(state.cluster, state.topologyHistory.up()); + return "Waiting for CMS to Quiesce on epoch " + maxEpoch.getEpoch() + state.commandNamePostfix(); + } + + @Override + public void applyUnit(State state) + { + Invariants.nonNull(maxEpoch, "detailed was not called before calling apply"); + ClusterUtils.waitForCMSToQuiesce(state.cluster, maxEpoch, true); + } + }; } - private Command, Void, ?> stopInstance(int toRemove) + private Command, Void, ?> waitForGossipToSettle() { - return new SimpleCommand<>(state -> "Stop Node" + toRemove + " for Assassinate" + state.commandNamePostfix(), - state -> { - IInvokableInstance inst = state.cluster.get(toRemove); - TopologyHistory.Node node = state.topologyHistory.node(toRemove); - ClusterUtils.stopUnchecked(inst); - node.down(); - }); + return new SimpleCommand<>(state -> "Waiting for Ring to Settle" + state.commandNamePostfix(), + state -> { + int[] up = state.topologyHistory.up(); + for (int node : up) + { + IInvokableInstance instance = state.cluster.get(node); + ClusterUtils.awaitRingJoin(state.cluster, up, instance); + } + }); + } + + private Command, Void, ?> waitAllNodesInPeers() + { + return new SimpleCommand<>(state -> "Waiting for all alive nodes to be in peers" + state.commandNamePostfix(), + state -> { + int[] up = state.topologyHistory.up(); + for (int node : up) + { + IInvokableInstance instance = state.cluster.get(node); + ClusterUtils.awaitInPeers(state.cluster, up, instance); + } + }); + } + + private Command, Void, ?> stopInstance(RandomSource rs, State state) + { + int toStop = rs.pickInt(state.upAndSafe()); + return stopInstance(toStop, "Normal Stop"); + } + + private Command, Void, ?> startInstance(RandomSource rs, State state) + { + int toStop = rs.pickInt(state.topologyHistory.down()); + return startInstance(toStop); + } + + private Command, Void, ?> startInstance(int toStart) + { + return new SimpleCommand<>(state -> "Start Node" + toStart + state.commandNamePostfix(), + state -> { + IInvokableInstance inst = state.cluster.get(toStart); + TopologyHistory.Node node = state.topologyHistory.node(toStart); + inst.startup(); + node.up(); + }); + } + + private Command, Void, ?> stopInstance(int toRemove, String why) + { + return new SimpleCommand<>(state -> "Stop Node" + toRemove + " for " + why + state.commandNamePostfix(), + state -> { + IInvokableInstance inst = state.cluster.get(toRemove); + TopologyHistory.Node node = state.topologyHistory.node(toRemove); + ClusterUtils.stopUnchecked(inst); + node.down(); + }); } private Command, Void, ?> addNode() { return new SimpleCommand<>(state -> "Add Node" + (state.topologyHistory.uniqueInstances + 1) + state.commandNamePostfix(), - state -> { - TopologyHistory.Node n = state.topologyHistory.addNode(); - IInvokableInstance newInstance = ClusterUtils.addInstance(state.cluster, n.dc, n.rack, c -> c.set("auto_bootstrap", true)); - newInstance.startup(state.cluster); - n.up(); - }); + state -> { + TopologyHistory.Node n = state.topologyHistory.addNode(); + IInvokableInstance newInstance = ClusterUtils.addInstance(state.cluster, n.dc, n.rack, c -> c.set("auto_bootstrap", true)); + newInstance.startup(state.cluster); + ClusterUtils.assertModeJoined(newInstance); + n.up(); + }); } private Command, Void, ?> removeNodeDecommission(RandomSource rs, State state) { - int toRemove = rs.pickInt(state.topologyHistory.up()); + int toRemove = rs.pickInt(state.upAndSafe()); return new SimpleCommand<>("nodetool decommission node" + toRemove + state.commandNamePostfix(), s2 -> { IInvokableInstance inst = s2.cluster.get(toRemove); TopologyHistory.Node node = s2.topologyHistory.node(toRemove); @@ -169,7 +270,7 @@ public abstract class TopologyMixupTestBase, Void, ?> removeNode(RandomSource rs, State state) { int[] up = state.topologyHistory.up(); - int toRemove = rs.pickInt(up); + int toRemove = rs.pickInt(state.upAndSafe()); int toCoordinate; { int picked; @@ -180,28 +281,21 @@ public abstract class TopologyMixupTestBase("nodetool removenode node" + toRemove + " from node" + toCoordinate + state.commandNamePostfix(), s2 -> { - TopologyHistory.Node node = s2.topologyHistory.node(toRemove); - node.status = TopologyHistory.Node.Status.BeingRemoved; - IInvokableInstance coordinator = s2.cluster.get(toCoordinate); - coordinator.nodetoolResult("removenode", Integer.toString(toRemove), "--force").asserts().success(); - node.removed(); - s2.currentEpoch.set(HackSerialization.tcmEpoch(coordinator)); - }), - repairCommand(toCoordinate)); + return multistep(stopInstance(toRemove, "nodetool removenode"), + new SimpleCommand<>("nodetool removenode node" + toRemove + " from node" + toCoordinate + state.commandNamePostfix(), s2 -> { + TopologyHistory.Node node = s2.topologyHistory.node(toRemove); + node.status = TopologyHistory.Node.Status.BeingRemoved; + IInvokableInstance coordinator = s2.cluster.get(toCoordinate); + coordinator.nodetoolResult("removenode", Integer.toString(toRemove), "--force").asserts().success(); + node.removed(); + s2.currentEpoch.set(HackSerialization.tcmEpoch(coordinator)); + }), + repairCommand(toCoordinate)); } private Command, Void, ?> removeNodeAssassinate(RandomSource rs, State state) { - //TODO (correctness): assassinate CMS member isn't allowed - IntHashSet up = asSet(state.topologyHistory.up()); - IntHashSet cmsGroup = asSet(state.cmsGroup); - Sets.SetView upAndNotInCMS = Sets.difference(up, cmsGroup); - if (upAndNotInCMS.isEmpty()) throw new AssertionError("Every node is a CMS member"); - List allowed = new ArrayList<>(upAndNotInCMS); - allowed.sort(Comparator.naturalOrder()); - int toRemove = rs.pick(allowed); + int toRemove = rs.pickInt(state.upAndSafe()); int toCoordinate; { int[] upInt = state.topologyHistory.up(); @@ -213,17 +307,17 @@ public abstract class TopologyMixupTestBase("nodetool assassinate node" + toRemove + " from node" + toCoordinate + state.commandNamePostfix(), s2 -> { - TopologyHistory.Node node = s2.topologyHistory.node(toRemove); - node.status = TopologyHistory.Node.Status.BeingAssassinated; - IInvokableInstance coordinator = s2.cluster.get(toCoordinate); - InetSocketAddress address = s2.cluster.get(toRemove).config().broadcastAddress(); - coordinator.nodetoolResult("assassinate", address.getAddress().getHostAddress() + ":" + address.getPort()).asserts().success(); - node.removed(); - s2.currentEpoch.set(HackSerialization.tcmEpoch(coordinator)); - }), - repairCommand(toCoordinate) + return multistep(stopInstance(toRemove, "nodetool assassinate"), + new SimpleCommand<>("nodetool assassinate node" + toRemove + " from node" + toCoordinate + state.commandNamePostfix(), s2 -> { + TopologyHistory.Node node = s2.topologyHistory.node(toRemove); + node.status = TopologyHistory.Node.Status.BeingAssassinated; + IInvokableInstance coordinator = s2.cluster.get(toCoordinate); + InetSocketAddress address = s2.cluster.get(toRemove).config().broadcastAddress(); + coordinator.nodetoolResult("assassinate", address.getAddress().getHostAddress() + ":" + address.getPort()).asserts().success(); + node.removed(); + s2.currentEpoch.set(HackSerialization.tcmEpoch(coordinator)); + }), + repairCommand(toCoordinate) ); } @@ -245,26 +339,24 @@ public abstract class TopologyMixupTestBase, Void, ?> hostReplace(RandomSource rs, State state) { - int nodeToReplace = rs.pickInt(state.topologyHistory.up()); + int nodeToReplace = rs.pickInt(state.upAndSafe()); IInvokableInstance toReplace = state.cluster.get(nodeToReplace); TopologyHistory.Node adding = state.topologyHistory.replace(nodeToReplace); TopologyHistory.Node removing = state.topologyHistory.nodes.get(nodeToReplace); - return multistep(new SimpleCommand<>("Stop Node" + nodeToReplace + " for HostReplace; Node" + adding.id + state.commandNamePostfix(), s2 -> { - ClusterUtils.stopUnchecked(toReplace); - removing.down(); - }), - new SimpleCommand<>("Host Replace Node" + nodeToReplace + "; Node" + adding.id + state.commandNamePostfix(), s2 -> { - logger.info("node{} starting host replacement; epoch={}", adding.id, HackSerialization.tcmEpochAndSync(s2.cluster.getFirstRunningInstance())); - removing.status = TopologyHistory.Node.Status.BeingReplaced; - IInvokableInstance inst = ClusterUtils.replaceHostAndStart(s2.cluster, toReplace); - s2.topologyHistory.replaced(removing, adding); - long epoch = HackSerialization.tcmEpoch(inst); - s2.currentEpoch.set(epoch); - logger.info("{} completed host replacement in epoch={}", inst, epoch); - }), - //TODO (remove after rebase to trunk): https://issues.apache.org/jira/browse/CASSANDRA-19705 After the rebase to trunk this is not needed. The issue is that the CMS placement removes the node, it does not promote another node, this cases rf=3 to become rf=2 - new SimpleCommand<>("CMS reconfigure on Node" + adding.id + state.commandNamePostfix(), s2 -> s2.cluster.get(adding.id).nodetoolResult("cms", "reconfigure", Integer.toString(TARGET_RF)).asserts().success()) + return multistep(stopInstance(nodeToReplace, "HostReplace; Node" + adding.id), + new SimpleCommand<>("Host Replace Node" + nodeToReplace + "; Node" + adding.id + state.commandNamePostfix(), s2 -> { + logger.info("node{} starting host replacement; epoch={}", adding.id, HackSerialization.tcmEpochAndSync(s2.cluster.getFirstRunningInstance())); + removing.status = TopologyHistory.Node.Status.BeingReplaced; + IInvokableInstance inst = ClusterUtils.replaceHostAndStart(s2.cluster, toReplace); + ClusterUtils.assertModeJoined(inst); + s2.topologyHistory.replaced(removing, adding); + long epoch = HackSerialization.tcmEpoch(inst); + s2.currentEpoch.set(epoch); + logger.info("{} completed host replacement in epoch={}", inst, epoch); + }), + //TODO (remove after rebase to trunk): https://issues.apache.org/jira/browse/CASSANDRA-19705 After the rebase to trunk this is not needed. The issue is that the CMS placement removes the node, it does not promote another node, this cases rf=3 to become rf=2 + new SimpleCommand<>("CMS reconfigure on Node" + adding.id + state.commandNamePostfix(), s2 -> s2.cluster.get(adding.id).nodetoolResult("cms", "reconfigure", Integer.toString(TARGET_RF)).asserts().success()) ); } @@ -283,24 +375,30 @@ public abstract class TopologyMixupTestBase state.preActions.forEach(Runnable::run)) - .add(2, (rs, state) -> { - EnumSet possibleTopologyChanges = possibleTopologyChanges(state); - if (possibleTopologyChanges.isEmpty()) return ignoreCommand(); - return topologyCommand(state, possibleTopologyChanges).next(rs); - }) - .add(1, (rs, state) -> repairCommand(rs.pickInt(state.topologyHistory.up()))) - .add(7, (rs, state) -> state.statementGen.apply(rs, state)) - .destroyState((state, cause) -> { - try (state) - { - TopologyMixupTestBase.this.destroyState(state, cause); - } - }) - .build()); + .preCommands(state -> state.preActions.forEach(Runnable::run)) + .add(2, (rs, state) -> { + EnumSet possibleTopologyChanges = possibleTopologyChanges(state); + if (possibleTopologyChanges.isEmpty()) return ignoreCommand(); + return topologyCommand(state, possibleTopologyChanges).next(rs); + }) + .add(1, (rs, state) -> repairCommand(rs.pickInt(state.topologyHistory.up()))) + .add(7, (rs, state) -> state.statementGen.apply(rs, state)) + .destroyState((state, cause) -> { + try (state) + { + TopologyMixupTestBase.this.destroyState(state, cause); + } + }) + .commandsTransformer((state, gen) -> { + for (BiFunction, Gen, Void, ?>>, Gen, Void, ?>>> fn : state.commandsTransformers) + gen = fn.apply(state, gen); + return gen; + }) + .onSuccess((state, sut, history) -> logger.info("Successful for the following:\nState {}\nHistory:\n{}", state, Property.formatList("\t\t", history))) + .build()); } private EnumSet possibleTopologyChanges(State state) @@ -308,18 +406,32 @@ public abstract class TopologyMixupTestBase possibleTopologyChanges = EnumSet.noneOf(TopologyChange.class); // up or down is logically more correct, but since this runs sequentially and after the topology changes are complete, we don't have downed nodes at this point // so up is enough to know the topology size - int size = state.topologyHistory.up().length; - if (size < state.topologyHistory.maxNodes) + int up = state.topologyHistory.up().length; + int down = state.topologyHistory.down().length; + int[] upAndSafe = state.upAndSafe(); + int total = up + down; + if (total < state.topologyHistory.maxNodes) possibleTopologyChanges.add(TopologyChange.AddNode); - if (size > state.topologyHistory.quorum()) + if (upAndSafe.length > 0) { - if (size > TARGET_RF) + // can't remove the node if all nodes are CMS nodes + if (!Sets.difference(asSet(upAndSafe), asSet(state.cmsGroup)).isEmpty()) possibleTopologyChanges.add(TopologyChange.RemoveNode); possibleTopologyChanges.add(TopologyChange.HostReplace); + possibleTopologyChanges.add(TopologyChange.StopNode); } + if (down > 0) + possibleTopologyChanges.add(TopologyChange.StartNode); return possibleTopologyChanges; } + private Command, Void, ?> awaitClusterStable() + { + return multistep(waitForCMSToQuiesce(), + waitForGossipToSettle(), + waitAllNodesInPeers()); + } + private Gen, Void, ?>> topologyCommand(State state, EnumSet possibleTopologyChanges) { Map, Void, ?>>, Integer> possible = new LinkedHashMap<>(); @@ -328,13 +440,19 @@ public abstract class TopologyMixupTestBase multistep(addNode(), waitForCMSToQuiesce()), 1); + possible.put(ignore -> multistep(addNode(), awaitClusterStable()), 1); break; case RemoveNode: - possible.put(rs -> multistep(removeNodeRandomizedDispatch(rs, state), waitForCMSToQuiesce()), 1); + possible.put(rs -> multistep(removeNodeRandomizedDispatch(rs, state), awaitClusterStable()), 1); break; case HostReplace: - possible.put(rs -> multistep(hostReplace(rs, state), waitForCMSToQuiesce()), 1); + possible.put(rs -> multistep(hostReplace(rs, state), awaitClusterStable()), 1); + break; + case StartNode: + possible.put(rs -> startInstance(rs, state), 1); + break; + case StopNode: + possible.put(rs -> stopInstance(rs, state), 1); break; default: throw new UnsupportedOperationException(task.name()); @@ -351,74 +469,142 @@ public abstract class TopologyMixupTestBase implements AutoCloseable + protected interface CommandGen + { + Command, Void, ?> apply(RandomSource rs, State state); + } + + private static class LoggingCommand extends Property.ForwardingCommand + { + private static final Logger logger = LoggerFactory.getLogger(LoggingCommand.class); + + private LoggingCommand(Command delegate) + { + super(delegate); + } + + @Override + public Result apply(State s) throws Throwable + { + String name = detailed(s); + long startNanos = Clock.Global.nanoTime(); + try + { + logger.info("Starting command: {}", name); + Result o = super.apply(s); + logger.info("Command {} was success after {}", name, Duration.ofNanos(Clock.Global.nanoTime() - startNanos)); + return o; + } + catch (Throwable t) + { + logger.warn("Command {} failed after {}: {}", name, Duration.ofNanos(Clock.Global.nanoTime() - startNanos), t.toString()); // don't want stack trace, just type/msg + throw t; + } + } + } + + protected static class State implements AutoCloseable { final TopologyHistory topologyHistory; final Cluster cluster; - final S schemaSpec; + final S schema; + final List, Gen, Void, ?>>, Gen, Void, ?>>>> commandsTransformers = new ArrayList<>(); final List preActions = new CopyOnWriteArrayList<>(); final AtomicLong currentEpoch = new AtomicLong(); - final BiFunction, Command, Void, ?>> statementGen; + final CommandGen statementGen; final Gen removeTypeGen; private final Map yamlConfigOverrides; int[] cmsGroup = new int[0]; + private ReplicationFactor rf; + private final RingModel ring = new RingModel(); - public State(RandomSource rs, BiFunction schemaSpecGen, Function, Command, Void, ?>>> cqlOperationsGen) + public State(RandomSource rs, BiFunction schemaSpecGen, Function> cqlOperationsGen) { this.topologyHistory = new TopologyHistory(rs.fork(), 2, 4); + rf = new SimpleReplicationFactor(2); try { this.yamlConfigOverrides = CONF_GEN.next(rs); cluster = Cluster.build(topologyHistory.minNodes) - .withTokenSupplier(topologyHistory) - .withConfig(c -> { - c.with(Feature.values()) - .set("write_request_timeout", "10s"); - //TODO (maintenance): where to put this? Anything touching ConfigGenBuilder with jvm-dtest needs this... - ((InstanceConfig) c).remove("commitlog_sync_period_in_ms"); - for (Map.Entry e : yamlConfigOverrides.entrySet()) - c.set(e.getKey(), e.getValue()); - onConfigure(c); - }) - //TODO (maintenance): should TopologyHistory also be a INodeProvisionStrategy.Factory so address information is stored in the Node? - //TODO (maintenance): AbstractCluster's Map nodeIdTopology makes playing with dc/rack annoying, if this becomes an interface then TopologyHistory could own - .withNodeProvisionStrategy((subnet, portMap) -> new INodeProvisionStrategy.AbstractNodeProvisionStrategy(portMap) - { - { - Invariants.checkArgument(subnet == 0, "Unexpected subnet detected: %d", subnet); - } + .withTokenSupplier(topologyHistory) + .withConfig(c -> { + c.with(Feature.values()) + .set("write_request_timeout", "10s") + .set("read_request_timeout", "10s") + .set("range_request_timeout", "20s") + .set("request_timeout", "20s") + .set("native_transport_timeout", "30s") + // bound startup to some value larger than the task timeout, this is to allow the + // tests to stop blocking when a startup issue is detected. The main reason for + // this is that startup blocks forever, waiting for accord and streaming to + // complete... but if there are bugs at these layers then the startup will never + // exit, blocking the JVM from giving the needed information (logs/seed) to debug. + .set(Constants.KEY_DTEST_STARTUP_TIMEOUT, "4m") + .set(Constants.KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN, false); + //TODO (maintenance): where to put this? Anything touching ConfigGenBuilder with jvm-dtest needs this... + ((InstanceConfig) c).remove("commitlog_sync_period_in_ms"); + for (Map.Entry e : yamlConfigOverrides.entrySet()) + c.set(e.getKey(), e.getValue()); + onConfigure(c); + }) + //TODO (maintenance): should TopologyHistory also be a INodeProvisionStrategy.Factory so address information is stored in the Node? + //TODO (maintenance): AbstractCluster's Map nodeIdTopology makes playing with dc/rack annoying, if this becomes an interface then TopologyHistory could own + .withNodeProvisionStrategy((subnet, portMap) -> new INodeProvisionStrategy.AbstractNodeProvisionStrategy(portMap) + { + { + Invariants.checkArgument(subnet == 0, "Unexpected subnet detected: %d", subnet); + } - private final String ipPrefix = "127.0." + subnet + '.'; + private final String ipPrefix = "127.0." + subnet + '.'; - @Override - public int seedNodeNum() - { - int[] up = topologyHistory.up(); - if (Arrays.equals(up, new int[]{ 1, 2 })) - return 1; - return rs.pickInt(up); - } + @Override + public int seedNodeNum() + { + int[] up = topologyHistory.up(); + if (Arrays.equals(up, new int[]{ 1, 2 })) + return 1; + return rs.pickInt(up); + } - @Override - public String ipAddress(int nodeNum) - { - return ipPrefix + nodeNum; - } - }) - .start(); + @Override + public String ipAddress(int nodeNum) + { + return ipPrefix + nodeNum; + } + }) + .start(); } catch (IOException e) { throw new UncheckedIOException(e); } + cluster.setUncaughtExceptionsFilter((node, t) -> { + // api is "ignore" so false means include, + var rootCause = Throwables.getRootCause(t); + if (rootCause.getMessage() != null) + { + if (rootCause.getMessage().startsWith("Queried for epoch") && rootCause.getMessage().contains("but could not catch up. Current epoch:")) + return true; + if (rootCause.getMessage().startsWith("Operation timed out")) + { + // is this due to TCM fetching epochs? PaxosBackedProcessor.getLogState is costly and more likely to timeout... so ignore those + Optional match = Stream.of(rootCause.getStackTrace()) + .filter(s -> s.getClassName().equals("org.apache.cassandra.tcm.PaxosBackedProcessor") && s.getMethodName().equals("getLogState")) + .findFirst(); + if (match.isPresent()) + return true; + } + } + return false; + }); fixDistributedSchemas(cluster); init(cluster, TARGET_RF); // fix TCM @@ -427,31 +613,71 @@ public abstract class TopologyMixupTestBase, Gen, Void, ?>>, Gen, Void, ?>>>() { // in order to remove this action, an anonymous class is needed so "this" works, lambda "this" is the parent class @Override - public void run() - { - if (topologyHistory.up().length == TARGET_RF) - { + public Gen, Void, ?>> apply(State state, Gen, Void, ?>> commandGen) { + if (topologyHistory.up().length < TARGET_RF) + return commandGen; + SimpleCommand> reconfig = new SimpleCommand<>("nodetool cms reconfigure " + TARGET_RF, ignore -> { NodeToolResult result = cluster.get(1).nodetoolResult("cms", "reconfigure", Integer.toString(TARGET_RF)); result.asserts().success(); logger.info("CMS reconfigure: {}", result.getStdout()); - preActions.remove(this); - } + }); + SimpleCommand> fixDistributedSchemas = new SimpleCommand<>("Set system distributed keyspaces to RF=" + TARGET_RF, ignore -> + fixDistributedSchemas(cluster)); + SimpleCommand> fixTestKeyspace = new SimpleCommand<>("Set " + KEYSPACE + " keyspace to RF=" + TARGET_RF, s -> { + cluster.schemaChange("ALTER KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + TARGET_RF + "}"); + rf = new SimpleReplicationFactor(TARGET_RF); + }); + var self = this; + return rs -> { + Command, Void, ?> next = commandGen.next(rs); + if (next.checkPreconditions(state) == Property.PreCheckResult.Ignore) + return next; + commandsTransformers.remove(self); + int[] up = state.topologyHistory.up(); + List, Void, ?>> commands = new ArrayList<>(); + commands.add(fixDistributedSchemas); + for (String ks : Arrays.asList("system_auth", "system_traces")) + { + int coordinator = rs.pickInt(up); + commands.add(repairCommand(coordinator, ks)); + } + commands.add(fixTestKeyspace); + { + int coordinator = rs.pickInt(up); + commands.add(repairCommand(coordinator, KEYSPACE)); + } + commands.add(reconfig); + commands.add(next); + return multistep(commands); + }; } }); + commandsTransformers.add((state, commandGen) -> rs2 -> { + Command, Void, ?> c = commandGen.next(rs2); + if (!(c instanceof Property.MultistepCommand)) + return new LoggingCommand<>(c); + Property.MultistepCommand, Void> multistep = (Property.MultistepCommand, Void>) c; + List, Void, ?>> subcommands = new ArrayList<>(); + for (var sub : multistep) + subcommands.add(new LoggingCommand<>(sub)); + return multistep(subcommands); + }); preActions.add(() -> { int[] up = topologyHistory.up(); // use the most recent node just in case the cluster isn't in-sync IInvokableInstance node = cluster.get(up[up.length - 1]); cmsGroup = HackSerialization.cmsGroup(node); currentEpoch.set(HackSerialization.tcmEpoch(node)); + + ring.rebuild(cluster.coordinator(up[0]), rf, up); + // ring must know about the up nodes }); preActions.add(() -> cluster.checkAndResetUncaughtExceptions()); - this.schemaSpec = schemaSpecGen.apply(rs, cluster); - statementGen = cqlOperationsGen.apply(schemaSpec); + this.schema = schemaSpecGen.apply(rs, cluster); + statementGen = cqlOperationsGen.apply(schema); removeTypeGen = REMOVE_TYPE_DISTRIBUTION.next(rs); @@ -472,7 +698,38 @@ public abstract class TopologyMixupTestBase ranges = ring.ranges(id); + if (ranges.stream().allMatch(safeRanges::contains)) + safeNodes.add(id); + } + + int[] upAndSafe = safeNodes.toIntArray(); + Arrays.sort(upAndSafe); + return upAndSafe; } @Override @@ -480,14 +737,42 @@ public abstract class TopologyMixupTestBase cluster.get(cmsNode).executeInternalWithResult("SELECT epoch, kind, transformation FROM system_views.cluster_metadata_log")); + TableBuilder builder = new TableBuilder(" | "); + builder.add(qr.names()); + while (qr.hasNext()) + { + Row next = qr.next(); + builder.add(Stream.of(next.toObjectArray()) + .map(Objects::toString) + .map(s -> s.length() > 100 ? s.substring(0, 100) + "..." : s) + .collect(Collectors.toList())); + } + epochHistory = "Epochs:\n" + builder; + } + catch (Throwable t) + { + logger.warn("Unable to fetch epoch history on node{}", cmsNode, t); + } + logger.info("Shutting down clusters"); cluster.close(); } } @@ -550,11 +835,21 @@ public abstract class TopologyMixupTestBase n : nodes.entrySet()) { - if (n.getValue().status == Node.Status.Up) + if (n.getValue().status == target) up.add(n.getKey()); } int[] ints = up.toIntArray(); @@ -571,12 +866,12 @@ public abstract class TopologyMixupTestBase instTokens = Gens.lists(MURMUR_TOKEN_GEN - .filterAsInt(t -> !activeTokens.contains(Integer.toString(t)))) - .unique() - .ofSize(tokensPerNode) - .next(rs).stream() - .map(Object::toString) - .collect(Collectors.toList()); + .filterAsInt(t -> !activeTokens.contains(Integer.toString(t)))) + .unique() + .ofSize(tokensPerNode) + .next(rs).stream() + .map(Object::toString) + .collect(Collectors.toList()); activeTokens.addAll(instTokens); Node node = new Node(this, id, instTokens, "datacenter0", "rack0"); node.status = Node.Status.Down; @@ -652,10 +947,10 @@ public abstract class TopologyMixupTestBase ranges(int node) + { + Replica replica = idToReplica.get(node); + if (replica == null) + throw new AssertionError("Unknown node" + node); + List ranges = ring.ranges(replica); + if (ranges == null) + throw new AssertionError("node" + node + " some how does not have ranges..."); + return ranges; + } + + private void rangesToReplicas(BiConsumer fn) + { + for (Map.Entry> e : ring.asMap().entrySet()) + { + int[] replicas = e.getValue().stream().mapToInt(RingModel::toNodeId).toArray(); + Arrays.sort(replicas); + fn.accept(e.getKey(), replicas); + } + } + } } diff --git a/test/harry/main/README.md b/test/harry/main/README.md deleted file mode 100644 index a2f2cfacb9..0000000000 --- a/test/harry/main/README.md +++ /dev/null @@ -1,647 +0,0 @@ -# Harry, a fuzz testing tool for Apache Cassandra - -The project aims to generate _reproducible_ workloads that are as close to real-life as possible, while being able to -_efficiently_ verify the cluster state against the model without pausing the workload itself. - -## Getting Started in under 5 minutes - -Harry can operate as a straightforward read/write "correctness stress tool" that will check to ensure reads on a cluster -are consistent with what it knows it wrote to the cluster. You have a couple options for this. - -### Option 2: Running things manually lower in the stack: - -To start a workload that performs a concurrent read/write workload, 2 read and 2 write threads for 60 seconds -against a in-jvm cluster you can use the following code: - -``` -try (Cluster cluster = builder().withNodes(3) - .start()) -{ - SchemaSpec schema = new SchemaSpec("harry", "test_table", - asList(pk("pk1", asciiType), pk("pk1", int64Type)), - asList(ck("ck1", asciiType), ck("ck1", int64Type)), - asList(regularColumn("regular1", asciiType), regularColumn("regular1", int64Type)), - asList(staticColumn("static1", asciiType), staticColumn("static1", int64Type))); - - Configuration config = HarryHelper.defaultConfiguration() - .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3)) - .setSUT(() -> new InJvmSut(cluster)) - .build(); - - Run run = config.createRun(); - - concurrent(run, config, - asList(pool("Writer", 2, MutatingVisitor::new), - pool("Reader", 2, RandomPartitionValidator::new)), - 2, TimeUnit.MINUTES) - .run(); -} -``` - -# I've found a falsification. What now? - -There is no one-size-fits-all solution for debugging a falsification. We did try to create a shrinker, but unfortunately -without Simulator, shrinker only works for issues that are non-concurrent in nature, since there's no way to create a -stable repro otherwise. That said, there are several things that might get you started and inspire further ideas about -how to debug the issue. - -First of all, understand whether or not the issue is likely to be concurrent in nature. If you re-run your test with the -same seed, but see no falsification, and it fails only sporadically, and often on different logical timestamp, it is -likely that the issue is, in fact concurrent. Here, it is important to note that when you are running concurrent -read/write workload, you will get different interleaving of reads and writes every time you do this. - -If you can get a stable repro with a sequential runner, you're in luck. Now all you need to do is to add logs everywhere -and understand what might be causing it. But even if you do not have a stable repro, you are still likely to follow the -same steps: - -* Inspect the error itself. Do Cassandra-returned results make sense? Is anything out of order? Are there any duplicates - or gaps? -* Switch to logging mutating visitor and closely inspect its output. Closely inspect the output of the model. Do the - values make sense? -* Check the output of data tracker. Does the model or Cassandra have missing columns or rows? Do these outputs contain - latest logical timestamps for each of the operations from the log? How about in-flight operations? -* Filter out relevant operation log entries and inspect them closely. Given these operations, does the output of the - model, or output of the database make most sense? - -Next, you might want to try to narrow down the scope of the problem. Depending on what the falsification looks like, use -your Cassandra knowledge to see what might apply in your situation: - -* Try checking if changing schema to use different column types does anything. -* Try disabling range deletes, regular deletes, or column deletes. -* Try changing the size of partition and see if the issue still reproduces. -* Try disabling static columns. - -To avoid listing every feature in Harry, it suffices to say you should try to enable/disable features that make sense -in the given context, and try to find the combination that avoids the failure, or a minimal combination that still -reproduces the issue. Your first goal should be to find a _stable repro_, even if it involves modifying Cassandra or -Harry, or taking the operations, and composing the repro manually. Having a stable repro will make finding a cause much -simpler. Sometimes you will find the cause before you have a stable repro, in which case, you _still_ have to produce a -stable repro to make things simpler for the reviewer, and to include it into the test suite of your patch. - -Lastly, *be patient*. Debugging falsifications is often a multi-hour endeavour, and things do not always jump out at you, -so you might have to spend a significant amount of time tracking the problem down. Once you have found it, it is very -rewarding. - -## Further Reading -* [Harry: An open-source fuzz testing and verification tool for Apache Cassandra](https://cassandra.apache.org/_/blog/Harry-an-Open-Source-Fuzz-Testing-and-Verification-Tool-for-Apache-Cassandra.html) - ---- -# Technical and Implementation Details - -## System Under Test implementations - -* `in_jvm/InJvmSut` - simple in-JVM-dtest system under test. -* `println/PrintlnSut` - system under test that prints to sdtout instead of executing queries on the cluster; useful for - debugging. -* `mixed_in_jvm/MixedVersionInJvmSut` - in-JVM-dtest system under test that works with mixed version clusters. -* `external/ExternalClusterSut` - system under test that works with CCM, Docker, Kubernetes, or cluster you may. have - deployed elsewhere - -Both in-JVM SUTs have fault-injecting functionality available. - -## Visitors - -* `single/SingleValidator` - visitor that runs several different read queries against a single partition that is - associated with current logical timestamp, and validates their results using given model. -* `all_partitions/AllPartitionsValidator` - concurrently validates all partitions that were visited during this run. -* `repair_and_validate_local_states/RepairingLocalStateValidator` - similar to `AllPartitionsValidator`, but performs - repair before checking node states. -* `mutating/MutatingVisitor` - visitor that performs all sorts of mutations. -* `logging/LoggingVisitor` - similar to `MutatingVisitor`, but also logs all operations to a file; useful for debug - purposes. -* `corrupting/CorruptingVisitor` - visitor that will deliberately change data in the partition it visits. Useful for - negative tests (i.e. to ensure that your model actually detects data inconsistencies). - -And more. - -## Models - -* `querying_no_op/QueryingNoOpValidator` - a model that can be used to "simply" run random queries. -* `quiescent_checker/QuiescentChecker` - a model that can be used to verify results of any read that has no writes to - the same partition_ concurrent to it. Should be used in conjunction with locking data tracker. -* `quiescent_local_state_checker/QuiescentLocalStateChecker` - a model that can check local states of each replica that - has to own - -## Runners - -* `sequential/SequentialRunner` - runs all visitors sequentially, in the loop, for a specified amount of time; useful - for simple tests that do not have to exercise concurrent read/write path. -* `concurrent/ConcurrentRunner` - runs all visitors concurrently, each visitor in its own thread, looped, for a - specified amount of time; useful for things like concurrent read/write workloads. -* `chain/ChainRunner` - receives other runners as input, and runs them one after another once. Useful for both simple - and complex scenarios that involve both read/write workloads, validating all partitions, exercising other node-local - or cluster-wide operations. -* `staged/StagedRunner` - receives other runners (stages) as input, and runs them one after another in a loop; useful - for implementing complex scenarios, such as read/write workloads followed by some cluster changing operations. - -## Clock - -* `approximate_monotonic/ApproximateMonotonicClock` - a timestamp supplier implementation that tries to keep as close to - real time as possible, while preserving mapping from real-time to logical timestamps. -* `offset/OffsetClock` - a (monotonic) clock that supplies timestamps that do not have any relation to real time. - -# Introduction - -Harry has two primary modes of functionality: - -* Unit test mode: in which you define specific sequences of - operations and let Harry test these operations using different - schemas and conditions. -* Exploratory/fuzz mode: in which you define distributions of events - rather rather than sequences themselves, and let Harry try out - different things. - -Usually, in unit-test mode, we’re applying several write operations to the cluster state and then run different read -queries and validate their results. To learn more about writing unit tests, refer to the "Writing Unit Tests" section. - -In exploratory mode, we continuously apply write operations to the cluster and validate their state, allowing data size -to grow and simulating real-life behaviour. To learn more about implementing test cases using fuzz mode, refer to the " -Implementing Tests" section of this guide, but it's likely you'll have to read the rest of this document to implement -more complex scenarios. - -# Writing Unit Tests - -To write unit tests with Harry, there's no special knowledge required. Usually, unit tests are written by simply -hardcoding the schema and then writing several modification statements one after the other, and then manually validating -results of a `SELECT` query. This might work for simple scenarios, but there’s still a chance that for some other schema -or some combination of values the tested feature may not work. - -To improve the situation, we can express the test in more abstract terms and, instead of writing it using specific -statements, we can describe which statement _types_ are to be used: - -``` -test(new SchemaGenerators.Builder("harry") - .partitionKeySpec(1, 5) - .clusteringKeySpec(1, 5) - .regularColumnSpec(1, 10) - .generator(), - historyBuilder -> { - historyBuilder.insert(); - historyBuilder.deletePartition(); - historyBuilder.deleteRowSlice(); - }); -``` - -This spec can be used to generate clusters of different sizes, configured with different schemas, executing the given a -sequence of actions both in isolation, and combined with other randomly generated ones, with failure-injection. - -Best of all is that this test will _not only_ ensure that such a sequence of actions does not produce an exception, but -also would ensure that cluster will respond with correct results to _any_ allowed read query. - -To begin specifying operations for a new partition, either start calling methods on the `HistoryBuilder`, or, if you -would like to specify the partition which Harry needs to visit use `#visitPartition` or `#beginBatch` have to be called, -for starting a visit to a particular partition with a single or multiple actions. - -After that, the actions are self-explanatory: `#insert`, `#update`, `#deleteRow`, `#deleteColumns`, `#deleteRowRange`, `#deleteRowSlice` -`#deletePartition`. - -After history generated by `HistoryBuilder` is replayed using `ReplayingVisitor` (or by using a `ReplayingHistoryBuilder` -which combines the two for your convenience), you can use any model (`QuiescentChecker` by default) to validate queries. -Queries can be provided manually or generated using `QueryGenerator` or `TypedQueryGenerator`. - -# Basic Terminology - -* Inflate / inflatable: a process of producing a value (for example, string, or a blob) from a `long` descriptor that - uniquely identifies the value. See data generation section of this guide for more details. -* Deflate / deflatable: a process of producing the descriptor the value was inflated from during verification. - See model section of this guide for more details. - -For definitions of logical timestamp, descriptor, and other entities used during inflation and deflation, refer -to formal relationships section. - -# Features - -Currently, Harry can exercise the following Cassandra functionality: - -* Supported data types: `int8`, `int16`, `int32`, `int64`, `boolean`, `float`, `double`, `ascii`, `uuid`, `timestamp`. - Collections are only _inflatable_. -* Random schema generation, with an arbitrary number of partition and clustering keys. -* Schemas with arbitrary `CLUSTERING ORDER BY` -* Randomly generated `INSERT` and `UPDATE` queries with all columns or arbitrary column subset -* Randomly generated `DELETE` queries: for a single column, single row, or a range of rows -* Inflating and validating entire partitions (with allowed in-flight queries) -* Inflating and validating random `SELECT` queries: single row, slices (with single open end), and ranges (with both - ends of clusterings specified) - -Inflating partitions is done using `Reconciler`. Validating partitions and random queries can be done using `QuiescentChecker`. - -## Outstanding Work - -#### The following have not yet been implemented: - -* Some types (such as collections) are not deflatable -* 2i queries are not implemented -* Fault injection is not implemented (available via Cassandra Simulator) -* TTL is not supported -* Some SELECT queries are not supported: `LIMIT`, `IN`, `GROUP BY`, token range queries - -#### Some features can be improved upon or further optimized: - -* Pagination is currently implemented but hard-coded to a page size of 1 -* RNG should be able to yield less than 64 bits of entropy per step -* State tracking should be done in a compact off-heap data structure -* Inflated partition state and per-row operation log should be done in a compact - off-heap data structure -* Decision-making about _when_ we visit partitions and/or rows should be improved - -While this list of improvements is incomplete, t should give the reader a rough idea about the state of the project. -The original goal of the project was to drive to stability after the significant storage engine rewrite in -CASSANDRA-8099 and help remove data loss bugs from the codebase before they got out into the wild. Next steps are to -integrate it into both CI and into regular daily dev workflows. - -# Goals: Reproducibility and Efficiency - -_Reproducibility_ is achieved by using the PCG family of random number generators and generating schema, configuration, -and every step of the workload from the repeatable sequence of random numbers. Schema and configuration are generated -from the _seed_. Each operation is assigned its own monotonically increasing _logical timestamp_, which preserves -logical operation order between different runs. - -_Efficiency_ is achieved by employing the features of the PCG random number generator (walking the sequence of random -numbers back and forth), and writing value generators in a way that preserves properties of the descriptor it was -generated from. - -Given a `long` _descriptor_ can be _inflated_ into some value: - -* value can be _deflated_ back to the descriptor it was generated from (in other words, generation is *invertible*) -* two inflated values will sort the same way as two descriptors they were generated from (in other words, generation is - *order-preserving*) - -These properties are also preserved for the composite values, such as clustering and partition keys. - -# Components - -Every Harry run starts from Configuration. You can find an example configuration under `conf/example.yml`. - -*Clock* is a component responsible for mapping _logical_ timestamps to _real-time_ ones. When reproducing test failures, -and for validation purposes, a snapshot of such be taken to map a real-time timestamp from the value retrieved from the -database to map it back to the logical timestamp of the operation that wrote this value. Given a real-time timestamp, -the clock can return a logical timestamp, and vice versa. - -*Runner* is a component that schedules operations that change the cluster (system under test) and model state. - -*System under test*: a Cassandra node or cluster. Default implementation is in_jvm (in-JVM DTest cluster). Harry also -supports external clusters. - -*Model* is responsible for tracking logical timestamps that system under test was notified about. - -*Partition descriptor selector* controls how partitions are selected based on the current logical timestamp. The default -implementation is a sliding window of partition descriptors that will visit one partition after the other in the -window `slide_after_repeats` times. After that, it will retire one partition descriptor, and pick a new one. - -*Clustering descriptor selector* controls how clustering keys are picked within the partition: how many rows there can -be in a partition, how many rows are visited for a logical timestamp, how many operations there will be in batch, what -kind of operations there will and how often each kind of operation is going to occur. - -# Implementing Tests - -All Harry components are pluggable and can be redefined. However, many of the default implementations will cover most of -the use-cases, so in this guide we’ll focus on ones that are most often used to implement different use cases: - -* System Under Test: defines how Harry can communicate with Cassandra instances and issue common queries. Examples of a - system under test can be a CCM cluster, a “real” Cassandra cluster, or an in-JVM dtest cluster. -* Visitor: defines behaviour that gets triggered at a specific logical timestamp. One of the default implementations is - MutatingVisitor, which executes write workload against SystemUnderTest. Examples of a visitor, besides a mutating - visitor, could be a validator that uses the model to validate results of different queries, a repair runner, or a - fault injector. -* Model: validates results of read queries by comparing its own internal representation against the results returned by - system under test. You can find three simplified implementations of model in this document: Visible Rows Checker, - Quiescent Checker, and an Exhaustive Checker. -* Runner: defines how operations defined by visitors are executed. Harry includes two default implementations: a - sequential and a concurrent runner. Sequential runner allows no overlap between different visitors or logical - timestamps. Concurrent runner allows visitors for different timestamps to overlap. - -System under test is the simplest one to implement: you only need a way to execute Cassandra queries. At the moment of -writing, all custom things, such as nodetool commands, failure injection, etc, are implemented using a SUT / visitor -combo: visitor knows about internals of the cluster it is dealing with. - -Generally, visitor has to follow the rules specified by DescriptorSelector and PdSelector: (it can only visit issue -mutations against the partition that PdSelector has picked for this LTS), and DescriptorSelector (it can visit exactly -DescriptorSelector#numberOfModifications rows within this partition, operations have to have a type specified by -#operationKind, clustering and value descriptors have to be in accordance with DescriptorSelector#cd and -DescriptorSelector#vds). The reason for these limitations is because model has to be able to reproduce the exact -sequence of events that was applied to system under test. - -Default implementations of partition and clustering descriptors, used in fuzz mode allow to optimise verification. For -example, it is possible to go find logical timestamps that are visiting the same partition as any given logical -timestamp. When running Harry in unit-test mode, we use a special generating visitor that keeps an entire given sequence -of events in memory rather than producing it on the fly. For reasons of efficiency, we do not use generating visitors -for generating and verifying large datasets. - -# Formal Relations Between Entities - -To be able to implement efficient models, we had to reduce the amount of state required for validation to a minimum and -try to operate on primitive data values most of the time. Any Harry run starts with a `seed`. Given the same -configuration, and the same seed, we're able to make runs deterministic (in other words, records in two clusters created -from the same seed are going to have different real-time timestamps, but will be otherwise identical; logical time -stamps will also be identical). - -Since it's clear how to generate things like random schemas, cluster configurations, etc., let's discuss how we're -generating data, and why this type of generation makes validation efficient. - -First, we're using PCG family of random number generators, which, besides having nice characteristics that any RNG -should have, have two important features: - -* Streams: for single seed, we can have several independent _different_ streams of random numbers. -* Walkability: PCG generators generate a stream of numbers you can walk _back_ and _forth_. That is, for any number _n_ - that represents a _position_ of the random number in the stream of random numbers, we can get the random number at - this position. Conversely, given a random number, we can determine what is its position in the stream. Moreover, - knowing a random number, we can determine which number precedes it in the stream of random numbers, and, finally, we - can determine how many numbers there are in a stream between the two random numbers. - -Out of these operations, determining the _next_ random number in the sequence can be done in constant time, `O(1)`. -Advancing generator by _n_ steps can be done in `O(log(n))` steps. Since generation is cyclical, advancing the iterator -backward is equivalent to advancing it by `cardinality - 1` steps. If we're generating 64 bits of entropy, advancing -by `-1` can be done in 64 steps. - -Let's introduce some definitions: - -* `lts` is a *logical timestamp*, an entity (number in our case), given by the clock, on which some action occurs -* `m` is a *modification id*, a sequential number of the modification that occurs on `lts` -* `rts` is an approximate real-time as of clock for this run -* `pid` is a partition position, a number between `0` and N, for `N` unique generated partitions -* `pd` is a partition descriptor, a unique descriptor identifying the partition -* `cd` is a clustering descriptor, a unique descriptor identifying row within some partition - -A small introduction that can help to understand the relation between these -entities. Hierarchically, the generation process looks as follows: - -* `lts` is an entry point, from which the decision process starts -* `pd` is picked from `lts`, and determines which partition is going to be visited -* for `(pd, lts)` combination, `#mods` (the number of modification batches) and `#rows` (the number of rows per - modification batch) is determined. `m` is an index of the modification batch, and `i` is an index of the operation in - the modification batch. -* `cd` is picked based on `(pd, lts)`, and `n`, a sequential number of the operation among all modification batches -* operation type (whether we're going to perform a write, delete, range delete, etc), columns involved in this - operation, and values for the modification are picked depending on the `pd`, `lts`, `m`, and `i` - -Most of this formalization is implemented in `OpSelectors`, and is relied upon in`PartitionVisitor` and any -implementation of a `Model`. - -Random number generation (see `OpSelectors#Rng`): - -* `rng(i, stream[, e])`: returns i'th number drawn from random sequence `stream` producing values with `e` bits of - entropy (64 bits for now). -* `rng'(s, stream[, e])`: returns `i` of the random number `s` drawn from the random sequence `stream`. This function is - an inverse of `rng`. -* `next(rnd, stream[, e])` and `prev(rnd, stream[, e])`: the next/previous number relative to `rnd` drawn from random - sequence `stream`. - -A simple example of a partition descriptor selector is one that is based on a sliding window of a size `s`, that slides -every `n` iterations. First, we determine _where_ the window should start for a given `lts` (in other words, how many -times it has already slid). After that, we determine which `pd` we pick out of `s` available ones. After picking each -one of the `s` descriptors `n` times, we retire the oldest descriptor and pick a new one to the window. Window start and -offset are then used as input for the `rng(start + offset, stream)` to make sure descriptors are uniformly distributed. - -We can build a clustering descriptor selector in a similar manner. Each partition will use its `pd` as a stream id, and -pick `cd` from a universe of possible `cds` of size `#cds`. On each `lts`, we pick a random `offset`, and start -picking `#ops` clusterings from this `offset < #cds`, and wrap around to index 0 after that. This way, each operation -maps to a unique `cd`, and `#op` can be determined from `cd` deterministically. - -# Data Generation - -So far, we have established how to generate partition, clustering, and value _descriptors_. Now, we need to understand -how we can generate data modification statements out of these descriptors in a way that helps us to validate data later. - -Since every run has a predefined schema, and by the time we visit a partition we have a logical timestamp, we can make -the rest of the decisions: pick a number of batches we're about to perform, determine what kind of operations each one -of the batches is going to contain, which rows we're going to visit (clustering for each modification operation). - -To generate a write, we need to know _which partition_ we're going to visit (in other words, partition descriptor), -_which row_ we'd like to modify (in other words, clustering descriptor), _which columns_ we're modifying (in other -words, a column mask), and, for each modified column - its value. By the time we're ready to make an actual query to the -database, we already know `pd`, `cd`, `rts`, and `vds[]`, which is all we need to "inflate" a write. - -To inflate each value descriptor, we take a generator for its datatype, and turn its descriptor into the object. This -generation process has the following important properties: - -* it is invertible: for every `inflate(vd) -> value`, there's `deflate(value) -> vd` -* it is order-preserving: `compare(vd1, vd2) == compare(inflate(vd1), inflate(vd2))` - -Inflating `pd` and `cd` is slightly more involved than inflating `vds`, since partition and clustering keys are often -composite. This means that `inflate(pd)` returns an array of objects, rather just a single -object: `inflate(pd) -> value[]`, and `deflate(value[]) -> pd`. Just like inflating value descriptors, inflating keys -preserves order. - -It is easy to see that, given two modifications: `Update(pd1, cd1, [vd1_1, vd2_1, vd3_1], lts1)` -and `Update(pd1, cd1, [vd1_2, vd3_2], lts2)`, we will end up with a resultset that contains effects of both -operations: `ResultSetRow(pd1, cd1, [vd1_2@rts2, vd2_1@rts1, vd3_2@rts2])`. - -# Model - -`Model` in Harry ties the rest of the components together and allows us to check whether or not data returned by the -cluster actually makes sense. The model relies on the clock, since we have to convert real-time timestamps of the -returned values back to logical timestamps, and on descriptor selectors to pick the right partition and rows. - -## Visible Rows Checker - -Let's try to put it all together and build a simple model. The simplest one is a visible row checker. It can check if -any row in the response returned from the database could have been produced by one of the operations. However, it won't -be able to find errors related to missing rows, and will only notice some cases of erroneously overwritten rows. - -In the model, we can see a response from the database in its deflated state. In other words, instead of the actual -values returned, we see their descriptors. Every resultset row consists of `pd`, `cd`, `vds[]` (value descriptors), -and `lts[]` (logical timestamps at which these values were written). - -To validate, we need to iterate through all operations for this partition, starting with the latest one the model is -aware of. This model has no internal state, and validates entire partitions: - -``` -void validatePartitionState(long validationLts, List rows) { - long pd = pdSelector.pd(validationLts, schema); - - for (ResultSetRow row : rows) { - // iterator that gives us unique lts from the row in descending order - LongIterator rowLtsIter = descendingIterator(row.lts); - // iterator that gives us unique lts from the model in descending order - LongIterator modelLtsIter = descendingIterator(pdSelector, validationLts); - - outer: - while (rowLtsIter.hasNext()) { - long rowLts = rowLtsIter.nextLong(); - - // this model can not check columns whose values were never written or were deleted - if (rowLts == NO_TIMESTAMP) - continue outer; - - if (!modelLtsIter.hasNext()) - throw new ValidationException(String.format("Model iterator is exhausted, could not verify %d lts for the row: \n%s %s", - rowLts, row)); - - while (modelLtsIter.hasNext()) { - long modelLts = modelLtsIter.nextLong(); - // column was written by the operation that has a lower lts than the current one from the model - if (modelLts > rowLts) - continue; - // column was written by the operation that has a higher lts, which contradicts to the model, since otherwise we'd validate it by now - if (modelLts < rowLts) - throw new RuntimeException("Can't find a corresponding event id in the model for: " + rowLts + " " + modelLts); - - // Compare values for columns that were supposed to be written with this lts - for (int col = 0; col < row.lts.length; col++) { - if (row.lts[col] != rowLts) - continue; - - long m = descriptorSelector.modificationId(pd, row.cd, rowLts, row.vds[col], col); - long vd = descriptorSelector.vd(pd, row.cd, rowLts, m, col); - - // If the value model predicts doesn't match the one received from the database, throw an exception - if (vd != row.vds[col]) - throw new RuntimeException("Returned value doesn't match the model"); - } - continue outer; - } - } - } -} -``` - -As you can see, all validation is done using deflated `ResultSetRows`, which contain enough data to say which logical -timestamp each value was written with, and which value descriptor each value has. This model can also validate data -concurrently to the ongoing data modification operations. - -## Quiescent Checker - -Let's consider one more checker. It'll be more powerful than the visible rows checker in one way since it can find any -inconsistency in data (incorrect timestamp, missing or additional row, rows coming in the wrong order, etc), but it'll -also have one limitation: it won't be able to run concurrently with data modification statements. This means that for -this model to be used, we should have no _in-flight_ queries, and all queries have to be in a deterministic state by the -time we're validating their results. - -For this checker, we assume that we have a component that is called `Reconciler`, which can inflate partition state _up -to some_ `lts`. `Reconciler` works by simply applying each modification in the same order they were applied to the -cluster, and using standard Cassandra data reconciliation rules (last write wins / DELETE wins over INSERT in case of a -timestamp collision). - -With this component, and knowing that there can be no in-fight queries, we can validate data in the following way: - -``` -public void validatePartitionState(Iterator actual, Query query) { - // find out up the highest completed logical timestamp - long maxCompleteLts = tracker.maxComplete(); - - // get the expected state from reconciler - Iterator expected = reconciler.inflatePartitionState(query.pd, maxCompleteLts, query).iterator(query.reverse); - - // compare actual and expected rows one-by-one in-order - while (actual.hasNext() && expected.hasNext()) { - ResultSetRow actualRowState = actual.next(); - Reconciler.RowState expectedRowState = expected.next(); - - if (actualRowState.cd != expectedRowState.cd) - throw new ValidationException("Found a row in the model that is not present in the resultset:\nExpected: %s\nActual: %s", - expectedRowState, actualRowState); - - if (!Arrays.equals(actualRowState.vds, expectedRowState.vds)) - throw new ValidationException("Returned row state doesn't match the one predicted by the model:\nExpected: %s (%s)\nActual: %s (%s).", - Arrays.toString(expectedRowState.vds), expectedRowState, - Arrays.toString(actualRowState.vds), actualRowState); - - if (!Arrays.equals(actualRowState.lts, expectedRowState.lts)) - throw new ValidationException("Timestamps in the row state don't match ones predicted by the model:\nExpected: %s (%s)\nActual: %s (%s).", - Arrays.toString(expectedRowState.lts), expectedRowState, - Arrays.toString(actualRowState.lts), actualRowState); - } - - if (actual.hasNext() || expected.hasNext()) { - throw new ValidationException("Expected results to have the same number of results, but %s result iterator has more results", - actual.hasNext() ? "actual" : "expected"); - } -} -``` - -If there's any mismatch, it'll be caught right away: if there's an extra row -(for example, there were issues in Cassandra that caused it to have duplicate -rows), or if some row or even value in the row is missing. - -## Exhaustive Checker - -To be able to both run validation concurrently to modifications and be able to -catch all kinds of inconsistencies, we need a more involved checker. - -In this checker, we rely on inflating partition state. However, we're most -interested in `lts`, `opId`, and visibility (whether or not it is still -in-flight) of each modification operation. To be able to give a reliable result, -we need to make sure we follow these rules: - -* every operation model _thinks_ should be visible, has to be visible -* every operation model _thinks_ should be invisible, has to be invisible -* every operation model doesn't know the state of (i.e., it is still - in-flight) can be _either_ visible _invisible_ -* there can be no state in the database that model is not aware of (in other words, - we either can _explain_ how a row came to be, or we conclude that the row is - erroneous) - -A naive way to do this would be to inflate every possible partition state, where -every in-flight operation would be either visible or invisible, but this gets -costly very quickly since the number of possible combinations grows -exponentially. A better (and simpler) way to do this is to iterate all -operations and keep the state of "explained" operations: - -``` -public class RowValidationState { - // every column starts in UNOBSERVED, and has to move to either REMOVED, or OBSERVED state - private final ColumnState[] columnStates; - // keep track of operations related to each column state - private final Operation[] causingOperations; -} -``` - -Now, we move through all operations for a given row, starting from the _newest_ ones, towards -the oldest ones: - -``` -public void validatePartitionState(long verificationLts, PeekingIterator actual_, Query query) { - // get a list of operations for each cd - NavigableMap> operations = inflatePartitionState(query); - - for (Map.Entry> entry : operations.entrySet()) { - long cd = entry.getKey(); - List ops = entry.getValue(); - - // Found a row that is present both in the model and in the resultset - if (actual.hasNext() && actual.peek().cd == cd) { - validateRow(new RowValidationState(actual.next), operations); - } else { - validateNoRow(cd, operations); - - // Row is not present in the resultset, and we currently look at modifications with a clustering past it - if (actual.hasNext() && cmp.compare(actual.peek().cd, cd) < 0) - throw new ValidationException("Couldn't find a corresponding explanation for the row in the model"); - } - } - - // if there are more rows in the resultset, and we don't have model explanation for them, we've found an issue - if (actual.hasNext()) - throw new ValidationException("Observed unvalidated rows"); -} -``` - -Now, we have to implement `validateRow` and `validateNoRow`. `validateNoRow` is easy: we only need to make sure that a -set of operations results in an invisible row. Since we're iterating operations in reverse order, if we encounter a -delete not followed by any writes, we can conclude that the row is invisible and exit early. If there's a write that is -not followed by a delete, and the row isn't covered by a range tombstone, we know it's an error. - -`validateRow` only has to iterate operations in reverse order until it can explain the value in every column. For -example, if a value is `UNOBSERVED`, and the first thing we encounter is a `DELETE` that removes this column, we only -need to make sure that the value is actually `null`, in which case we can conclude that the value can be explained -as `REMOVED`. - -Similarly, if we encounter an operation that has written the expected value, we conclude that the value is `OBSERVED`. -If there are any seeming inconsistencies between the model and resultset, we have to check whether or not the operation -in question is still in flight. If it is, its results may still not be visible, so we can't reliably say it's an error. - -To summarize, in order for us to implement an exhaustive checker, we have to iterate operations for each of the rows -present in the model in reverse order until we either detect inconsistency that can't be explained by an in-flight -operation or until we explain every value in the row. - -## Conclusion - -As you can see, all checkers up till now are almost entirely stateless. Exhaustive and quiescent models rely -on `DataTracker` component that is aware of the in-flight and completed `lts`, but don't need any other state apart from -that, since we can always inflate a complete partition from scratch every time we validate. - -While not relying on the state is a useful feature, at least _some_ state is useful to have. For example, if we're -validating just a few rows in the partition, right now we have to iterate through each and every `lts` that has visited -this partition and filter out only modifications that have visited it. However, since the model is notified of each -_started_, and, later, finished modification via `recordEvent`, we can keep track of `pd -> (cd -> lts)` map. You can -check out `VisibleRowsChecker` as an example of that. diff --git a/test/harry/main/org/apache/cassandra/harry/ColumnSpec.java b/test/harry/main/org/apache/cassandra/harry/ColumnSpec.java new file mode 100644 index 0000000000..1bad365240 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/ColumnSpec.java @@ -0,0 +1,610 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import accord.utils.Invariants; +import org.apache.cassandra.cql3.ast.Symbol; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.ByteType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.DoubleType; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.ShortType; +import org.apache.cassandra.db.marshal.TimeType; +import org.apache.cassandra.db.marshal.TimeUUIDType; +import org.apache.cassandra.db.marshal.TimestampType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.TypeAdapters; +import org.apache.cassandra.utils.ByteArrayUtil; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.TimeUUID; + +// TODO: counters +// TODO: UDTs +// TODO: collections: frozen/unfrozen +// TODO: empty / 0 / min / max values if present +public class ColumnSpec +{ + public final String name; + public final DataType type; + public final Generator gen; + public final Kind kind; + + public ColumnSpec(String name, + DataType type, + Generator gen, + Kind kind) + { + + this.name = name; + this.type = Invariants.nonNull(type); + this.gen = Invariants.nonNull(gen); + this.kind = kind; + } + + public String toCQL() + { + return String.format("%s %s%s", + Symbol.maybeQuote(name), + type, + 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.cqlName, that.type.cqlName) && + kind == that.kind; + } + + @Override + public int hashCode() + { + return Objects.hash(name, type.cqlName, kind); + } + + public String name() + { + return name; + } + + public boolean isReversed() + { + return type.isReversed(); + } + + public String toString() + { + return name + '(' + type.toString() + ")"; + } + + public Generator gen() + { + return gen; + } + + public static ColumnSpec pk(String name, DataType type, Generator gen) + { + return new ColumnSpec<>(name, type, gen, Kind.PARTITION_KEY); + } + + public static ColumnSpec pk(String name, DataType type) + { + return new ColumnSpec<>(name, type, (Generator) TypeAdapters.forValues(type.asServerType()), Kind.PARTITION_KEY); + } + + @SuppressWarnings("unchecked") + public static ColumnSpec ck(String name, DataType type, Generator gen, boolean isReversed) + { + return new ColumnSpec(name, isReversed ? ReversedType.getInstance(type) : type, gen, Kind.CLUSTERING); + } + + public static ColumnSpec ck(String name, DataType type) + { + return ck(name, type, false); + } + + public static ColumnSpec ck(String name, DataType type, boolean isReversed) + { + return new ColumnSpec(name, isReversed ? ReversedType.getInstance(type) : type, + TypeAdapters.forValues(type.asServerType()), + Kind.CLUSTERING); + } + + + public static ColumnSpec regularColumn(String name, DataType type, Generator gen) + { + return new ColumnSpec<>(name, type, gen, Kind.REGULAR); + } + + public static ColumnSpec regularColumn(String name, DataType type) + { + return new ColumnSpec(name, type, TypeAdapters.forValues(type.asServerType()), Kind.REGULAR); + } + + public static ColumnSpec staticColumn(String name, DataType type, Generator gen) + { + return new ColumnSpec<>(name, type, gen, Kind.STATIC); + } + + public static ColumnSpec staticColumn(String name, DataType type) + { + return new ColumnSpec(name, type, TypeAdapters.forValues(type.asServerType()), Kind.STATIC); + } + + public enum Kind + { + CLUSTERING, REGULAR, STATIC, PARTITION_KEY + } + + public static abstract class DataType + { + protected final String cqlName; + + protected DataType(String cqlName) + { + this.cqlName = cqlName; + } + + public abstract /* unsigned */ long typeEntropy(); + + public boolean isReversed() + { + return false; + } + + public abstract AbstractType asServerType(); + + public final String toString() + { + return cqlName; + } + + public final 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 final int hashCode() + { + return Objects.hash(cqlName); + } + + public abstract Comparator comparator(); + } + + public static abstract class ComparableDataType> extends DataType + { + protected ComparableDataType(String cqlName) + { + super(cqlName); + } + + @Override + public Comparator comparator() + { + return Comparable::compareTo; + } + } + + public static final DataType int8Type = new ComparableDataType<>("tinyint") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << (8 * Byte.BYTES); + } + + @Override + public AbstractType asServerType() + { + return ByteType.instance; + } + }; + + public static final DataType int16Type = new ComparableDataType<>("smallint") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << Byte.SIZE; + } + + @Override + public AbstractType asServerType() + { + return ShortType.instance; + } + }; + + public static final DataType int32Type = new ComparableDataType<>("int") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << Integer.SIZE; + } + + @Override + public AbstractType asServerType() + { + return Int32Type.instance; + } + }; + + public static final DataType int64Type = new ComparableDataType("bigint") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << (8 * Long.BYTES - 1); + } + + @Override + public AbstractType asServerType() + { + return LongType.instance; + } + }; + + public static final DataType booleanType = new ComparableDataType("boolean") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 2; + } + + @Override + public AbstractType asServerType() + { + return BooleanType.instance; + } + }; + + public static final DataType floatType = new ComparableDataType("float") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << (4 * Float.BYTES); + } + + @Override + public AbstractType asServerType() + { + return FloatType.instance; + } + }; + + public static final DataType doubleType = new ComparableDataType("double") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return DoubleType.instance; + } + }; + + public static final DataType blobType = new DataType<>("blob") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return BytesType.instance; + } + + public Comparator comparator() + { + return ByteBufferUtil::compareUnsigned; + } + }; + + public static final DataType asciiType = new ComparableDataType<>("ascii") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return AsciiType.instance; + } + }; + + // utf8 + public static final DataType textType = new DataType<>("text") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return UTF8Type.instance; + } + + @Override + public Comparator comparator() + { + return (o1, o2) -> ByteArrayUtil.compareUnsigned(o1.getBytes(), o2.getBytes()); + } + }; + + public static final DataType uuidType = new DataType<>("uuid") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return UUIDType.instance; + } + + public Comparator comparator() + { + // TODO: avoid serialization to match C* order + return (o1, o2) -> UUIDType.instance.compare(UUIDType.instance.decompose(o1), + UUIDType.instance.decompose(o2)); + } + }; + + public static final DataType timeUuidType = new ComparableDataType<>("timeuuid") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return TimeUUIDType.instance; + } + }; + + public static final DataType timestampType = new ComparableDataType<>("timestamp") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return TimestampType.instance; + } + }; + + public static final DataType varintType = new ComparableDataType<>("varint") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return IntegerType.instance; + } + }; + + public static final DataType timeType = new ComparableDataType<>("time") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return TimeType.instance; + } + }; + + public static final DataType decimalType = new ComparableDataType<>("decimal") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return DecimalType.instance; + } + }; + + public static final DataType inetType = new DataType<>("inet") + { + @Override + public /* unsigned */ long typeEntropy() + { + return 1L << 63; + } + + @Override + public AbstractType asServerType() + { + return InetAddressType.instance; + } + + @Override + public Comparator comparator() + { + return (o1, o2) -> { + byte[] b1 = o1.getAddress(); + byte[] b2 = o2.getAddress(); + return ByteArrayUtil.compareUnsigned(b1, b2); + }; + } + }; + + public static final List> TYPES; + + static + { + List> types = new ArrayList<>() + {{ + add(int8Type); + add(int16Type); + add(int32Type); + add(int64Type); + add(floatType); + add(doubleType); + // TODO: SAI tests seem to fail these types + // add(booleanType); + // add(inetType); + // add(varintType); + // add(decimalType); + add(asciiType); + add(textType); + // TODO: blob is not supported in SAI + // add(blobType); + add(uuidType); + add(timestampType); + // TODO: SAI test fails due to TimeSerializer#toString in tracing + // add(timeType); + // TODO: compose proper value + // add(timeUuidType); + }}; + TYPES = Collections.unmodifiableList(types); + } + + public static Generator> regularColumnTypeGen() + { + return Generators.pick(TYPES); + } + + public static Generator> clusteringColumnTypeGen() + { + return Generators.pick(new ArrayList<>(ReversedType.cache.keySet())); + } + + public static class ReversedType extends DataType + { + public static final Map, ReversedType> cache = new HashMap<>() + {{ + for (DataType type : TYPES) + put(type, new ReversedType<>(type)); + }}; + + private final DataType baseType; + + public ReversedType(DataType baseType) + { + super(baseType.cqlName); + this.baseType = baseType; + } + + @Override + public /* unsigned */ long typeEntropy() + { + return baseType.typeEntropy(); + } + + public boolean isReversed() + { + return true; + } + + @Override + public AbstractType asServerType() + { + return org.apache.cassandra.db.marshal.ReversedType.getInstance(baseType.asServerType()); + } + + public static DataType getInstance(DataType type) + { + ReversedType t = (ReversedType) cache.get(type); + if (t == null) + t = new ReversedType<>(type); + assert t.baseType == type : String.format("Type mismatch %s != %s", t.baseType, type); + return t; + } + + @Override + public Comparator comparator() + { + return baseType.comparator(); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/HarryHelper.java b/test/harry/main/org/apache/cassandra/harry/HarryHelper.java deleted file mode 100644 index 9dc0fcfaf4..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/HarryHelper.java +++ /dev/null @@ -1,172 +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.harry; - -import java.io.File; -import java.util.concurrent.atomic.AtomicInteger; - -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; - -public class HarryHelper -{ - public static final String KEYSPACE = "harry"; - - public static void init() - { - // setting both ways as changes between versions - LOG4J2_DISABLE_JMX.setBoolean(true); - LOG4J2_DISABLE_JMX_LEGACY.setBoolean(true); - LOG4J_SHUTDOWN_HOOK_ENABLED.setBoolean(false); - CASSANDRA_ALLOW_SIMPLE_STRATEGY.setBoolean(true); // makes easier to share OSS tests without RF limits - CASSANDRA_MINIMUM_REPLICATION_FACTOR.setInt(0); // makes easier to share OSS tests without RF limits - DISABLE_TCACTIVE_OPENSSL.setBoolean(true); - IO_NETTY_TRANSPORT_NONATIVE.setBoolean(true); - ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); - } - - public static Configuration configuration(String... args) throws Exception - { - File configFile = HarryRunner.loadConfig(args); - Configuration configuration = Configuration.fromFile(configFile); - System.out.println("Using configuration generated from: " + configFile); - return configuration; - } - - private static AtomicInteger counter = new AtomicInteger(); - - public static Surjections.Surjection schemaSpecGen(String keyspace, String prefix) - { - return new SchemaGenerators.Builder(keyspace, () -> prefix + counter.getAndIncrement()) - .partitionKeySpec(1, 2, - ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.doubleType, - ColumnSpec.asciiType, - ColumnSpec.textType) - .clusteringKeySpec(1, 2, - ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.doubleType, - ColumnSpec.asciiType, - ColumnSpec.textType, - ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), - ColumnSpec.ReversedType.getInstance(ColumnSpec.doubleType), - ColumnSpec.ReversedType.getInstance(ColumnSpec.asciiType), - ColumnSpec.ReversedType.getInstance(ColumnSpec.textType)) - .regularColumnSpec(1, 5, - ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType(4, 128)) - .staticColumnSpec(0, 5, - ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType(4, 128)) - .surjection(); - } - - public static Configuration.ConfigurationBuilder defaultConfiguration() throws Exception - { - return new Configuration.ConfigurationBuilder() - .setClock(() -> new OffsetClock(100000)) - .setCreateSchema(true) - .setTruncateTable(false) - .setDropSchema(false) - .setSchemaProvider((seed, sut) -> schemaSpecGen("harry", "tbl_").inflate(seed)) - .setClusteringDescriptorSelector(defaultClusteringDescriptorSelectorConfiguration().build()) - .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1)) - .setDataTracker(new Configuration.DefaultDataTrackerConfiguration()) - .setRunner((run, configuration) -> { - throw new IllegalArgumentException("Runner is not configured by default."); - }) - .setMetricReporter(new Configuration.NoOpMetricReporterConfiguration()); - } - - public static ReplayingHistoryBuilder dataGen(SystemUnderTest sut, TokenPlacementModel.ReplicationFactor rf, SystemUnderTest.ConsistencyLevel writeCl) - { - return dataGen(1, sut, rf, writeCl); - } - - public static ReplayingHistoryBuilder dataGen(long seed, SystemUnderTest sut, TokenPlacementModel.ReplicationFactor rf, SystemUnderTest.ConsistencyLevel writeCl) - { - 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() - .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(1)) - .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, 1) - .addWeight(OpSelectors.OperationKind.INSERT_WITH_STATICS, 20) - .addWeight(OpSelectors.OperationKind.INSERT, 20) - .addWeight(OpSelectors.OperationKind.UPDATE_WITH_STATICS, 20) - .addWeight(OpSelectors.OperationKind.UPDATE, 20) - .build()); - } - - public static Configuration.CDSelectorConfigurationBuilder singleRowPerModification() - { - return new Configuration.CDSelectorConfigurationBuilder() - .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(1)) - .setMaxPartitionSize(100) - .setOperationKindWeights(new Configuration.OperationKindSelectorBuilder() - .addWeight(OpSelectors.OperationKind.INSERT_WITH_STATICS, 100) - .build()); - } -} - diff --git a/test/harry/main/org/apache/cassandra/harry/MagicConstants.java b/test/harry/main/org/apache/cassandra/harry/MagicConstants.java new file mode 100644 index 0000000000..9e7137fccc --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/MagicConstants.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import java.util.Set; + +import org.apache.cassandra.harry.util.BitSet; + +public class MagicConstants +{ + public static final BitSet ALL_COLUMNS = BitSet.allSet(64); + /** + * For keys + */ + public static final Object[] UNKNOWN_KEY = new Object[]{}; + public static final Object[] NIL_KEY = new Object[]{}; + + /** + * For values + */ + public static final Object UNKNOWN_VALUE = new Object() { + public String toString() + { + return "UNKNOWN"; + } + }; + public static final Object UNSET_VALUE = new Object() { + public String toString() + { + return "UNSET"; + } + }; + /** + * For Descriptors + */ + public static final long UNKNOWN_DESCR = Long.MIN_VALUE + 2; + // TODO: Empty value, for the types that support it + public static final long EMPTY_VALUE_DESCR = Long.MIN_VALUE + 1; + public static final long UNSET_DESCR = Long.MIN_VALUE + 3; + public static final long NIL_DESCR = Long.MIN_VALUE; + public static final Set MAGIC_DESCRIPTOR_VALS = Set.of(UNKNOWN_DESCR, EMPTY_VALUE_DESCR, UNSET_DESCR, NIL_DESCR); + /** + * For LTS + */ + public static final long[] LTS_UNKNOWN = new long[]{}; + public static final long NO_TIMESTAMP = Long.MIN_VALUE; + + /** + * For indices + */ + public static final int UNKNOWN_IDX = Integer.MIN_VALUE + 2; + public static final int UNSET_IDX = Integer.MIN_VALUE + 1; + public static final int NIL_IDX = Integer.MIN_VALUE; +} diff --git a/test/harry/main/org/apache/cassandra/harry/Relations.java b/test/harry/main/org/apache/cassandra/harry/Relations.java new file mode 100644 index 0000000000..91f64ded48 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/Relations.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.IntFunction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.gen.Bijections; + +public class Relations +{ + private static final Logger logger = LoggerFactory.getLogger(Relations.class); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static boolean matchRange(Bijections.Bijection ckGen, + IntFunction> comparators, + int ckCoulmnCount, + long lowBoundDescr, long highBoundDescr, + Relations.RelationKind[] lowBoundRelations, Relations.RelationKind[] highBoundRelations, + long matchDescr) + { + Object[] lowBoundValue = lowBoundDescr == MagicConstants.UNSET_DESCR ? null : ckGen.inflate(lowBoundDescr); + Object[] highBoundValue = highBoundDescr == MagicConstants.UNSET_DESCR ? null : ckGen.inflate(highBoundDescr); + Object[] matchValue = ckGen.inflate(matchDescr); + // TODO: assert that all equals + null checks + for (int i = 0; i < ckCoulmnCount; i++) + { + Object matched = matchValue[i]; + + if (lowBoundValue != null) + { + Object l = lowBoundValue[i]; + Relations.RelationKind lr = lowBoundRelations[i]; + + if (lr != null && !lr.match(comparators.apply(i), matched, l)) + { + if (logger.isTraceEnabled()) + logger.trace("Low Bound {} {} {} did match {}", lowBoundValue[i], lr, matchValue[i], i); + return false; + } + } + + if (highBoundValue != null) + { + Object h = highBoundValue[i]; + Relations.RelationKind hr = highBoundRelations[i]; + + if (hr != null && !hr.match(comparators.apply(i), matched, h)) + { + if (logger.isTraceEnabled()) + logger.trace("High Bound {} {} {} did match {}", highBoundValue[i], hr, matchValue[i], i); + return false; + } + } + } + + if (logger.isTraceEnabled()) + logger.trace("{} is between {} and {} fully matched match", Arrays.toString(matchValue), Arrays.toString(lowBoundValue), Arrays.toString(highBoundValue)); + return true; + } + + public static class Relation + { + public final Relations.RelationKind kind; + public final long descriptor; + public final int column; + public Relation(RelationKind kind, long descriptor, int column) + { + this.kind = kind; + this.descriptor = descriptor; + this.column = column; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public enum RelationKind + { + LT + { + @Override + public boolean match(Comparator comparator, Object l, Object r) + { + return comparator.compare(l, r) < 0; + } + + @Override + public String symbol() + { + return "<"; + } + + @Override + public RelationKind reverse() + { + return GT; + } + }, + GT + { + @Override + public boolean match(Comparator comparator, Object l, Object r) + { + return comparator.compare(l, r) > 0; + } + + @Override + public String symbol() + { + return ">"; + } + + @Override + public RelationKind reverse() + { + return LT; + } + }, + LTE + { + @Override + public boolean match(Comparator comparator, Object l, Object r) + { + return comparator.compare(l, r) <= 0; + } + + @Override + public String symbol() + { + return "<="; + } + + @Override + public RelationKind reverse() + { + return GTE; + } + }, + GTE + { + @Override + public boolean match(Comparator comparator, Object l, Object r) + { + return comparator.compare(l, r) >= 0; + } + + @Override + public String symbol() + { + return ">="; + } + + @Override + public RelationKind reverse() + { + return LTE; + } + }, + EQ + { + @Override + public boolean match(Comparator comparator, Object l, Object r) + { + return comparator.compare(l, r) == 0; + } + + @Override + public String symbol() + { + return "="; + } + + @Override + public RelationKind reverse() + { + return EQ; + } + }; + + public abstract boolean match(Comparator comparator, Object l, Object r); + + public abstract String symbol(); + + public abstract RelationKind reverse(); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/SchemaSpec.java b/test/harry/main/org/apache/cassandra/harry/SchemaSpec.java new file mode 100644 index 0000000000..5ab6862994 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/SchemaSpec.java @@ -0,0 +1,440 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import org.apache.cassandra.cql3.ast.Symbol; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.util.IteratorsUtil; +import org.apache.cassandra.utils.ByteArrayUtil; + +import static org.apache.cassandra.harry.gen.InvertibleGenerator.MAX_ENTROPY; + +public class SchemaSpec +{ + public final String keyspace; + public final String table; + + public final List> partitionKeys; + public final List> clusteringKeys; + public final List> regularColumns; + public final List> staticColumns; + + public final List> allColumnInSelectOrder; + public final ValueGenerators valueGenerators; + public final Options options; + + public SchemaSpec(long seed, + int populationPerColumn, + String keyspace, + String table, + List> partitionKeys, + List> clusteringKeys, + List> regularColumns, + List> staticColumns) + { + this(seed, populationPerColumn, keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, optionsBuilder()); + } + + @SuppressWarnings({ "unchecked" }) + public SchemaSpec(long seed, + int populationPerColumn, + String keyspace, + String table, + List> partitionKeys, + List> clusteringKeys, + List> regularColumns, + List> staticColumns, + Options options) + { + this.keyspace = keyspace; + this.table = table; + this.options = options; + + this.partitionKeys = Collections.unmodifiableList(new ArrayList<>(partitionKeys)); + this.clusteringKeys = Collections.unmodifiableList(new ArrayList<>(clusteringKeys)); + this.staticColumns = Collections.unmodifiableList(new ArrayList<>(staticColumns)); + this.regularColumns = Collections.unmodifiableList(new ArrayList<>(regularColumns)); + + List> staticSelectOrder = new ArrayList<>(staticColumns); + staticSelectOrder.sort((s1, s2) -> ByteArrayUtil.compareUnsigned(s1.name.getBytes(), s2.name.getBytes())); + List> regularSelectOrder = new ArrayList<>(regularColumns); + regularSelectOrder.sort((s1, s2) -> ByteArrayUtil.compareUnsigned(s1.name.getBytes(), s2.name.getBytes())); + + List> selectOrder = new ArrayList<>(); + for (ColumnSpec column : IteratorsUtil.concat(partitionKeys, + clusteringKeys, + staticSelectOrder, + regularSelectOrder)) + selectOrder.add(column); + this.allColumnInSelectOrder = Collections.unmodifiableList(selectOrder); + + // TODO: empty gen + this.valueGenerators = HistoryBuilder.valueGenerators(this, seed, populationPerColumn); + } + + public static /* unsigned */ long cumulativeEntropy(List> columns) + { + if (columns.isEmpty()) + return 0; + + long entropy = 1; + for (ColumnSpec column : columns) + { + if (Long.compareUnsigned(column.type.typeEntropy(), MAX_ENTROPY) == 0) + return MAX_ENTROPY; + + long next = entropy * column.type.typeEntropy(); + if (Long.compareUnsigned(next, entropy) < 0 || Long.compareUnsigned(next, column.type.typeEntropy()) < 0) + return MAX_ENTROPY; + + entropy = next; + } + + return entropy; + } + + public static Generator forKeys(List> columns) + { + Generator[] gens = new Generator[columns.size()]; + for (int i = 0; i < gens.length; i++) + gens[i] = columns.get(i).gen; + return Generators.zipArray(gens); + } + + public String compile() + { + StringBuilder sb = new StringBuilder(); + + sb.append("CREATE TABLE "); + if (options.ifNotExists()) + sb.append("IF NOT EXISTS "); + + sb.append(Symbol.maybeQuote(keyspace)) + .append(".") + .append(Symbol.maybeQuote(table)) + .append(" ("); + + SeparatorAppender commaAppender = new SeparatorAppender(); + for (ColumnSpec cd : partitionKeys) + { + commaAppender.accept(sb); + sb.append(cd.toCQL()); + if (partitionKeys.size() == 1 && clusteringKeys.isEmpty()) + sb.append(" PRIMARY KEY"); + } + + for (ColumnSpec cd : IteratorsUtil.concat(clusteringKeys, + staticColumns, + regularColumns)) + { + commaAppender.accept(sb); + sb.append(cd.toCQL()); + } + + if (!clusteringKeys.isEmpty() || partitionKeys.size() > 1) + { + sb.append(", ").append(getPrimaryKeyCql()); + } + + // TODO: test + if (options.trackLts()) + sb.append(", ").append("visited_lts list static"); + + sb.append(')'); + + Runnable appendWith = doOnce(() -> sb.append(" WITH")); + boolean shouldAppendAnd = false; + + if (options.compactStorage()) + { + appendWith.run(); + sb.append(" COMPACT STORAGE"); + shouldAppendAnd = true; + } + + if (options.disableReadRepair()) + { + appendWith.run(); + if (shouldAppendAnd) + sb.append(" AND"); + sb.append(" read_repair = 'NONE' "); + shouldAppendAnd = true; + } + + if (options.compactionStrategy() != null) + { + appendWith.run(); + if (shouldAppendAnd) + sb.append(" AND"); + sb.append(" compaction = {'class': '").append(options.compactionStrategy()).append("'}"); + shouldAppendAnd = true; + } + + if (!clusteringKeys.isEmpty()) + { + appendWith.run(); + if (shouldAppendAnd) + { + sb.append(" AND"); + shouldAppendAnd = false; + } + sb.append(getClusteringOrderCql()); + } + + if (shouldAppendAnd) + { + sb.append(" AND"); + shouldAppendAnd = false; + } + + sb.append(';'); + return sb.toString(); + } + + private String getClusteringOrderCql() + { + StringBuilder sb = new StringBuilder(); + if (!clusteringKeys.isEmpty()) + { + 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}", compile()); + } + + private static Runnable doOnce(Runnable r) + { + return new Runnable() + { + boolean executed = false; + + public void run() + { + if (executed) + return; + + executed = true; + r.run(); + } + }; + } + + public static class SeparatorAppender implements Consumer + { + boolean isFirst = true; + private final String separator; + + public SeparatorAppender() + { + this(","); + } + + public SeparatorAppender(String separator) + { + this.separator = separator; + } + + public void accept(StringBuilder stringBuilder) + { + if (isFirst) + isFirst = false; + else + stringBuilder.append(separator); + } + + public void accept(StringBuilder stringBuilder, String s) + { + accept(stringBuilder); + stringBuilder.append(s); + } + + + public void reset() + { + isFirst = true; + } + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SchemaSpec that = (SchemaSpec) o; + return Objects.equals(keyspace, that.keyspace) && + Objects.equals(table, that.table) && + Objects.equals(partitionKeys, that.partitionKeys) && + Objects.equals(clusteringKeys, that.clusteringKeys) && + Objects.equals(regularColumns, that.regularColumns); + } + + public int hashCode() + { + return Objects.hash(keyspace, table, partitionKeys, clusteringKeys, regularColumns); + } + + public interface Options + { + boolean addWriteTimestamps(); + boolean disableReadRepair(); + String compactionStrategy(); + boolean compactStorage(); + boolean ifNotExists(); + boolean trackLts(); + } + + public static OptionsBuilder optionsBuilder() + { + return new OptionsBuilder(); + } + + public static class OptionsBuilder implements Options + { + private boolean addWriteTimestamps = true; + private boolean disableReadRepair = false; + private String compactionStrategy = null; + private boolean ifNotExists = false; + private boolean trackLts = false; + private boolean compactStorage = false; + + private OptionsBuilder() + { + } + + public OptionsBuilder addWriteTimestamps(boolean newValue) + { + this.addWriteTimestamps = newValue; + return this; + } + + @Override + public boolean addWriteTimestamps() + { + return addWriteTimestamps; + } + + public OptionsBuilder disableReadRepair(boolean newValue) + { + this.disableReadRepair = newValue; + return this; + } + + @Override + public boolean disableReadRepair() + { + return disableReadRepair; + } + + public OptionsBuilder compactionStrategy(String compactionStrategy) + { + this.compactionStrategy = compactionStrategy; + return this; + } + + @Override + public String compactionStrategy() + { + return compactionStrategy; + } + + public OptionsBuilder withCompactStorage() + { + this.compactStorage = true; + return this; + } + + @Override + public boolean compactStorage() + { + return compactStorage; + } + + public OptionsBuilder ifNotExists(boolean v) + { + this.ifNotExists = v; + return this; + } + + @Override + public boolean ifNotExists() + { + return ifNotExists; + } + + public OptionsBuilder trackLts(boolean v) + { + this.trackLts = v; + return this; + } + + @Override + public boolean trackLts() + { + return trackLts; + } + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/gen/ExtensionsTest.java b/test/harry/main/org/apache/cassandra/harry/ValueGeneratorHelper.java similarity index 58% rename from test/distributed/org/apache/cassandra/fuzz/harry/gen/ExtensionsTest.java rename to test/harry/main/org/apache/cassandra/harry/ValueGeneratorHelper.java index d6d331eed5..c4bbababc0 100644 --- a/test/distributed/org/apache/cassandra/fuzz/harry/gen/ExtensionsTest.java +++ b/test/harry/main/org/apache/cassandra/harry/ValueGeneratorHelper.java @@ -16,29 +16,26 @@ * limitations under the License. */ -package org.apache.cassandra.fuzz.harry.gen; +package org.apache.cassandra.harry; -import java.util.function.Supplier; - -import org.junit.Assert; -import org.junit.Test; +import java.util.function.IntFunction; +import org.apache.cassandra.harry.dsl.HistoryBuilder; import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.gen.Generators; -public class ExtensionsTest +public class ValueGeneratorHelper { - - @Test - public void testPick() + public static long[] randomDescriptors(EntropySource rng, IntFunction> valueGens, int count) { - Supplier gen = Generators.pick(101, 102, 103, 104, 105).bind(EntropySource.forTests()); + long[] vds = new long[count]; + for (int i = 0; i < count; i++) + { + if (rng.nextBoolean()) + vds[i] = MagicConstants.UNSET_DESCR; + else + vds[i] = valueGens.apply(i).descriptorAt(rng.nextInt(count)); + } - 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); + return vds; } } diff --git a/test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java b/test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java index a5dc2ffd25..9b4ad21317 100644 --- a/test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java +++ b/test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java @@ -53,13 +53,13 @@ public class ModelChecker run(minSteps, maxSteps, new JdkRandomEntropySource(System.currentTimeMillis())); } - public void run(int minSteps, long maxSteps, EntropySource entropySource) throws Throwable + public void run(int minSteps, long maxSteps, EntropySource rng) throws Throwable { assert init != null : "Initial condition is not specified"; Ref> state = new Ref<>(init, Pair.unchanged()); if (beforeAll != null) - state.map((s) -> beforeAll.next(s.l, s.r, entropySource)); + state.map((s) -> beforeAll.next(s.l, s.r, rng)); for (int i = 0; i < maxSteps; i++) { @@ -67,13 +67,13 @@ public class ModelChecker return; // TODO: add randomisation / probability for triggering a specific step - steps.get(entropySource.nextInt(steps.size())).execute(state, entropySource.derive()); + steps.get(rng.nextInt(steps.size())).execute(state, rng.derive()); for (Precondition invariant : invariants) invariant.test(state.get()); } if (afterAll != null) - state.map((s) -> afterAll.next(s.l, s.r, entropySource)); + state.map((s) -> afterAll.next(s.l, s.r, rng)); } public ModelChecker init(STATE state, SUT sut) @@ -111,12 +111,12 @@ public class ModelChecker public ModelChecker step(Precondition precondition, Step step) { - steps.add((ref, entropySource) -> { + steps.add((ref, rng) -> { ref.map(state -> { if (!precondition.test(state)) return state; - return step.next(state.l, state.r, entropySource); + return step.next(state.l, state.r, rng); }); }); @@ -136,7 +136,7 @@ public class ModelChecker public ModelChecker step(StatePrecondition precondition, ThrowingFunction step) { - steps.add((ref, entropySource) -> { + steps.add((ref, rng) -> { ref.map(state -> { if (!precondition.test(state.l)) return state; @@ -154,7 +154,7 @@ public class ModelChecker public ModelChecker step(StatePrecondition precondition, ThrowingBiFunction step) { - steps.add((ref, entropySource) -> { + steps.add((ref, rng) -> { ref.map(state -> { if (!precondition.test(state.l)) return state; @@ -172,12 +172,12 @@ public class ModelChecker public ModelChecker step(ThrowingFunction step) { - return step((t, sut, entropySource) -> new Pair<>(step.apply(t), sut)); + return step((t, sut, rng) -> new Pair<>(step.apply(t), sut)); } interface StepExecutor { - void execute(Ref> state, EntropySource entropySource) throws Throwable; + void execute(Ref> state, EntropySource rng) throws Throwable; } public interface StatePrecondition @@ -202,7 +202,7 @@ public class ModelChecker public interface Step { - Pair next(STATE t, SUT sut, EntropySource entropySource) throws Throwable; + Pair next(STATE t, SUT sut, EntropySource rng) throws Throwable; } public interface ThrowingConsumer @@ -289,7 +289,7 @@ public class ModelChecker @SuppressWarnings("unused") public Simple beforeAll(ThrowingConsumer beforeAll) { - ModelChecker.this.beforeAll = (t, sut, entropySource) -> { + ModelChecker.this.beforeAll = (t, sut, rng) -> { beforeAll.accept(t); return Pair.unchanged(); }; @@ -298,8 +298,8 @@ public class ModelChecker public Simple beforeAll(ThrowingBiConsumer beforeAll) { - ModelChecker.this.beforeAll = (t, sut, entropySource) -> { - beforeAll.accept(t, entropySource); + ModelChecker.this.beforeAll = (t, sut, rng) -> { + beforeAll.accept(t, rng); return Pair.unchanged(); }; return this; @@ -308,13 +308,13 @@ public class ModelChecker @SuppressWarnings("unused") public Simple beforeAll(ThrowingFunction beforeAll) { - ModelChecker.this.beforeAll = (t, sut, entropySource) -> new Pair<>(beforeAll.apply(t), sut); + ModelChecker.this.beforeAll = (t, sut, rng) -> new Pair<>(beforeAll.apply(t), sut); return this; } public Simple afterAll(ThrowingConsumer afterAll) { - ModelChecker.this.afterAll = (t, sut, entropySource) -> { + ModelChecker.this.afterAll = (t, sut, rng) -> { afterAll.accept(t); return Pair.unchanged(); }; @@ -324,7 +324,7 @@ public class ModelChecker @SuppressWarnings("unused") public Simple afterAll(ThrowingFunction afterAll) { - ModelChecker.this.afterAll = (t, sut, entropySource) -> new Pair(afterAll.apply(t), sut); + ModelChecker.this.afterAll = (t, sut, rng) -> new Pair(afterAll.apply(t), sut); return this; } @@ -343,13 +343,13 @@ public class ModelChecker public Simple step(ThrowingFunction step) { - ModelChecker.this.step((state, sut, entropySource) -> new Pair<>(step.apply(state), sut)); + ModelChecker.this.step((state, sut, rng) -> new Pair<>(step.apply(state), sut)); return this; } public Simple step(ThrowingConsumer step) { - ModelChecker.this.step((state, sut, entropySource) -> { + ModelChecker.this.step((state, sut, rng) -> { step.accept(state); return Pair.unchanged(); }); @@ -358,8 +358,8 @@ public class ModelChecker public Simple step(ThrowingBiConsumer step) { - ModelChecker.this.step((state, sut, entropySource) -> { - step.accept(state, entropySource); + ModelChecker.this.step((state, sut, rng) -> { + step.accept(state, rng); return Pair.unchanged(); }); return this; @@ -367,11 +367,11 @@ public class ModelChecker public Simple step(BiPredicate precondition, BiConsumer step) { - ModelChecker.this.step((state, sut, entropySource) -> { - if (!precondition.test(state, entropySource)) + ModelChecker.this.step((state, sut, rng) -> { + if (!precondition.test(state, rng)) return Pair.unchanged(); - step.accept(state, entropySource); + step.accept(state, rng); return Pair.unchanged(); }); @@ -381,7 +381,7 @@ public class ModelChecker public Simple step(Predicate precondition, Consumer step) { ModelChecker.this.step((state, ignore) -> precondition.test(state), - (t, sut, entropySource) -> { + (t, sut, rng) -> { step.accept(t); return Pair.unchanged(); }); @@ -394,9 +394,15 @@ public class ModelChecker ModelChecker.this.run(); } - public void run(int minSteps, long maxSteps, EntropySource entropySource) throws Throwable + public void run(EntropySource rng) throws Throwable { - ModelChecker.this.run(minSteps, maxSteps, entropySource); + ModelChecker.this.run(0, Integer.MAX_VALUE, rng); + } + + public void run(int minSteps, long maxSteps, EntropySource rng) throws Throwable + { + ModelChecker.this.run(minSteps, maxSteps, rng); } } + } diff --git a/test/harry/main/org/apache/cassandra/harry/checker/TestHelper.java b/test/harry/main/org/apache/cassandra/harry/checker/TestHelper.java new file mode 100644 index 0000000000..f44b6e5fa2 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/checker/TestHelper.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.distributed.test.ExecUtil; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; + +public class TestHelper +{ + private static final Logger logger = LoggerFactory.getLogger(TestHelper.class); + + public static void withRandom(ModelChecker.ThrowingConsumer rng) + { + withRandom(System.nanoTime(), rng); + } + + public static void withRandom(long seed, ModelChecker.ThrowingConsumer rng) + { + try + { + logger.info("Seed: {}", seed); + rng.accept(new JdkRandomEntropySource(seed)); + } + catch (Throwable t) + { + throw new AssertionError(String.format("Caught an exception at seed:%dL", seed), t); + } + } + + public static void repeat(int num, ExecUtil.ThrowingSerializableRunnable r) + { + for (int i = 0; i < num; i++) + { + try + { + r.run(); + } + catch (Throwable throwable) + { + throw new AssertionError(throwable); + } + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/clock/ApproximateClock.java b/test/harry/main/org/apache/cassandra/harry/clock/ApproximateClock.java deleted file mode 100644 index 5bb4aa00ef..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/clock/ApproximateClock.java +++ /dev/null @@ -1,279 +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.harry.clock; - -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.LockSupport; - -import com.google.common.annotations.VisibleForTesting; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.model.OpSelectors; - -/** - * Monotonic clock, that guarantees that any LTS can be converted to a unique RTS, given - * the number of LTS does not exceed the number of RTS (e.g., we do not draw LTS more frequently - * than once per microsecond). - *

- * This conversion works as follows: - *

- * * every `period`, we record the current timestamp and maximum seen LTS, and keep history of up to - * `historySize` LTS/timestamp combinations - * * when queried to retrieve the LTS, we find a timestamp, relative to which we can calculate RTS. - * After that, we calculate a difference between the largest LTS that is still smaller than the converted - * one, and add this difference to the timestamp. - *

- * This way, later LTS can only be mapped to later RTS, and any LTS that was drawn previously, will be - * uniquely mapped relative to some timestamp, with the order matching the LTS order. - */ -public class ApproximateClock implements OpSelectors.Clock -{ - public static final long START_VALUE = 0; - public static final long DEFUNCT = Long.MIN_VALUE; - public static final long REBASE_IN_PROGRESS = Long.MIN_VALUE + 1; - - // TODO: there's a theoretical possibility of a bug; when we have several consecutive epochs without - // change in LTS, current implementation will return the latest epoch instead of the earliest one. - // This is not a big deal in terms of monotonicity but can cause some problems when validating TTL. - // The simples fix would be to find the smallest matching epoch. - private final ScheduledExecutorService executor; - private final int historySize; - private final CopyOnWriteArrayList ltsHistory; - private final long startTimeMicros; - private volatile int idx; - private final AtomicLong lts; - - private final long periodMicros; - - private final long epoch; - private final TimeUnit epochTimeUnit; - - public ApproximateClock(long period, TimeUnit timeUnit) - { - this(10000, period, timeUnit); - } - - public ApproximateClock(int historySize, long epoch, TimeUnit epochTimeUnit) - { - this(TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()), - historySize, new CopyOnWriteArrayList<>(), START_VALUE, 0, epoch, epochTimeUnit); - rebase(); - } - - ApproximateClock(long startTimeMicros, - int historySize, - CopyOnWriteArrayList history, - long lts, - int idx, - long epoch, - TimeUnit epochTimeUnit) - { - this.startTimeMicros = startTimeMicros; - this.historySize = historySize; - this.ltsHistory = history; - this.lts = new AtomicLong(lts); - this.idx = idx; - this.periodMicros = epochTimeUnit.toMicros(epoch); - this.executor = Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r); - t.setName("ApproximateMonotonicClock-ScheduledTasks"); - t.setDaemon(true); - return t; - }); - this.executor.scheduleAtFixedRate(this::rebase, epoch, epoch, epochTimeUnit); - this.epoch = epoch; - this.epochTimeUnit = epochTimeUnit; - } - - @VisibleForTesting - public static ApproximateClock forDebug(long startTimeMicros, int historySize, long lts, int idx, long period, TimeUnit timeUnit, long... values) - { - CopyOnWriteArrayList history = new CopyOnWriteArrayList<>(); - for (int i = 0; i < values.length; i++) - history.set(i, values[i]); - - assert values.length == idx; // sanity check - return new ApproximateClock(startTimeMicros, historySize, history, lts, idx, period, timeUnit); - } - - public long get(long idx) - { - return ltsHistory.get((int) (idx % historySize)); - } - - private void rebase() - { - int arrayIdx = idx % historySize; - long rebaseLts = lts.get(); - if (rebaseLts == DEFUNCT) - throw new IllegalStateException(); - - while (!lts.compareAndSet(rebaseLts, REBASE_IN_PROGRESS)) - rebaseLts = lts.get(); - - ltsHistory.add(arrayIdx, rebaseLts == START_VALUE ? START_VALUE : (rebaseLts + 1)); - - // If we happen to exhaust counter, we just need to make operations "wider". - // It is unsafe to proceed, so we defunct the clock. - // - // We could make a clock implementation that would sleep on `get`, but it will - // be more expensive, since we'll have to check for overflow each time before - // returning anything. - if (idx > 1 && get(idx) - get(idx - 1) > periodMicros) - { - lts.set(DEFUNCT); - executor.shutdown(); - throwCounterExhaustedException(); - } - - idx = idx + 1; - if (!lts.compareAndSet(REBASE_IN_PROGRESS, rebaseLts)) - throw new IllegalStateException("No thread should have changed LTS during rebase. " + lts.get()); - } - - @Override - public long nextLts() - { - long current = lts.get(); - while (true) - { - if (current >= 0) - { - if (lts.compareAndSet(current, current + 1)) - return current; - - current = lts.get(); - continue; - } - - if (current == REBASE_IN_PROGRESS) - { - LockSupport.parkNanos(1); - current = lts.get(); - continue; - } - - if (current == DEFUNCT) - throwCounterExhaustedException(); - - throw new IllegalStateException("This should have been unreachable: " + current); - } - } - - public long peek() - { - while (true) - { - long ret = lts.get(); - - if (ret == REBASE_IN_PROGRESS) - { - LockSupport.parkNanos(1); - continue; - } - - if (ret == DEFUNCT) - throwCounterExhaustedException(); - - return ret; - } - } - - public Configuration.ClockConfiguration toConfig() - { - int idx = this.idx; - long[] history = new long[Math.min(idx, historySize)]; - for (int i = 0; i < history.length; i++) - history[i] = ltsHistory.get(i); - return new Configuration.DebugApproximateClockConfiguration(startTimeMicros, - ltsHistory.size(), - history, - lts.get(), - idx, - epoch, - epochTimeUnit); - } - - public long lts(final long rts) - { - final int historyIdx = idx - 1; - for (int i = 0; i < historySize - 1 && historyIdx - i >= 0; i++) - { - long periodStartRts = startTimeMicros + periodMicros * (historyIdx - i); - if (rts >= periodStartRts) - { - long periodStartLts = get(historyIdx - i); - return periodStartLts + rts - periodStartRts; - } - } - throw new IllegalStateException("RTS is too old to convert to LTS: " + rts + "\n " + ltsHistory); - } - - public long rts(final long lts) - { - assert lts <= peek() : String.format("Queried for LTS we haven't yet issued %d. Max is %d.", lts, peek()); - - final int historyIdx = idx - 1; - for (int i = 0; i < historySize - 1 && historyIdx - i >= 0; i++) - { - long periodStartLts = get(historyIdx - i); - if (lts >= periodStartLts) - { - long periodStartRts = startTimeMicros + periodMicros * (historyIdx - i); - return periodStartRts + lts - periodStartLts; - } - } - - throw new IllegalStateException("LTS is too old to convert to RTS: " + lts + "\n " + dumpHistory()); - } - - private String dumpHistory() - { - String s = ""; - int idx = this.idx; - for (int i = 0; i < Math.min(idx, historySize); i++) - { - s += ltsHistory.get(i) + ","; - } - return s.substring(0, Math.max(0, s.length() - 1)); - } - - public String toString() - { - return String.format("withDebugClock(%dL,\n\t%d,\n\t%d,\n\t%d,\n\t%d,\n\t%s,\n\t%s)", - startTimeMicros, - historySize, - lts.get(), - idx, - epoch, - epochTimeUnit, - dumpHistory()); - } - - private void throwCounterExhaustedException() - { - long diff = get(idx) - get(idx - 1); - throw new RuntimeException(String.format("Counter was exhausted. Drawn %d out of %d lts during the period.", - diff, periodMicros)); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/clock/OffsetClock.java b/test/harry/main/org/apache/cassandra/harry/clock/OffsetClock.java deleted file mode 100644 index d12948cf1e..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/clock/OffsetClock.java +++ /dev/null @@ -1,94 +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.harry.clock; - -import java.util.concurrent.atomic.AtomicLong; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.model.OpSelectors; - -/** - * A trivial implemementation of the clock, one that does not attempt to follow the wall clock. - * This clock simply offsets LTS by a preset value. - */ -public class OffsetClock implements OpSelectors.Clock -{ - final AtomicLong lts; - - private final long base; - - public OffsetClock(long base) - { - this(ApproximateClock.START_VALUE, base); - } - - public OffsetClock(long startValue, long base) - { - this.lts = new AtomicLong(startValue); - this.base = base; - } - - public long rts(long lts) - { - return base + lts; - } - - public long lts(long rts) - { - return rts - base; - } - - public long nextLts() - { - return lts.getAndIncrement(); - } - - public long peek() - { - return lts.get(); - } - - public Configuration.ClockConfiguration toConfig() - { - return new OffsetClockConfiguration(lts.get(), base); - } - - @JsonTypeName("offset") - public static class OffsetClockConfiguration implements Configuration.ClockConfiguration - { - public final long offset; - public final long base; - - @JsonCreator - public OffsetClockConfiguration(@JsonProperty("offset") long offset, - @JsonProperty(value = "base", defaultValue = "0") long base) - { - this.offset = offset; - this.base = base; - } - - public OpSelectors.Clock make() - { - return new OffsetClock(base, offset); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/core/Configuration.java b/test/harry/main/org/apache/cassandra/harry/core/Configuration.java deleted file mode 100644 index 47e1d3bb5d..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/core/Configuration.java +++ /dev/null @@ -1,1255 +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.harry.core; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.lang.annotation.Annotation; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.gen.distribution.Distribution; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.model.NoOpChecker; -import org.apache.cassandra.harry.model.QuiescentChecker; -import org.apache.cassandra.harry.clock.ApproximateClock; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.tracker.LockingDataTracker; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.tracker.DefaultDataTracker; -import org.apache.cassandra.harry.runner.Runner; -import org.apache.cassandra.harry.util.BitSet; -import org.apache.cassandra.harry.visitors.AllPartitionsValidator; -import org.apache.cassandra.harry.visitors.CorruptingVisitor; -import org.apache.cassandra.harry.visitors.LoggingVisitor; -import org.apache.cassandra.harry.visitors.MutatingRowVisitor; -import org.apache.cassandra.harry.visitors.MutatingVisitor; -import org.apache.cassandra.harry.visitors.OperationExecutor; -import org.apache.cassandra.harry.visitors.QueryLogger; -import org.apache.cassandra.harry.visitors.RandomValidator; -import org.apache.cassandra.harry.visitors.RecentValidator; -import org.apache.cassandra.harry.visitors.Visitor; -import org.reflections.Reflections; -import org.reflections.scanners.Scanners; -import org.reflections.util.NameHelper; - -public class Configuration -{ - private enum NameUtils implements NameHelper - { - INSTANCE; - } - - private static final ObjectMapper mapper; - - private static Set> findClassesMarkedWith(Class annotation) - { - Reflections reflections = new Reflections(org.reflections.util.ConfigurationBuilder.build("harry").setExpandSuperTypes(false)); - Collection> classes = NameUtils.INSTANCE.forNames(reflections.get(Scanners.TypesAnnotated.get(annotation.getName())), - reflections.getConfiguration().getClassLoaders()); - return new HashSet<>(classes); - } - - static - { - mapper = new ObjectMapper(new YAMLFactory() // checkstyle: permit this instantiation - .disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID) - .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) - .disable(YAMLGenerator.Feature.CANONICAL_OUTPUT) - .enable(YAMLGenerator.Feature.INDENT_ARRAYS)); - - findClassesMarkedWith(JsonTypeName.class) - .forEach(mapper::registerSubtypes); - } - - public final long seed; - public final SchemaProviderConfiguration schema_provider; - - public final boolean drop_schema; - public final String keyspace_ddl; - public final boolean create_schema; - public final boolean truncate_table; - - public final MetricReporterConfiguration metric_reporter; - public final ClockConfiguration clock; - public final SutConfiguration system_under_test; - public final DataTrackerConfiguration data_tracker; - public final RunnerConfiguration runner; - public final PDSelectorConfiguration partition_descriptor_selector; - public final CDSelectorConfiguration clustering_descriptor_selector; - - @JsonCreator - public Configuration(@JsonProperty("seed") long seed, - @JsonProperty("schema_provider") SchemaProviderConfiguration schema_provider, - @JsonProperty("drop_schema") boolean drop_schema, - @JsonProperty("create_keyspace") String keyspace_ddl, - @JsonProperty("create_schema") boolean create_schema, - @JsonProperty("truncate_schema") boolean truncate_table, - @JsonProperty("metric_reporter") MetricReporterConfiguration metric_reporter, - @JsonProperty("clock") ClockConfiguration clock, - @JsonProperty("runner") RunnerConfiguration runner, - @JsonProperty("system_under_test") SutConfiguration system_under_test, - @JsonProperty("data_tracker") DataTrackerConfiguration data_tracker, - @JsonProperty("partition_descriptor_selector") PDSelectorConfiguration partition_descriptor_selector, - @JsonProperty("clustering_descriptor_selector") CDSelectorConfiguration clustering_descriptor_selector) - { - this.seed = seed; - this.schema_provider = schema_provider; - this.keyspace_ddl = keyspace_ddl; - this.drop_schema = drop_schema; - this.create_schema = create_schema; - this.truncate_table = truncate_table; - this.metric_reporter = metric_reporter; - this.clock = clock; - this.system_under_test = system_under_test; - this.data_tracker = data_tracker; - this.partition_descriptor_selector = partition_descriptor_selector; - this.clustering_descriptor_selector = clustering_descriptor_selector; - this.runner = runner; - } - - public static void registerSubtypes(Class... classes) - { - mapper.registerSubtypes(classes); - } - - public static void toFile(File file, Configuration config) - { - try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) - { - bw.write(Configuration.toYamlString(config)); - bw.flush(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - public static String toYamlString(Configuration config) - { - try - { - return mapper.writeValueAsString(config); - } - catch (Throwable t) - { - throw new RuntimeException(t); - } - } - - public static Configuration fromYamlString(String config) - { - try - { - return mapper.readValue(config, Configuration.class); - } - catch (Throwable t) - { - throw new RuntimeException(t); - } - } - - public static Configuration fromFile(String path) - { - return fromFile(new File(path)); - } - - public static Configuration fromFile(File file) - { - try - { - return mapper.readValue(file, Configuration.class); - } - catch (Throwable t) - { - throw new RuntimeException(t); - } - } - - public static void validate(Configuration config) - { - Objects.requireNonNull(config.schema_provider, "Schema provider should not be null"); - Objects.requireNonNull(config.metric_reporter, "Metric reporter should not be null"); - Objects.requireNonNull(config.clock, "Clock should not be null"); - Objects.requireNonNull(config.system_under_test, "System under test should not be null"); - Objects.requireNonNull(config.partition_descriptor_selector, "Partition descriptor selector should not be null"); - Objects.requireNonNull(config.clustering_descriptor_selector, "Clustering descriptor selector should not be null"); - } - - public Runner createRunner() - { - return createRunner(this); - } - - public Run createRun() - { - return createRun(this); - } - - public static Run createRun(Configuration snapshot) - { - SystemUnderTest sut = null; - try - { - validate(snapshot); - - long seed = snapshot.seed; - - OpSelectors.PureRng rng = new OpSelectors.PCGFast(seed); - - // TODO: validate that operation kind is compatible with schema, due to statics etc - sut = snapshot.system_under_test.make(); - SchemaSpec schemaSpec = snapshot.schema_provider.make(seed, sut); - schemaSpec.validate(); - - OpSelectors.PdSelector pdSelector = snapshot.partition_descriptor_selector.make(rng); - DataTrackerConfiguration dataTrackerConfiguration = snapshot.data_tracker == null ? new DefaultDataTrackerConfiguration() : snapshot.data_tracker; - DataTracker tracker = dataTrackerConfiguration.make(pdSelector, schemaSpec); - - OpSelectors.DescriptorSelector descriptorSelector = snapshot.clustering_descriptor_selector.make(rng, schemaSpec); - OpSelectors.Clock clock = snapshot.clock.make(); - - MetricReporter metricReporter = snapshot.metric_reporter.make(); - - return new Run(rng, - clock, - pdSelector, - descriptorSelector, - schemaSpec, - tracker, - sut, - metricReporter); - } - catch (Throwable t) - { - // Make sure to shut down all SUT threads if it has been started - if (sut != null) - { - sut.shutdown(); - } - throw t; - } - } - - public static Runner createRunner(Configuration config) - { - Run run = createRun(config); - return config.runner.make(run, config); - } - - public static class ConfigurationBuilder - { - long seed; - SchemaProviderConfiguration schema_provider = new DefaultSchemaProviderConfiguration(); - - String keyspace_ddl; - boolean drop_schema; - boolean create_schema; - boolean truncate_table; - - ClockConfiguration clock; - MetricReporterConfiguration metric_reporter = new NoOpMetricReporterConfiguration(); - DataTrackerConfiguration data_tracker = new DefaultDataTrackerConfiguration(); - RunnerConfiguration runner; - SutConfiguration system_under_test; - PDSelectorConfiguration partition_descriptor_selector = new Configuration.DefaultPDSelectorConfiguration(10, 100); - CDSelectorConfiguration clustering_descriptor_selector; // TODO: sensible default value - - public ConfigurationBuilder setSeed(long seed) - { - this.seed = seed; - return this; - } - - public ConfigurationBuilder setSchemaProvider(SchemaProviderConfiguration schema_provider) - { - this.schema_provider = schema_provider; - return this; - } - - public ConfigurationBuilder setDataTracker(DataTrackerConfiguration tracker) - { - this.data_tracker = tracker; - return this; - } - - public ConfigurationBuilder setKeyspaceDdl(String keyspace_ddl) - { - this.keyspace_ddl = keyspace_ddl; - return this; - } - - - public ConfigurationBuilder setClock(ClockConfiguration clock) - { - this.clock = clock; - return this; - } - - public ConfigurationBuilder setSUT(SutConfiguration system_under_test) - { - this.system_under_test = system_under_test; - return this; - } - - public ConfigurationBuilder setDropSchema(boolean drop_schema) - { - this.drop_schema = drop_schema; - return this; - } - - public ConfigurationBuilder setCreateSchema(boolean create_schema) - { - this.create_schema = create_schema; - return this; - } - - public ConfigurationBuilder setTruncateTable(boolean truncate_table) - { - this.truncate_table = truncate_table; - return this; - } - - public ConfigurationBuilder setRunner(RunnerConfiguration runner) - { - this.runner = runner; - return this; - } - - public ConfigurationBuilder setPartitionDescriptorSelector(PDSelectorConfiguration partition_descriptor_selector) - { - this.partition_descriptor_selector = partition_descriptor_selector; - return this; - } - - public ConfigurationBuilder setClusteringDescriptorSelector(CDSelectorConfiguration builder) - { - this.clustering_descriptor_selector = builder; - return this; - } - - public ConfigurationBuilder setClusteringDescriptorSelector(Consumer build) - { - CDSelectorConfigurationBuilder builder = new CDSelectorConfigurationBuilder(); - build.accept(builder); - return setClusteringDescriptorSelector(builder.build()); - } - - public ConfigurationBuilder setMetricReporter(MetricReporterConfiguration metric_reporter) - { - this.metric_reporter = metric_reporter; - return this; - } - - public Configuration build() - { - return new Configuration(seed, - schema_provider, - drop_schema, - keyspace_ddl, - create_schema, - truncate_table, - metric_reporter, - clock, - runner, - system_under_test, - data_tracker, - partition_descriptor_selector, - clustering_descriptor_selector); - } - } - - public ConfigurationBuilder unbuild() - { - ConfigurationBuilder builder = new ConfigurationBuilder(); - builder.seed = seed; - - builder.schema_provider = schema_provider; - builder.keyspace_ddl = keyspace_ddl; - builder.drop_schema = drop_schema; - builder.create_schema = create_schema; - builder.truncate_table = truncate_table; - - builder.data_tracker = data_tracker; - builder.clock = clock; - builder.runner = runner; - builder.system_under_test = system_under_test; - builder.metric_reporter = metric_reporter; - - builder.partition_descriptor_selector = partition_descriptor_selector; - builder.clustering_descriptor_selector = clustering_descriptor_selector; - - return builder; - } - - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface DataTrackerConfiguration extends DataTracker.DataTrackerFactory - { - - } - - @JsonTypeName("no_op") - public static class NoOpDataTrackerConfiguration implements DataTrackerConfiguration - { - @JsonCreator - public NoOpDataTrackerConfiguration() - { - } - - public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) - { - return DataTracker.NO_OP; - } - } - - @JsonTypeName("default") - public static class DefaultDataTrackerConfiguration implements DataTrackerConfiguration - { - public final long max_seen_lts; - public final long max_complete_lts; - public final List reorder_buffer; - - public DefaultDataTrackerConfiguration() - { - this(-1, -1, null); - } - - @JsonCreator - public DefaultDataTrackerConfiguration(@JsonProperty(value = "max_seen_lts", defaultValue = "-1") long max_seen_lts, - @JsonProperty(value = "max_complete_lts", defaultValue = "-1") long max_complete_lts, - @JsonProperty(value = "reorder_buffer", defaultValue = "null") List reorder_buffer) - { - this.max_seen_lts = max_seen_lts; - this.max_complete_lts = max_complete_lts; - this.reorder_buffer = reorder_buffer; - } - - @Override - public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) - { - DefaultDataTracker tracker = new DefaultDataTracker(); - tracker.forceLts(max_seen_lts, max_complete_lts, reorder_buffer); - return tracker; - } - } - - @JsonTypeName("locking") - public static class LockingDataTrackerConfiguration implements DataTrackerConfiguration - { - public final long max_seen_lts; - public final long max_complete_lts; - public final List reorder_buffer; - - @JsonCreator - public LockingDataTrackerConfiguration(@JsonProperty(value = "max_seen_lts", defaultValue = "-1") long max_seen_lts, - @JsonProperty(value = "max_complete_lts", defaultValue = "-1") long max_complete_lts, - @JsonProperty(value = "reorder_buffer", defaultValue = "null") List reorder_buffer) - { - this.max_seen_lts = max_seen_lts; - this.max_complete_lts = max_complete_lts; - this.reorder_buffer = reorder_buffer; - } - - @Override - public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) - { - LockingDataTracker tracker = new LockingDataTracker(pdSelector, schemaSpec); - tracker.forceLts(max_seen_lts, max_complete_lts, reorder_buffer); - return tracker; - } - } - - public static class DefaultLockingDataTrackerConfiguration implements DataTrackerConfiguration - { - @Override - public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) - { - LockingDataTracker tracker = new LockingDataTracker(pdSelector, schemaSpec); - tracker.forceLts(-1, -1, null); - return tracker; - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface ClockConfiguration extends OpSelectors.ClockFactory - { - } - - @JsonTypeName("approximate_monotonic") - public static class ApproximateClockConfiguration implements ClockConfiguration - { - public final int history_size; - public final int epoch_length; - public final TimeUnit epoch_time_unit; - - @JsonCreator - public ApproximateClockConfiguration(@JsonProperty("history_size") int history_size, - @JsonProperty("epoch_length") int epoch_length, - @JsonProperty("epoch_time_unit") TimeUnit epoch_time_unit) - { - this.history_size = history_size; - this.epoch_length = epoch_length; - this.epoch_time_unit = epoch_time_unit; - } - - public OpSelectors.Clock make() - { - return new ApproximateClock(history_size, - epoch_length, - epoch_time_unit); - } - } - - @JsonTypeName("debug_approximate_monotonic") - public static class DebugApproximateClockConfiguration implements ClockConfiguration - { - public final long start_time_micros; - public final int history_size; - public final long[] history; - public final long lts; - public final int idx; - public final long epoch_period; - public final TimeUnit epoch_time_unit; - - @JsonCreator - public DebugApproximateClockConfiguration(@JsonProperty("start_time_micros") long start_time_micros, - @JsonProperty("history_size") int history_size, - @JsonProperty("history") long[] history, - @JsonProperty("lts") long lts, - @JsonProperty("idx") int idx, - @JsonProperty("epoch_period") long epoch_period, - @JsonProperty("epoch_time_unit") TimeUnit epoch_time_unit) - { - this.start_time_micros = start_time_micros; - this.history_size = history_size; - this.history = history; - this.lts = lts; - this.idx = idx; - this.epoch_period = epoch_period; - this.epoch_time_unit = epoch_time_unit; - } - - public OpSelectors.Clock make() - { - return ApproximateClock.forDebug(start_time_micros, - history_size, - lts, - idx, - epoch_period, - epoch_time_unit, - history); - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface RunnerConfiguration extends Runner.RunnerFactory - { - } - - @JsonTypeName("concurrent") - public static class ConcurrentRunnerConfig implements RunnerConfiguration - { - @JsonProperty(value = "visitor_pools") - public final List visitor_pools; - - public final long run_time; - public final TimeUnit run_time_unit; - - @JsonCreator - public ConcurrentRunnerConfig(@JsonProperty(value = "visitor_pools") List visitor_pools, - @JsonProperty(value = "run_time", defaultValue = "2") long runtime, - @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) - { - this.visitor_pools = visitor_pools; - this.run_time = runtime; - this.run_time_unit = runtimeUnit; - } - - @Override - public Runner make(Run run, Configuration config) - { - return new Runner.ConcurrentRunner(run, config, visitor_pools, run_time, run_time_unit); - } - } - - public static class VisitorPoolConfiguration - { - public final String prefix; - public final int concurrency; - public final VisitorConfiguration visitor; - - @JsonCreator - public VisitorPoolConfiguration(@JsonProperty(value = "prefix") String prefix, - @JsonProperty(value = "concurrency") int concurrency, - @JsonProperty(value = "visitor") VisitorConfiguration visitor) - { - this.prefix = prefix; - this.concurrency = concurrency; - this.visitor = visitor; - } - - public static VisitorPoolConfiguration pool(String prefix, int concurrency, VisitorConfiguration visitor) - { - return new VisitorPoolConfiguration(prefix, concurrency, visitor); - } - } - - @JsonTypeName("sequential") - public static class SequentialRunnerConfig implements RunnerConfiguration - { - @JsonProperty(value = "visitors") - public final List visitorFactories; - - public final long run_time; - public final TimeUnit run_time_unit; - - @JsonCreator - public SequentialRunnerConfig(@JsonProperty(value = "visitors") List visitors, - @JsonProperty(value = "run_time", defaultValue = "2") long runtime, - @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) - { - this.visitorFactories = visitors; - this.run_time = runtime; - this.run_time_unit = runtimeUnit; - } - - @Override - public Runner make(Run run, Configuration config) - { - return new Runner.SequentialRunner(run, config, visitorFactories, run_time, run_time_unit); - } - } - - @JsonTypeName("single") - public static class SingleVisitRunnerConfig implements RunnerConfiguration - { - @JsonProperty(value = "visitors") - public final List visitorFactories; - - @JsonCreator - public SingleVisitRunnerConfig(@JsonProperty(value = "visitors") List visitors) - { - this.visitorFactories = visitors; - } - - @Override - public Runner make(Run run, Configuration config) - { - return new Runner.SingleVisitRunner(run, config, visitorFactories); - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface SutConfiguration extends SystemUnderTest.SUTFactory - { - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface ModelConfiguration extends Model.ModelFactory - { - } - - @JsonTypeName("quiescent_checker") - public static class QuiescentCheckerConfig implements ModelConfiguration - { - @JsonCreator - public QuiescentCheckerConfig() - { - } - - public Model make(Run run) - { - return new QuiescentChecker(run); - } - } - - @JsonTypeName("no_op") - public static class NoOpCheckerConfig implements ModelConfiguration - { - @JsonCreator - public NoOpCheckerConfig() - { - } - - public Model make(Run run) - { - return new NoOpChecker(run); - } - } - - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface PDSelectorConfiguration extends OpSelectors.PdSelectorFactory - { - } - - @JsonTypeName("default") - public static class DefaultPDSelectorConfiguration implements PDSelectorConfiguration - { - public final int window_size; - public final int slide_after_repeats; - public final long position_offset; - public final long position_window_size; - - public DefaultPDSelectorConfiguration(int window_size, - int slide_after_repeats) - { - this.window_size = window_size; - this.slide_after_repeats = slide_after_repeats; - this.position_offset = 0L; - this.position_window_size = Long.MAX_VALUE; - } - - @JsonCreator - public DefaultPDSelectorConfiguration(@JsonProperty(value = "window_size", defaultValue = "10") int window_size, - @JsonProperty(value = "slide_after_repeats", defaultValue = "100") int slide_after_repeats, - @JsonProperty(value = "runner_index") Long runner_index, - @JsonProperty(value = "total_runners") Long total_runners, - @JsonProperty(value = "position_offset") Long position_offset, - @JsonProperty(value = "position_window_size") Long position_window_size) - { - this.window_size = window_size; - this.slide_after_repeats = slide_after_repeats; - if (runner_index != null || total_runners != null) - { - assert runner_index != null && total_runners != null : "Both runner_index and total_runners are required"; - assert position_offset == null && position_window_size == null : "Please use either runner_index/total_runners or position_offset/position_window_size combinations."; - this.position_window_size = Long.MAX_VALUE / total_runners; - this.position_offset = this.position_window_size * runner_index; - } - else - { - assert runner_index == null && total_runners == null : "Please use either runner_index/total_runners or position_offset/position_window_size combinations."; - this.position_offset = position_offset == null ? 0 : position_offset; - if (position_window_size == null) - this.position_window_size = Long.MAX_VALUE - this.position_offset; - else - this.position_window_size = position_window_size; - } - } - - public OpSelectors.PdSelector make(OpSelectors.PureRng rng) - { - return new OpSelectors.DefaultPdSelector(rng, window_size, slide_after_repeats, position_offset, position_window_size); - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface CDSelectorConfiguration extends OpSelectors.DescriptorSelectorFactory - { - } - - public static class WeightedSelectorBuilder - { - private final Map operation_kind_weights; - - public WeightedSelectorBuilder() - { - operation_kind_weights = new HashMap<>(); - } - - public WeightedSelectorBuilder addWeight(T v, int weight) - { - operation_kind_weights.put(v, weight); - return this; - } - - public Map build() - { - return operation_kind_weights; - } - } - - public static class OperationKindSelectorBuilder extends WeightedSelectorBuilder - { - } - - // TODO: configure fractions/fractional builder - public static class CDSelectorConfigurationBuilder - { - private DistributionConfig operations_per_lts = new ConstantDistributionConfig(10); - private int max_partition_size = 100; - private Map operation_kind_weights = new OperationKindSelectorBuilder() - .addWeight(OpSelectors.OperationKind.DELETE_ROW, 1) - .addWeight(OpSelectors.OperationKind.DELETE_COLUMN, 1) - .addWeight(OpSelectors.OperationKind.INSERT, 98) - .build(); - private Map column_mask_bitsets; - private int[] fractions; - - public CDSelectorConfigurationBuilder setOperationsPerLtsDistribution(DistributionConfig operations_per_lts) - { - this.operations_per_lts = operations_per_lts; - return this; - } - - public CDSelectorConfigurationBuilder setMaxPartitionSize(int max_partition_size) - { - if (max_partition_size <= 0) - throw new IllegalArgumentException("Max partition size should be positive"); - this.max_partition_size = max_partition_size; - return this; - } - - public CDSelectorConfigurationBuilder setOperationKindWeights(Map operation_kind_weights) - { - this.operation_kind_weights = operation_kind_weights; - return this; - } - - public CDSelectorConfigurationBuilder setColumnMasks(Map column_mask_bitsets) - { - this.column_mask_bitsets = column_mask_bitsets; - return this; - } - - public CDSelectorConfigurationBuilder setFractions(int[] fractions) - { - this.fractions = fractions; - return this; - } - - public DefaultCDSelectorConfiguration build() - { - if (fractions == null) - { - return new DefaultCDSelectorConfiguration(operations_per_lts, - max_partition_size, - operation_kind_weights, - column_mask_bitsets); - } - else - { - return new HierarchicalCDSelectorConfiguration(operations_per_lts, - max_partition_size, - operation_kind_weights, - column_mask_bitsets, - fractions); - } - } - } - - @JsonTypeName("default") - public static class DefaultCDSelectorConfiguration implements CDSelectorConfiguration - { - public final DistributionConfig operations_per_lts; - public final int max_partition_size; - public final Map operation_kind_weights; - public final Map column_mask_bitsets; - - @JsonCreator - public DefaultCDSelectorConfiguration(@JsonProperty("operations_per_lts") DistributionConfig operations_per_lts, - @JsonProperty(value = "window_size", defaultValue = "100") int max_partition_size, - @JsonProperty("operation_kind_weights") Map operation_kind_weights, - @JsonProperty("column_mask_bitsets") Map column_mask_bitsets) - { - this.operations_per_lts = operations_per_lts; - this.max_partition_size = max_partition_size; - this.operation_kind_weights = operation_kind_weights; - this.column_mask_bitsets = column_mask_bitsets; - } - - protected OpSelectors.ColumnSelector columnSelector(SchemaSpec schemaSpec) - { - OpSelectors.ColumnSelector columnSelector; - if (column_mask_bitsets == null) - { - columnSelector = OpSelectors.columnSelectorBuilder().forAll(schemaSpec).build(); - } - else - { - Map> m = new EnumMap<>(OpSelectors.OperationKind.class); - for (Map.Entry entry : column_mask_bitsets.entrySet()) - { - List bitSets = new ArrayList<>(entry.getValue().length); - for (long raw_bitset : entry.getValue()) - { - bitSets.add(BitSet.create(raw_bitset, schemaSpec.allColumns.size())); - } - Surjections.Surjection selector = Surjections.pick(bitSets); - m.put(entry.getKey(), selector); - } - columnSelector = (opKind, descr) -> m.get(opKind).inflate(descr); - } - - return columnSelector; - } - - public OpSelectors.DescriptorSelector make(OpSelectors.PureRng rng, SchemaSpec schemaSpec) - { - return new OpSelectors.DefaultDescriptorSelector(rng, - columnSelector(schemaSpec), - OpSelectors.OperationSelector.weighted(operation_kind_weights), - operations_per_lts.make(), - max_partition_size); - } - } - - public static class HierarchicalCDSelectorConfiguration extends DefaultCDSelectorConfiguration - { - private final int[] fractions; - - public HierarchicalCDSelectorConfiguration(DistributionConfig operations_per_lts, - int max_partition_size, - Map operation_kind_weights, - Map column_mask_bitsets, - int[] fractions) - { - super(operations_per_lts, max_partition_size, operation_kind_weights, column_mask_bitsets); - this.fractions = fractions; - } - - public OpSelectors.DescriptorSelector make(OpSelectors.PureRng rng, SchemaSpec schemaSpec) - { - return new OpSelectors.HierarchicalDescriptorSelector(rng, - fractions, - columnSelector(schemaSpec), - OpSelectors.OperationSelector.weighted(operation_kind_weights), - operations_per_lts.make(), - max_partition_size); - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - public interface DistributionConfig extends Distribution.DistributionFactory - { - } - - @JsonTypeName("identity") - public static class IdentityDistributionConfig implements DistributionConfig - { - @JsonCreator - public IdentityDistributionConfig() - { - } - - public Distribution make() - { - return new Distribution.IdentityDistribution(); - } - } - - @JsonTypeName("normal") - public static class NormalDistributionConfig implements DistributionConfig - { - @JsonCreator - public NormalDistributionConfig() - { - } - - public Distribution make() - { - return new Distribution.NormalDistribution(); - } - } - - @JsonTypeName("constant") - public static class ConstantDistributionConfig implements DistributionConfig - { - public final long constant; - - @JsonCreator - public ConstantDistributionConfig(@JsonProperty("constant") long constant) - { - this.constant = constant; - } - - public Distribution make() - { - return new Distribution.ConstantDistribution(constant); - } - } - - @JsonTypeName("scaled") - public static class ScaledDistributionConfig implements DistributionConfig - { - private final long min; - private final long max; - - @JsonCreator - public ScaledDistributionConfig(long min, long max) - { - this.min = min; - this.max = max; - } - - public Distribution make() - { - return new Distribution.ScaledDistribution(min, max); - } - } - - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface VisitorConfiguration extends Visitor.VisitorFactory - { - } - - - @JsonTypeName("mutating") - public static class MutatingVisitorConfiguation implements VisitorConfiguration - { - public final RowVisitorConfiguration row_visitor; - - @JsonCreator - public MutatingVisitorConfiguation(@JsonProperty("row_visitor") RowVisitorConfiguration row_visitor) - { - this.row_visitor = row_visitor; - } - - @Override - public Visitor make(Run run) - { - return new MutatingVisitor(run, row_visitor::make); - } - } - - @JsonTypeName("logging") - public static class LoggingVisitorConfiguration implements VisitorConfiguration - { - protected final RowVisitorConfiguration row_visitor; - - @JsonCreator - public LoggingVisitorConfiguration(@JsonProperty("row_visitor") RowVisitorConfiguration row_visitor) - { - this.row_visitor = row_visitor; - } - - @Override - public Visitor make(Run run) - { - return new LoggingVisitor(run, row_visitor::make); - } - } - - @JsonTypeName("validate_all_partitions") - public static class AllPartitionsValidatorConfiguration implements VisitorConfiguration - { - public final int concurrency; - public final QueryLoggerConfiguration query_logger; - public final Configuration.ModelConfiguration model; - - @JsonCreator - public AllPartitionsValidatorConfiguration(@JsonProperty("concurrency") int concurrency, - @JsonProperty("model") Configuration.ModelConfiguration model, - @JsonProperty("query_logger") QueryLoggerConfiguration query_logger) - { - this.concurrency = concurrency; - this.model = model; - this.query_logger = QueryLogger.thisOrDefault(query_logger); - } - - public Visitor make(Run run) - { - return new AllPartitionsValidator(run, concurrency, model, query_logger.make()); - } - } - - @JsonTypeName("corrupt") - public static class CorruptingVisitorConfiguration implements VisitorConfiguration - { - public final int trigger_after; - - @JsonCreator - public CorruptingVisitorConfiguration(@JsonProperty("trigger_after") int trigger_after) - { - this.trigger_after = trigger_after; - } - - public Visitor make(Run run) - { - return new CorruptingVisitor(trigger_after, run); - } - } - - @JsonTypeName("validate_recent_partitions") - public static class RecentPartitionsValidatorConfiguration implements VisitorConfiguration - { - public final int partition_count; - public final int queries; - public final Configuration.ModelConfiguration modelConfiguration; - public final QueryLoggerConfiguration query_logger; - - // TODO: make query selector configurable - @JsonCreator - public RecentPartitionsValidatorConfiguration(@JsonProperty("partition_count") int partition_count, - @JsonProperty("queries_per_partition") int queries, - @JsonProperty("model") Configuration.ModelConfiguration model, - @JsonProperty("logger") QueryLoggerConfiguration query_logger) - { - this.partition_count = partition_count; - this.queries = queries; - this.modelConfiguration = model; - this.query_logger = QueryLogger.thisOrDefault(query_logger); - } - - @Override - public Visitor make(Run run) - { - return new RecentValidator(partition_count, queries, run, modelConfiguration, query_logger.make()); - } - } - - @JsonTypeName("validate_random_partitions") - public static class RandomPartitionValidatorConfiguration implements VisitorConfiguration - { - public final int partition_count; - public final int queries; - public final Configuration.ModelConfiguration model_configuration; - public final QueryLoggerConfiguration query_logger; - - // TODO: make query selector configurable - @JsonCreator - public RandomPartitionValidatorConfiguration(@JsonProperty("partition_count") int partition_count, - @JsonProperty("queries_per_partition") int queries, - @JsonProperty("model") Configuration.ModelConfiguration model, - @JsonProperty("logger") QueryLoggerConfiguration query_logger) - { - this.partition_count = partition_count; - this.queries = queries; - this.model_configuration = model; - this.query_logger = QueryLogger.thisOrDefault(query_logger); - } - - @Override - public Visitor make(Run run) - { - return new RandomValidator(partition_count, queries, run, model_configuration, query_logger.make()); - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface QueryLoggerConfiguration extends QueryLogger.QueryLoggerFactory - { - } - - @JsonTypeName("no_op") - public static class NoOpQueryLoggerConfiguration implements QueryLoggerConfiguration - { - public QueryLogger make() - { - return QueryLogger.NO_OP; - } - } - - @JsonTypeName("file") - public static class FileQueryLoggerConfiguration implements QueryLoggerConfiguration - { - public final String filename; - - @JsonCreator - public FileQueryLoggerConfiguration(@JsonProperty("filename") String filename) - { - this.filename = filename; - } - - public QueryLogger make() - { - return new QueryLogger.FileQueryLogger(filename); - } - } - - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface RowVisitorConfiguration extends OperationExecutor.RowVisitorFactory - { - } - - @JsonTypeName("mutating") - public static class MutatingRowVisitorConfiguration implements RowVisitorConfiguration - { - @Override - public OperationExecutor make(Run run) - { - return new MutatingRowVisitor(run); - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface SchemaProviderConfiguration extends SchemaSpec.SchemaSpecFactory - { - } - - @JsonTypeName("default") - public static class DefaultSchemaProviderConfiguration implements SchemaProviderConfiguration - { - public SchemaSpec make(long seed, SystemUnderTest sut) - { - return SchemaGenerators.defaultSchemaSpecGen("table0") - .inflate(seed); - } - } - - @JsonTypeName("fixed") - public static class FixedSchemaProviderConfiguration implements SchemaProviderConfiguration - { - private final SchemaSpec schemaSpec; - - @JsonCreator - public FixedSchemaProviderConfiguration(@JsonProperty("keyspace") String keyspace, - @JsonProperty("table") String table, - @JsonProperty("partition_keys") Map pks, - @JsonProperty("clustering_keys") Map cks, - @JsonProperty("regular_columns") Map regulars, - @JsonProperty("static_columns") Map statics) - { - this(SchemaGenerators.parse(keyspace, table, - pks, cks, regulars, statics)); - } - - public FixedSchemaProviderConfiguration(SchemaSpec schemaSpec) - { - this.schemaSpec = schemaSpec; - } - public SchemaSpec make(long seed, SystemUnderTest sut) - { - return schemaSpec; - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) - public interface MetricReporterConfiguration extends MetricReporter.MetricReporterFactory - { - } - - @JsonTypeName("no_op") - public static class NoOpMetricReporterConfiguration implements MetricReporterConfiguration - { - public MetricReporter make() - { - return MetricReporter.NO_OP; - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/core/MetricReporter.java b/test/harry/main/org/apache/cassandra/harry/core/MetricReporter.java deleted file mode 100644 index 4034eb97c5..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/core/MetricReporter.java +++ /dev/null @@ -1,51 +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.harry.core; - -public interface MetricReporter -{ - void columnDelete(); - void rowDelete(); - void partitionDelete(); - void insert(); - void rangeDelete(); - - void validatePartition(); - void validateRandomQuery(); - - interface MetricReporterFactory - { - MetricReporter make(); - } - - MetricReporter NO_OP = new NoOpMetricReporter(); - - class NoOpMetricReporter implements MetricReporter - { - private NoOpMetricReporter() {} - - public void columnDelete(){} - public void rowDelete(){} - public void partitionDelete(){} - public void insert(){} - public void rangeDelete(){} - public void validatePartition(){} - public void validateRandomQuery(){} - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/core/Run.java b/test/harry/main/org/apache/cassandra/harry/core/Run.java deleted file mode 100644 index 0399a34072..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/core/Run.java +++ /dev/null @@ -1,75 +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.harry.core; - -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.operations.QueryGenerator; - -public class Run -{ - public final OpSelectors.PureRng rng; - public final OpSelectors.Clock clock; - public final OpSelectors.PdSelector pdSelector; - public final OpSelectors.DescriptorSelector descriptorSelector; - public final QueryGenerator rangeSelector; - - public final SchemaSpec schemaSpec; - public final DataTracker tracker; - public final SystemUnderTest sut; - - public final MetricReporter metricReporter; - - public Run(OpSelectors.PureRng rng, - OpSelectors.Clock clock, - OpSelectors.PdSelector pdSelector, - OpSelectors.DescriptorSelector descriptorSelector, - SchemaSpec schemaSpec, - DataTracker tracker, - SystemUnderTest sut, - MetricReporter metricReporter) - { - this(rng, clock, pdSelector, descriptorSelector, - new QueryGenerator(schemaSpec, pdSelector, descriptorSelector, rng), - schemaSpec, tracker, sut, metricReporter); - } - - private Run(OpSelectors.PureRng rng, - OpSelectors.Clock clock, - OpSelectors.PdSelector pdSelector, - OpSelectors.DescriptorSelector descriptorSelector, - QueryGenerator rangeSelector, - SchemaSpec schemaSpec, - DataTracker tracker, - SystemUnderTest sut, - MetricReporter metricReporter) - { - this.rng = rng; - this.clock = clock; - this.pdSelector = pdSelector; - this.descriptorSelector = descriptorSelector; - this.rangeSelector = rangeSelector; - this.schemaSpec = schemaSpec; - this.tracker = tracker; - this.sut = sut; - this.metricReporter = metricReporter; - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/AddExtraRowCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/AddExtraRowCorruptor.java deleted file mode 100644 index 11de57ba4a..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/corruptor/AddExtraRowCorruptor.java +++ /dev/null @@ -1,90 +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.harry.corruptor; - -import java.util.HashSet; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.WriteHelper; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.tracker.DataTracker; - -public class AddExtraRowCorruptor implements QueryResponseCorruptor -{ - private static final Logger logger = LoggerFactory.getLogger(AddExtraRowCorruptor.class); - - private final SchemaSpec schema; - private final OpSelectors.Clock clock; - private final DataTracker tracker; - private final OpSelectors.DescriptorSelector descriptorSelector; - - public AddExtraRowCorruptor(SchemaSpec schema, - OpSelectors.Clock clock, - DataTracker tracker, - OpSelectors.DescriptorSelector descriptorSelector) - { - this.schema = schema; - this.clock = clock; - this.tracker = tracker; - this.descriptorSelector = descriptorSelector; - } - - public boolean maybeCorrupt(Query query, SystemUnderTest sut) - { - Set cds = new HashSet<>(); - long maxLts = tracker.maxStarted(); - for (Object[] obj : sut.execute(query.toSelectStatement(), SystemUnderTest.ConsistencyLevel.ALL)) - { - ResultSetRow row = SelectHelper.resultSetToRow(schema, clock, obj); - cds.add(row.cd); - } - boolean partitionIsFull = cds.size() >= descriptorSelector.maxPartitionSize(); - - long attempt = 0; - long cd = descriptorSelector.randomCd(query.pd, attempt, schema); - while (!query.matchCd(cd) || cds.contains(cd)) - { - if (partitionIsFull) - // We can't pick from the existing CDs, so let's try to come up with a new one that would match the query - cd += descriptorSelector.randomCd(query.pd, attempt, schema); - else - cd = descriptorSelector.randomCd(query.pd, attempt, schema); - if (attempt++ == 1000) - return false; - } - - long[] vds = descriptorSelector.vds(query.pd, cd, maxLts, 0, OpSelectors.OperationKind.INSERT, schema); - - // We do not know if the row was deleted. We could try inferring it, but that - // still won't help since we can't use it anyways, since collisions between a - // written value and tombstone are resolved in favour of tombstone, so we're - // just going to take the next lts. - logger.info("Corrupting the resultset by writing a row with cd {}", cd); - sut.execute(WriteHelper.inflateInsert(schema, query.pd, cd, vds, null, clock.rts(maxLts) + 1), SystemUnderTest.ConsistencyLevel.ALL); - return true; - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/ChangeValueCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/ChangeValueCorruptor.java deleted file mode 100644 index 1e9189a93b..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/corruptor/ChangeValueCorruptor.java +++ /dev/null @@ -1,85 +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.harry.corruptor; - -import java.util.Arrays; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.DataGenerators; -import org.apache.cassandra.harry.gen.rng.PcgRSUFast; -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.WriteHelper; - -/** - * Corrupts a single value written value in the row by writing a valid, invertible value with an incorrect - * descriptor, if row has any values written. - */ -public class ChangeValueCorruptor implements RowCorruptor -{ - private final SchemaSpec schema; - private final OpSelectors.Clock clock; - private final EntropySource rng; - - public ChangeValueCorruptor(SchemaSpec schemaSpec, - OpSelectors.Clock clock) - { - this.schema = schemaSpec; - this.clock = clock; - this.rng = new PcgRSUFast(1, 1); - } - - // Can corrupt any row that has at least one written non-null value - public boolean canCorrupt(ResultSetRow row) - { - for (int idx = 0; idx < row.lts.length; idx++) - { - // TODO: in addition to this, we should check if the value equals to the largest possible - // value, since otherwise it won't sort correctly. - if (row.lts[idx] != Model.NO_TIMESTAMP) - return true; - } - return false; - } - - public CompiledStatement corrupt(ResultSetRow row) - { - long[] corruptedVds = new long[row.vds.length]; - Arrays.fill(corruptedVds, DataGenerators.UNSET_DESCR); - - int idx; - do - { - idx = rng.nextInt(corruptedVds.length - 1); - } while (row.lts[idx] == Model.NO_TIMESTAMP); - - final long oldV = row.vds[idx]; - do - { - corruptedVds[idx] = schema.regularColumns.get(idx).type.generator().adjustEntropyDomain(rng.next()); - } - // we need to find a value that sorts strictly greater than the current one - while (schema.regularColumns.get(idx).type.compareLexicographically(corruptedVds[idx], oldV) <= 0); - - return WriteHelper.inflateInsert(schema, row.pd, row.cd, corruptedVds, null, clock.rts(row.lts[idx])); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/HideRowCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/HideRowCorruptor.java deleted file mode 100644 index d8b8ed971c..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/corruptor/HideRowCorruptor.java +++ /dev/null @@ -1,49 +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.harry.corruptor; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.DeleteHelper; - -public class HideRowCorruptor implements RowCorruptor -{ - private final SchemaSpec schema; - private final OpSelectors.Clock clock; - - public HideRowCorruptor(SchemaSpec schemaSpec, - OpSelectors.Clock clock) - { - this.schema = schemaSpec; - this.clock = clock; - } - - // Can corrupt any row that has at least one written non-null value - public boolean canCorrupt(ResultSetRow row) - { - return row != null; - } - - public CompiledStatement corrupt(ResultSetRow row) - { - return DeleteHelper.deleteRow(schema, row.pd, row.cd, clock.rts(clock.peek())); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/HideValueCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/HideValueCorruptor.java deleted file mode 100644 index 7b2d1fbfe7..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/corruptor/HideValueCorruptor.java +++ /dev/null @@ -1,109 +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.harry.corruptor; - -import java.util.HashSet; -import java.util.Set; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.DeleteHelper; -import org.apache.cassandra.harry.util.BitSet; - -// removes/hides the value of one of the columns that was previously set -public class HideValueCorruptor implements RowCorruptor -{ - private final SchemaSpec schema; - private final OpSelectors.Clock clock; - private final EntropySource rng; - - public HideValueCorruptor(SchemaSpec schemaSpec, - OpSelectors.Clock clock) - { - this.schema = schemaSpec; - this.clock = clock; - this.rng = new JdkRandomEntropySource(1L); - } - - // Can corrupt any row that has at least one written non-null value - public boolean canCorrupt(ResultSetRow row) - { - for (int idx = 0; idx < row.lts.length; idx++) - { - if (row.lts[idx] != Model.NO_TIMESTAMP) - return true; - } - return false; - } - - public CompiledStatement corrupt(ResultSetRow row) - { - BitSet mask; - // Corrupt a static row, if it is available and if RNG says so - if (row.hasStaticColumns() && rng.nextBoolean()) - { - int cnt = 0; - int idx; - do - { - idx = rng.nextInt(row.slts.length); - cnt++; - } - while (row.slts[idx] == Model.NO_TIMESTAMP && cnt < 10); - - if (row.slts[idx] != Model.NO_TIMESTAMP) - { - mask = BitSet.allUnset(schema.allColumns.size()); - mask.set(schema.staticColumnsOffset + idx); - - return DeleteHelper.deleteColumn(schema, - row.pd, - mask, - schema.regularAndStaticColumnsMask(), - clock.rts(clock.peek())); - } - } - - Set tried = new HashSet<>(); - int idx; - do - { - if (tried.size() == row.lts.length) - throw new IllegalStateException(String.format("Could not corrupt after trying all %s indexes", tried)); - idx = rng.nextInt(row.lts.length); - tried.add(idx); - } - while (row.lts[idx] == Model.NO_TIMESTAMP); - - mask = BitSet.allUnset(schema.allColumns.size()); - mask.set(schema.regularColumnsOffset + idx); - - return DeleteHelper.deleteColumn(schema, - row.pd, - row.cd, - mask, - schema.regularAndStaticColumnsMask(), - clock.rts(clock.peek())); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/QueryResponseCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/QueryResponseCorruptor.java deleted file mode 100644 index 6495bd4d11..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/corruptor/QueryResponseCorruptor.java +++ /dev/null @@ -1,108 +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.harry.corruptor; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.Query; - -public interface QueryResponseCorruptor -{ - Logger logger = LoggerFactory.getLogger(QueryResponseCorruptor.class); - - boolean maybeCorrupt(Query query, SystemUnderTest sut); - - class SimpleQueryResponseCorruptor implements QueryResponseCorruptor - { - private final RowCorruptor rowCorruptor; - private final SchemaSpec schema; - private final OpSelectors.Clock clock; - - public SimpleQueryResponseCorruptor(SchemaSpec schema, - OpSelectors.Clock clock, - RowCorruptor.RowCorruptorFactory factory) - { - this.rowCorruptor = factory.create(schema, clock); - this.schema = schema; - this.clock = clock; - } - - public boolean maybeCorrupt(Query query, SystemUnderTest sut) - { - List result = new ArrayList<>(); - CompiledStatement statement = query.toSelectStatement(); - Object[][] before = sut.execute(statement.cql(), SystemUnderTest.ConsistencyLevel.ALL, statement.bindings()); - for (Object[] obj : before) - result.add(SelectHelper.resultSetToRow(schema, clock, obj)); - - // Technically, we can do this just depends on corruption strategy, - // we just need to corrupt results of the current query. - if (result.isEmpty()) - return false; - - for (ResultSetRow row : result) - { - if (rowCorruptor.maybeCorrupt(row, sut)) - { - Object[][] after = sut.execute(statement.cql(), SystemUnderTest.ConsistencyLevel.ALL, statement.bindings()); - boolean mismatch = false; - for (int i = 0; i < before.length && i < after.length; i++) - { - if (!Arrays.equals(before[i], after[i])) - { - logger.info("Corrupted: \nBefore: {}\n" + - "After: {}\n", - Arrays.toString(before[i]), - Arrays.toString(after[i])); - mismatch = true; - } - } - assert mismatch || before.length != after.length : String.format("Could not corrupt.\n" + - "Before\n%s\n" + - "After\n%s\nkma", - toString(before), - toString(after)); - return true; - } - } - return false; - } - - private static String toString(Object[][] obj) - { - StringBuilder sb = new StringBuilder(); - for (Object[] objects : obj) - { - sb.append(Arrays.toString(objects)).append("\n"); - } - return sb.toString(); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/RowCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/RowCorruptor.java deleted file mode 100644 index 969f49ce46..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/corruptor/RowCorruptor.java +++ /dev/null @@ -1,56 +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.harry.corruptor; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; - -public interface RowCorruptor -{ - final Logger logger = LoggerFactory.getLogger(QueryResponseCorruptor.class); - - boolean canCorrupt(ResultSetRow row); - - CompiledStatement corrupt(ResultSetRow row); - - // Returns true if it could corrupt a row, false otherwise - default boolean maybeCorrupt(ResultSetRow row, SystemUnderTest sut) - { - if (canCorrupt(row)) - { - CompiledStatement statement = corrupt(row); - sut.execute(statement.cql(), SystemUnderTest.ConsistencyLevel.ALL, statement.bindings()); - logger.info("Corrupting with: {} ({})", statement.cql(), CompiledStatement.bindingsToString(statement.bindings())); - return true; - } - return false; - } - - interface RowCorruptorFactory - { - RowCorruptor create(SchemaSpec schemaSpec, - OpSelectors.Clock clock); - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/ShowValueCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/ShowValueCorruptor.java deleted file mode 100644 index 79c6c737e7..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/corruptor/ShowValueCorruptor.java +++ /dev/null @@ -1,77 +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.harry.corruptor; - -import java.util.Arrays; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.DataGenerators; -import org.apache.cassandra.harry.gen.rng.PcgRSUFast; -import org.apache.cassandra.harry.gen.EntropySource; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.WriteHelper; - -public class ShowValueCorruptor implements RowCorruptor -{ - private final SchemaSpec schema; - private final OpSelectors.Clock clock; - private final EntropySource rng; - - public ShowValueCorruptor(SchemaSpec schemaSpec, - OpSelectors.Clock clock) - { - this.schema = schemaSpec; - this.clock = clock; - this.rng = new PcgRSUFast(1, 1); - } - - // Can corrupt any row that has at least one written non-null value - public boolean canCorrupt(ResultSetRow row) - { - for (int idx = 0; idx < row.lts.length; idx++) - { - if (row.lts[idx] == Model.NO_TIMESTAMP) - return true; - } - return false; - } - - public CompiledStatement corrupt(ResultSetRow row) - { - long[] corruptedVds = new long[row.lts.length]; - Arrays.fill(corruptedVds, DataGenerators.UNSET_DESCR); - - int idx; - do - { - idx = rng.nextInt(corruptedVds.length - 1); - } - while (row.lts[idx] != Model.NO_TIMESTAMP); - - corruptedVds[idx] = rng.next(); - - // We do not know LTS of the deleted row. We could try inferring it, but that - // still won't help since we can't use it anyways, since collisions between a - // written value and tombstone are resolved in favour of tombstone. - return WriteHelper.inflateInsert(schema, row.pd, row.cd, corruptedVds, null, clock.rts(clock.peek())); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/cql/DeleteHelper.java b/test/harry/main/org/apache/cassandra/harry/cql/DeleteHelper.java new file mode 100644 index 0000000000..1a237f5828 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/cql/DeleteHelper.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cql; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +import org.apache.cassandra.cql3.ast.Symbol; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.util.BitSet; + +public class DeleteHelper +{ + public static CompiledStatement inflateDelete(Operations.DeletePartition delete, + SchemaSpec schema, + long timestamp) + { + StringBuilder b = new StringBuilder(); + b.append("DELETE FROM ") + .append(Symbol.maybeQuote(schema.keyspace)) + .append(".") + .append(Symbol.maybeQuote(schema.table)); + + if (timestamp != -1 && schema.options.addWriteTimestamps()) + b.append(" USING TIMESTAMP ").append(timestamp); + + b.append(" WHERE "); + + List bindings = new ArrayList<>(); + + Object[] pk = schema.valueGenerators.pkGen().inflate(delete.pd()); + + RelationWriter writer = new RelationWriter(b, bindings::add) ; + + for (int i = 0; i < pk.length; i++) + writer.write(schema.partitionKeys.get(i), Relations.RelationKind.EQ, pk[i]); + + b.append(";"); + + Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); + + return new CompiledStatement(b.toString(), bindingsArr); + } + + public static CompiledStatement inflateDelete(Operations.DeleteRow delete, + SchemaSpec schema, + long timestamp) + { + StringBuilder b = new StringBuilder(); + b.append("DELETE FROM ") + .append(Symbol.maybeQuote(schema.keyspace)) + .append(".") + .append(Symbol.maybeQuote(schema.table)); + + if (timestamp != -1 && schema.options.addWriteTimestamps()) + b.append(" USING TIMESTAMP ").append(timestamp); + + b.append(" WHERE "); + + List bindings = new ArrayList<>(); + + Object[] pk = schema.valueGenerators.pkGen().inflate(delete.pd()); + Object[] ck = schema.valueGenerators.ckGen().inflate(delete.cd()); + + RelationWriter writer = new RelationWriter(b, bindings::add); + + for (int i = 0; i < pk.length; i++) + writer.write(schema.partitionKeys.get(i), Relations.RelationKind.EQ, pk[i]); + for (int i = 0; i < ck.length; i++) + writer.write(schema.clusteringKeys.get(i), Relations.RelationKind.EQ, ck[i]); + + b.append(";"); + + Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); + + return new CompiledStatement(b.toString(), bindingsArr); + } + + public static CompiledStatement inflateDelete(Operations.DeleteColumns delete, + SchemaSpec schema, + long timestamp) + { + StringBuilder b = new StringBuilder(); + b.append("DELETE "); + + { + String[] names = columnNames(schema.regularColumns, delete.regularColumns()); + for (int i = 0; i < names.length; i++) + { + if (i > 0) + b.append(", "); + b.append(Symbol.maybeQuote(names[i])); + } + b.append(" "); + } + + { + String[] names = columnNames(schema.staticColumns, delete.staticColumns()); + for (int i = 0; i < names.length; i++) + { + if (i > 0) + b.append(", "); + b.append(Symbol.maybeQuote(names[i])); + } + b.append(" "); + } + + b.append("FROM ") + .append(Symbol.maybeQuote(schema.keyspace)) + .append(".") + .append(Symbol.maybeQuote(schema.table)); + + if (timestamp != -1 && schema.options.addWriteTimestamps()) + b.append(" USING TIMESTAMP ").append(timestamp); + + b.append(" WHERE "); + + List bindings = new ArrayList<>(); + + Object[] pk = schema.valueGenerators.pkGen().inflate(delete.pd()); + Object[] ck = schema.valueGenerators.ckGen().inflate(delete.cd()); + + RelationWriter writer = new RelationWriter(b, bindings::add); + + for (int i = 0; i < pk.length; i++) + writer.write(schema.partitionKeys.get(i), Relations.RelationKind.EQ, pk[i]); + for (int i = 0; i < ck.length; i++) + writer.write(schema.clusteringKeys.get(i), Relations.RelationKind.EQ, ck[i]); + + b.append(";"); + + Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); + + return new CompiledStatement(b.toString(), bindingsArr); + } + + public static CompiledStatement inflateDelete(Operations.DeleteRange delete, + SchemaSpec schema, + long timestamp) + { + StringBuilder b = new StringBuilder(); + b.append("DELETE FROM ") + .append(Symbol.maybeQuote(schema.keyspace)) + .append(".") + .append(Symbol.maybeQuote(schema.table)); + + if (timestamp != -1 && schema.options.addWriteTimestamps()) + b.append(" USING TIMESTAMP ").append(timestamp); + + b.append(" WHERE "); + + List bindings = new ArrayList<>(); + + Object[] pk = schema.valueGenerators.pkGen().inflate(delete.pd()); + Object[] lowBound = schema.valueGenerators.ckGen().inflate(delete.lowerBound()); + Object[] highBound = schema.valueGenerators.ckGen().inflate(delete.upperBound()); + + RelationWriter writer = new RelationWriter(b, bindings::add); + + for (int i = 0; i < pk.length; i++) + writer.write(schema.partitionKeys.get(i), Relations.RelationKind.EQ, pk[i]); + for (int i = 0; i < delete.lowerBoundRelation().length; i++) + { + Relations.RelationKind kind; + kind = delete.lowerBoundRelation()[i]; + if (kind != null) + writer.write(schema.clusteringKeys.get(i), kind, lowBound[i]); + kind = delete.upperBoundRelation()[i]; + if (kind != null) + writer.write(schema.clusteringKeys.get(i), kind, highBound[i]); + } + + b.append(";"); + + Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); + + return new CompiledStatement(b.toString(), bindingsArr); + } + + private static final class RelationWriter + { + boolean isFirst = true; + final StringBuilder builder; + final Consumer collectBindings; + + private RelationWriter(StringBuilder builder, Consumer bindings) + { + this.builder = builder; + this.collectBindings = bindings; + } + + public void write(ColumnSpec column, Relations.RelationKind kind, Object value) + { + if (isFirst) + isFirst = false; + else + builder.append(" AND "); + + builder.append(Symbol.maybeQuote(column.name)) + .append(" ") + .append(kind.symbol()) + .append(" ") + .append("?"); + collectBindings.accept(value); + } + } + + + private static String[] columnNames(List> columns, BitSet selectedColumns) + { + String[] columnNames = new String[selectedColumns.size()]; + selectedColumns.eachSetBit(new IntConsumer() + { + int i = 0; + + public void accept(int idx) + { + columnNames[i++] = columns.get(idx).name; + } + }); + return columnNames; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/cql/SelectHelper.java b/test/harry/main/org/apache/cassandra/harry/cql/SelectHelper.java new file mode 100644 index 0000000000..148711173c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/cql/SelectHelper.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cql; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.cassandra.cql3.ast.Bind; +import org.apache.cassandra.cql3.ast.CQLFormatter; +import org.apache.cassandra.cql3.ast.Conditional.Where; +import org.apache.cassandra.cql3.ast.FunctionCall; +import org.apache.cassandra.cql3.ast.Select; +import org.apache.cassandra.cql3.ast.Symbol; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.execution.CompiledStatement; + +public class SelectHelper +{ + public static CompiledStatement select(Operations.SelectPartition select, SchemaSpec schema) + { + Select.Builder builder = commmonPart(select, schema); + + if (select.orderBy() == Operations.ClusteringOrderBy.DESC) + { + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec c = schema.clusteringKeys.get(i); + builder.orderByColumn(c.name, c.type.asServerType(), c.isReversed() ? Select.OrderBy.Ordering.ASC : Select.OrderBy.Ordering.DESC); + } + } + + return toCompiled(builder.build()); + } + + public static CompiledStatement select(Operations.SelectRow select, SchemaSpec schema) + { + Select.Builder builder = commmonPart(select, schema); + + Object[] ck = schema.valueGenerators.ckGen().inflate(select.cd()); + + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec column = schema.clusteringKeys.get(i); + builder.where(new Symbol(column.name, column.type.asServerType()), + toInequality(Relations.RelationKind.EQ), + new Bind(ck[i], column.type.asServerType())); + } + + return toCompiled(builder.build()); + } + + public static CompiledStatement select(Operations.SelectRange select, SchemaSpec schema) + { + Select.Builder builder = commmonPart(select, schema); + + Object[] lowBound = schema.valueGenerators.ckGen().inflate(select.lowerBound()); + Object[] highBound = schema.valueGenerators.ckGen().inflate(select.upperBound()); + + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec column = schema.clusteringKeys.get(i); + if (select.lowerBoundRelation()[i] != null) + { + builder.where(new Symbol(column.name, column.type.asServerType()), + toInequality(select.lowerBoundRelation()[i]), + new Bind(lowBound[i], column.type.asServerType())); + } + + if (select.upperBoundRelation()[i] != null) + { + builder.where(new Symbol(column.name, column.type.asServerType()), + toInequality(select.upperBoundRelation()[i]), + new Bind(highBound[i], column.type.asServerType())); + } + } + + if (select.orderBy() == Operations.ClusteringOrderBy.DESC) + { + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec c = schema.clusteringKeys.get(i); + builder.orderByColumn(c.name, c.type.asServerType(), c.isReversed() ? Select.OrderBy.Ordering.ASC : Select.OrderBy.Ordering.DESC); + } + } + + return toCompiled(builder.build()); + } + + public static CompiledStatement select(Operations.SelectCustom select, SchemaSpec schema) + { + Select.Builder builder = commmonPart(select, schema); + + Map cache = new HashMap<>(); + for (Relations.Relation relation : select.ckRelations()) + { + Object[] query = cache.computeIfAbsent(relation.descriptor, schema.valueGenerators.ckGen()::inflate); + ColumnSpec column = schema.clusteringKeys.get(relation.column); + builder.where(new Symbol(column.name, column.type.asServerType()), + toInequality(relation.kind), + new Bind(query[relation.column], column.type.asServerType())); + } + + for (Relations.Relation relation : select.regularRelations()) + { + ColumnSpec column = schema.regularColumns.get(relation.column); + Object query = schema.valueGenerators.regularColumnGen(relation.column).inflate(relation.descriptor); + builder.where(new Symbol(column.name, column.type.asServerType()), + toInequality(relation.kind), + new Bind(query, column.type.asServerType())); + } + + for (Relations.Relation relation : select.staticRelations()) + { + Object query = schema.valueGenerators.staticColumnGen(relation.column).inflate(relation.descriptor); + ColumnSpec column = schema.staticColumns.get(relation.column); + builder.where(new Symbol(column.name, column.type.asServerType()), + toInequality(relation.kind), + new Bind(query, column.type.asServerType())); + } + + if (select.orderBy() == Operations.ClusteringOrderBy.DESC) + { + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec c = schema.clusteringKeys.get(i); + builder.orderByColumn(c.name, c.type.asServerType(), c.isReversed() ? Select.OrderBy.Ordering.ASC : Select.OrderBy.Ordering.DESC); + } + } + + builder.allowFiltering(); + + return toCompiled(builder.build()); + } + + public static Select.Builder commmonPart(Operations.SelectStatement select, SchemaSpec schema) + { + Select.Builder builder = new Select.Builder(); + + Operations.Selection selection = Operations.Selection.fromBitSet(select.selection(), schema); + if (selection.isWildcard()) + { + builder.wildcard(); + } + else + { + for (int i = 0; i < schema.allColumnInSelectOrder.size(); i++) + { + ColumnSpec spec = schema.allColumnInSelectOrder.get(i); + if (!selection.columns().contains(spec)) + continue; + + builder.columnSelection(spec.name, spec.type.asServerType()); + } + + if (selection.includeTimestamps()) + { + for (ColumnSpec spec : schema.staticColumns) + { + if (!selection.columns().contains(spec)) + continue; + builder.selection(FunctionCall.writetime(spec.name, spec.type.asServerType())); + } + + for (ColumnSpec spec : schema.regularColumns) + { + if (!selection.columns().contains(spec)) + continue; + builder.selection(FunctionCall.writetime(spec.name, spec.type.asServerType())); + } + } + } + + builder.table(schema.keyspace, schema.table); + + Object[] pk = schema.valueGenerators.pkGen().inflate(select.pd()); + for (int i = 0; i < schema.partitionKeys.size(); i++) + { + ColumnSpec column = schema.partitionKeys.get(i); + Object value = pk[i]; + builder.where(new Symbol(column.name, column.type.asServerType()), + Where.Inequality.EQUAL, + new Bind(value, column.type.asServerType())); + } + + return builder; + } + + private static Where.Inequality toInequality(Relations.RelationKind kind) + { + Where.Inequality inequalities; + switch (kind) + { + case LT: + inequalities = Where.Inequality.LESS_THAN; + break; + case LTE: + inequalities = Where.Inequality.LESS_THAN_EQ; + break; + case GT: + inequalities = Where.Inequality.GREATER_THAN; + break; + case GTE: + inequalities = Where.Inequality.GREATER_THAN_EQ; + break; + case EQ: + inequalities = Where.Inequality.EQUAL; + break; + default: + throw new UnsupportedOperationException("Unknown kind: " + kind); + } + return inequalities; + } + + private static CompiledStatement toCompiled(Select select) + { + // Select does not add ';' by default, but CompiledStatement expects this + String cql = select.toCQL(CQLFormatter.None.instance) + ';'; + Object[] bindingsArr = select.binds(); + return new CompiledStatement(cql, bindingsArr); + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/cql/WriteHelper.java b/test/harry/main/org/apache/cassandra/harry/cql/WriteHelper.java new file mode 100644 index 0000000000..b3143a7043 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/cql/WriteHelper.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cql; + +import java.util.List; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.op.Operations; + +public class WriteHelper +{ + public static CompiledStatement inflateInsert(Operations.WriteOp op, + SchemaSpec schema, + long timestamp) + { + assert op.vds().length == schema.regularColumns.size(); + assert op.sds().length == schema.staticColumns.size(); + assert op.vds().length == schema.valueGenerators.regularColumnCount(); + assert op.sds().length == schema.valueGenerators.staticColumnCount(); + + Object[] partitionKey = schema.valueGenerators.pkGen().inflate(op.pd()); + assert partitionKey.length == schema.partitionKeys.size(); + Object[] clusteringKey = schema.valueGenerators.ckGen().inflate(op.cd()); + assert clusteringKey.length == schema.clusteringKeys.size(); + Object[] regularColumns = new Object[op.vds().length]; + Object[] staticColumns = new Object[op.sds().length]; + + for (int i = 0; i < op.vds().length; i++) + { + long descriptor = op.vds()[i]; + if (descriptor == MagicConstants.UNSET_DESCR) + regularColumns[i] = MagicConstants.UNSET_VALUE; + else + regularColumns[i] = schema.valueGenerators.regularColumnGen(i).inflate(descriptor); + } + + for (int i = 0; i < op.sds().length; i++) + { + long descriptor = op.sds()[i]; + if (descriptor == MagicConstants.UNSET_DESCR) + staticColumns[i] = MagicConstants.UNSET_VALUE; + else + staticColumns[i] = schema.valueGenerators.staticColumnGen(i).inflate(descriptor); + } + + Object[] bindings = new Object[schema.allColumnInSelectOrder.size()]; + + StringBuilder b = new StringBuilder(); + b.append("INSERT INTO ") + .append(schema.keyspace) + .append('.') + .append(schema.table) + .append(" ("); + + int bindingsCount = 0; + bindingsCount += appendStatements(b, bindings, schema.partitionKeys, partitionKey, bindingsCount, true, ",", "%s"); + bindingsCount += appendStatements(b, bindings, schema.clusteringKeys, clusteringKey, bindingsCount, false, ",", "%s"); + bindingsCount += appendStatements(b, bindings, schema.regularColumns, regularColumns, bindingsCount, false, ",", "%s"); + bindingsCount += appendStatements(b, bindings, schema.staticColumns, staticColumns, bindingsCount, false, ",", "%s"); + + b.append(") VALUES ("); + + for (int i = 0; i < bindingsCount; i++) + { + if (i > 0) + b.append(", "); + b.append("?"); + } + + b.append(")"); + if (timestamp != -1 && schema.options.addWriteTimestamps()) + { + b.append(" USING TIMESTAMP ") + .append(timestamp); + } + + b.append(";"); + return new CompiledStatement(b.toString(), adjustArraySize(bindings, bindingsCount)); + } + + public static Object[] adjustArraySize(Object[] bindings, int bindingsCount) + { + if (bindingsCount != bindings.length) + { + Object[] tmp = new Object[bindingsCount]; + System.arraycopy(bindings, 0, tmp, 0, bindingsCount); + bindings = tmp; + } + return bindings; + } + + public static CompiledStatement inflateUpdate(Operations.WriteOp op, + SchemaSpec schema, + long timestamp) + { + assert op.vds().length == schema.regularColumns.size(); + assert op.sds().length == schema.staticColumns.size(); + assert op.vds().length == schema.valueGenerators.regularColumnCount(); + assert op.sds().length == schema.valueGenerators.staticColumnCount(); + + Object[] partitionKey = schema.valueGenerators.pkGen().inflate(op.pd); + assert partitionKey.length == schema.partitionKeys.size(); + Object[] clusteringKey = schema.valueGenerators.ckGen().inflate(op.cd()); + assert clusteringKey.length == schema.clusteringKeys.size(); + Object[] regularColumns = new Object[op.vds().length]; + Object[] staticColumns = new Object[op.sds().length]; + + for (int i = 0; i < op.vds().length; i++) + regularColumns[i] = schema.valueGenerators.regularColumnGen(i).inflate(op.vds()[i]); + + for (int i = 0; i < op.sds().length; i++) + staticColumns[i] = schema.valueGenerators.staticColumnGen(i).inflate(op.sds()[i]); + + Object[] bindings = new Object[schema.allColumnInSelectOrder.size()]; + + StringBuilder b = new StringBuilder(); + b.append("UPDATE ") + .append(schema.keyspace) + .append('.') + .append(schema.table); + + if (timestamp != -1 && schema.options.addWriteTimestamps()) + { + b.append(" USING TIMESTAMP ") + .append(timestamp) + .append(" SET "); + } + + int bindingsCount = 0; + bindingsCount += addSetStatements(b, bindings, schema.regularColumns, regularColumns, bindingsCount); + if (staticColumns.length != 0) + bindingsCount += addSetStatements(b, bindings, schema.staticColumns, staticColumns, bindingsCount); + + assert bindingsCount > 0 : "Can not have an UPDATE statement without any updates"; + b.append(" WHERE "); + + bindingsCount += addWhereStatements(b, bindings, schema.partitionKeys, partitionKey, bindingsCount, true); + bindingsCount += addWhereStatements(b, bindings, schema.clusteringKeys, clusteringKey, bindingsCount, false); + b.append(";"); + return new CompiledStatement(b.toString(), adjustArraySize(bindings, bindingsCount)); + } + + private static int addSetStatements(StringBuilder b, + Object[] bindings, + List> columns, + Object[] values, + int bound) + { + return appendStatements(b, bindings, columns, values, bound, bound == 0, ", ", "%s = ?"); + } + + private static int addWhereStatements(StringBuilder b, + Object[] bindings, + List> columns, + Object[] values, + int bound, + boolean firstStatement) + { + return appendStatements(b, bindings, columns, values, bound, firstStatement, " AND ", "%s = ?"); + } + + private static int appendStatements(StringBuilder b, + Object[] allBindings, + List> columns, + Object[] values, + int bound, + boolean firstStatement, + String separator, + String nameFormatter) + { + int bindingsCount = 0; + for (int i = 0; i < values.length; i++) + { + Object value = values[i]; + if (value == MagicConstants.UNSET_VALUE) + continue; + Invariants.nonNull(value); + ColumnSpec column = columns.get(i); + if (bindingsCount > 0 || !firstStatement) + b.append(separator); + + b.append(String.format(nameFormatter, column.name)); + allBindings[bound + bindingsCount] = value; + bindingsCount++; + } + return bindingsCount; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/data/ResultSetRow.java b/test/harry/main/org/apache/cassandra/harry/data/ResultSetRow.java deleted file mode 100644 index 7ee1e06e0c..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/data/ResultSetRow.java +++ /dev/null @@ -1,137 +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.harry.data; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.util.StringUtils; - -public class ResultSetRow -{ - public final long pd; - public final long cd; - public final long[] vds; - public final long[] lts; - - public final long[] sds; - public final long[] slts; - public final List visited_lts; - - private ResultSetRow(long pd, - long cd, - long[] sds, - long[] slts, - long[] vds, - long[] lts) - { - this(pd, cd, sds, slts, vds, lts, Collections.emptyList()); - } - - public ResultSetRow(long pd, - long cd, - long[] sds, - long[] slts, - long[] vds, - long[] lts, - List visited_lts) - { - // There should be as many timestamps as value descriptors, both for regular and static columns - assert slts.length == sds.length : String.format("Descriptors: %s, timestamps: %s", Arrays.toString(sds), Arrays.toString(slts)); - assert lts.length == vds.length : String.format("Descriptors: %s, timestamps: %s", Arrays.toString(vds), Arrays.toString(lts)); - this.pd = pd; - this.cd = cd; - this.vds = vds; - this.lts = lts; - this.sds = sds; - this.slts = slts; - this.visited_lts = visited_lts; - } - - public boolean hasStaticColumns() - { - return slts.length > 0; - } - - @Override - public ResultSetRow clone() - { - return new ResultSetRow(pd, cd, - Arrays.copyOf(sds, sds.length), Arrays.copyOf(slts, slts.length), - Arrays.copyOf(vds, vds.length), Arrays.copyOf(lts, lts.length), - visited_lts); - } - - @Override - public int hashCode() - { - int result = Objects.hash(pd, cd, visited_lts); - result = 31 * result + Arrays.hashCode(vds); - result = 31 * result + Arrays.hashCode(lts); - result = 31 * result + Arrays.hashCode(sds); - result = 31 * result + Arrays.hashCode(slts); - return result; - } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ResultSetRow that = (ResultSetRow) o; - return pd == that.pd && - cd == that.cd && - Arrays.equals(vds, that.vds) && - Arrays.equals(lts, that.lts) && - Arrays.equals(sds, that.sds) && - Arrays.equals(slts, that.slts) && - Objects.equals(visited_lts, that.visited_lts); - } - - @Override - public String toString() - { - return "resultSetRow(" - + pd + - "L, " + cd + - (sds == null ? "" : "L, statics(" + StringUtils.toString(sds) + ")") + - (slts == null ? "" : ", lts(" + StringUtils.toString(slts) + ")") + - ", values(" + StringUtils.toString(vds) + ")" + - ", lts(" + StringUtils.toString(lts) + ")" + - ")"; - } - - public String toString(SchemaSpec schema) - { - return "resultSetRow(" - + pd + - "L, " + cd + - (sds == null ? "" : "L, staticValues(" + StringUtils.toString(sds) + ")") + - (slts == null ? "" : ", slts(" + StringUtils.toString(slts) + ")") + - ", values(" + StringUtils.toString(vds) + ")" + - ", lts(" + StringUtils.toString(lts) + ")" + - ", clustering=" + Arrays.toString(schema.inflateClusteringKey(cd)) + - ", values=" + Arrays.toString(schema.inflateRegularColumns(vds)) + - (sds == null ? "" : ", statics=" + Arrays.toString(schema.inflateStaticColumns(sds))) + - ")"; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/ddl/ColumnSpec.java b/test/harry/main/org/apache/cassandra/harry/ddl/ColumnSpec.java deleted file mode 100644 index 88bd95d6a9..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/ddl/ColumnSpec.java +++ /dev/null @@ -1,460 +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.harry.ddl; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import org.apache.cassandra.harry.gen.Bijections; -import org.apache.cassandra.harry.gen.StringBijection; - -public class ColumnSpec -{ - public final String name; - public final DataType type; - public final Kind kind; - int columnIndex; - - public ColumnSpec(String name, - DataType type, - Kind kind) - { - this.name = name; - this.type = type; - this.kind = kind; - } - - - public ColumnSpec override(Bijections.Bijection override) - { - return new ColumnSpec<>(name, - new DataType<>(type.cqlName) { - @Override - public int compareLexicographically(long l, long r) - { - return type.compareLexicographically(l, r); - } - - @Override - public boolean isReversed() - { - return type.isReversed(); - } - - @Override - public Bijections.Bijection generator() - { - return override; - } - }, - 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.cqlName, that.type.cqlName) && - kind == that.kind; - } - - @Override - public int hashCode() - { - return Objects.hash(name, type.cqlName, kind); - } - - public String name() - { - return name; - } - - public boolean isReversed() - { - return type.isReversed(); - } - - public String toString() - { - return name + '(' + type.toString() + ")"; - } - - public Bijections.Bijection generator() - { - return type.generator(); - } - - public T inflate(long current) - { - return type.generator().inflate(current); - } - - public long deflate(T value) - { - return type.generator().deflate(value); - } - - public static ColumnSpec pk(String name, DataType type) - { - return new ColumnSpec<>(name, type, Kind.PARTITION_KEY); - } - - @SuppressWarnings("unchecked") - public static ColumnSpec ck(String name, DataType type, boolean isReversed) - { - return new ColumnSpec(name, isReversed ? ReversedType.getInstance(type) : type, Kind.CLUSTERING); - } - - @SuppressWarnings("unchecked") - public static ColumnSpec ck(String name, DataType type) - { - return new ColumnSpec(name, type, Kind.CLUSTERING); - } - - public static ColumnSpec regularColumn(String name, DataType type) - { - return new ColumnSpec<>(name, type, Kind.REGULAR); - } - - public static ColumnSpec staticColumn(String name, DataType type) - { - return new ColumnSpec<>(name, type, Kind.STATIC); - } - - public enum Kind - { - CLUSTERING, REGULAR, STATIC, PARTITION_KEY - } - - public static abstract class DataType - { - protected final String cqlName; - - protected DataType(String cqlName) - { - this.cqlName = cqlName; - } - - public boolean isReversed() - { - return false; - } - - /** - * Cassandra uses lexicographical oder for resolving timestamp ties - */ - public int compareLexicographically(long l, long r) - { - for (int i = Long.BYTES - 1; i >= 0; i--) - { - int cmp = Integer.compare((int) ((l >> (i * 8)) & 0xffL), - (int) ((r >> (i * 8)) & 0xffL)); - if (cmp != 0) - return cmp; - } - return 0; - } - - public abstract Bijections.Bijection generator(); - - public int maxSize() - { - return generator().byteSize(); - } - - public final String toString() - { - return cqlName; - } - - public String nameForParser() - { - return cqlName; - } - - public final 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 final int hashCode() - { - return Objects.hash(cqlName); - } - } - - public static final DataType int8Type = new DataType("tinyint") - { - public Bijections.Bijection generator() - { - return Bijections.INT8_GENERATOR; - } - }; - - public static final DataType int16Type = new DataType("smallint") - { - public Bijections.Bijection generator() - { - return Bijections.INT16_GENERATOR; - } - }; - - public static final DataType int32Type = new DataType("int") - { - public Bijections.Bijection generator() - { - return Bijections.INT32_GENERATOR; - } - }; - - public static final DataType int64Type = new DataType("bigint") - { - public Bijections.Bijection generator() - { - return Bijections.INT64_GENERATOR; - } - }; - - public static final DataType booleanType = new DataType("boolean") - { - public Bijections.Bijection generator() - { - return Bijections.BOOLEAN_GENERATOR; - } - - public int compareLexicographically(long l, long r) - { - throw new RuntimeException("Boolean does not support custom comparators"); - } - }; - - public static final DataType floatType = new DataType("float") - { - public Bijections.Bijection generator() - { - return Bijections.FLOAT_GENERATOR; - } - }; - - public static final DataType doubleType = new DataType("double") - { - public Bijections.Bijection generator() - { - return Bijections.DOUBLE_GENERATOR; - } - }; - - public static final DataType asciiType = new DataType("ascii") - { - private final Bijections.Bijection gen = new StringBijection(); - - public Bijections.Bijection generator() - { - return gen; - } - - public int compareLexicographically(long l, long r) - { - return Long.compare(l, r); - } - }; - - public static final DataType textType = new DataType("text") - { - private final Bijections.Bijection gen = new StringBijection(); - - public Bijections.Bijection generator() - { - return gen; - } - - public int compareLexicographically(long l, long r) - { - return Long.compare(l, r); - } - }; - - public static DataType asciiType(int nibbleSize, int maxRandomBytes) - { - Bijections.Bijection gen = new StringBijection(nibbleSize, maxRandomBytes); - - return new DataType("ascii") - { - public Bijections.Bijection generator() - { - return gen; - } - - public int compareLexicographically(long l, long r) - { - return Long.compare(l, r); - } - - public String nameForParser() - { - return String.format("%s(%d,%d)", - super.nameForParser(), - nibbleSize, - maxRandomBytes); - } - }; - } - - public static final DataType uuidType = new DataType("uuid") - { - public Bijections.Bijection generator() - { - return Bijections.UUID_GENERATOR; - } - - public int compareLexicographically(long l, long r) - { - throw new RuntimeException("UUID does not support custom comparators"); - } - }; - - public static final DataType timeUuidType = new DataType("timeuuid") - { - public Bijections.Bijection generator() - { - return Bijections.TIME_UUID_GENERATOR; - } - - public int compareLexicographically(long l, long r) - { - throw new RuntimeException("UUID does not support custom comparators"); - } - }; - - public static final DataType timestampType = new DataType("timestamp") - { - public Bijections.Bijection generator() - { - return Bijections.TIMESTAMP_GENERATOR; - } - - public int compareLexicographically(long l, long r) - { - throw new RuntimeException("Date does not support custom comparators"); - } - }; - - public static final Collection> DATA_TYPES = Collections.unmodifiableList( - Arrays.asList(ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.booleanType, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType, - ColumnSpec.textType, - ColumnSpec.uuidType, - ColumnSpec.timeUuidType, - ColumnSpec.timestampType)); - - public static class ReversedType extends DataType - { - public static final Map, ReversedType> cache = new HashMap() - {{ - put(int8Type, new ReversedType<>(int8Type)); - put(int16Type, new ReversedType<>(int16Type)); - put(int32Type, new ReversedType<>(int32Type)); - put(int64Type, new ReversedType<>(int64Type)); - put(booleanType, new ReversedType<>(booleanType)); - put(floatType, new ReversedType<>(floatType, new Bijections.ReverseFloatGenerator())); - put(doubleType, new ReversedType<>(doubleType, new Bijections.ReverseDoubleGenerator())); - put(asciiType, new ReversedType<>(asciiType)); - put(uuidType, new ReversedType<>(uuidType)); - put(timeUuidType, new ReversedType<>(timeUuidType)); - }}; - - private final DataType baseType; - private final Bijections.Bijection generator; - - public ReversedType(DataType baseType) - { - super(baseType.cqlName); - this.baseType = baseType; - this.generator = new Bijections.ReverseBijection<>(baseType.generator()); - } - - public ReversedType(DataType baseType, Bijections.Bijection generator) - { - super(baseType.cqlName); - this.baseType = baseType; - this.generator = generator; - } - - public boolean isReversed() - { - return true; - } - - public Bijections.Bijection generator() - { - return generator; - } - - public int maxSize() - { - return baseType.maxSize(); - } - - public static DataType getInstance(DataType type) - { - ReversedType t = (ReversedType) cache.get(type); - if (t == null) - t = new ReversedType<>(type); - assert t.baseType == type : String.format("Type mismatch %s != %s", t.baseType, type); - return t; - } - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/ddl/SchemaGenerators.java b/test/harry/main/org/apache/cassandra/harry/ddl/SchemaGenerators.java deleted file mode 100644 index 7f1a8790bd..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/ddl/SchemaGenerators.java +++ /dev/null @@ -1,512 +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.harry.ddl; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.harry.gen.Generator; -import org.apache.cassandra.harry.gen.Surjections; - -public class SchemaGenerators -{ - private final static long SCHEMAGEN_STREAM_ID = 0x6264593273L; - - public static Builder schema(String ks) - { - return new Builder(ks); - } - - public static final Map> nameToTypeMap; - public static final Collection> columnTypes; - public static final Collection> partitionKeyTypes; - public static final Collection> clusteringKeyTypes; - - static - { - partitionKeyTypes = Collections.unmodifiableList(Arrays.asList(ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType, - ColumnSpec.textType)); - - columnTypes = Collections.unmodifiableList(Arrays.asList(ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType, - ColumnSpec.textType)); - - - List> builder = new ArrayList<>(partitionKeyTypes); - Map> mapBuilder = new HashMap<>(); - - for (ColumnSpec.DataType columnType : partitionKeyTypes) - { - ColumnSpec.DataType reversedType = ColumnSpec.ReversedType.getInstance(columnType); - builder.add(reversedType); - - mapBuilder.put(columnType.nameForParser(), columnType); - mapBuilder.put(String.format("desc(%s)", columnType.nameForParser()), columnType); - } - - builder.add(ColumnSpec.floatType); - builder.add(ColumnSpec.doubleType); - - clusteringKeyTypes = Collections.unmodifiableList(builder); - nameToTypeMap = Collections.unmodifiableMap(mapBuilder); - } - - @SuppressWarnings("unchecked") - public static Generator fromValues(Collection allValues) - { - return fromValues((T[]) allValues.toArray()); - } - - public static Generator fromValues(T[] allValues) - { - return (rng) -> { - return allValues[rng.nextInt(allValues.length - 1)]; - }; - } - - @SuppressWarnings("unchecked") - public static Generator> columnSpecGenerator(String prefix, ColumnSpec.Kind kind) - { - return fromValues(columnTypes) - .map(new Function, ColumnSpec>() - { - private int counter = 0; - - public ColumnSpec apply(ColumnSpec.DataType type) - { - return new ColumnSpec<>(prefix + (counter++), - type, - kind); - } - }); - } - - @SuppressWarnings("unchecked") - public static Generator> columnSpecGenerator(Collection> columnTypes, String prefix, ColumnSpec.Kind kind) - { - return fromValues(columnTypes) - .map(new Function, ColumnSpec>() - { - private int counter = 0; - - public ColumnSpec apply(ColumnSpec.DataType type) - { - return new ColumnSpec<>(String.format("%s%04d", prefix, counter++), - type, - kind); - } - }); - } - - @SuppressWarnings("unchecked") - public static Generator> clusteringColumnSpecGenerator(String prefix) - { - return fromValues(clusteringKeyTypes) - .map(new Function, ColumnSpec>() - { - private int counter = 0; - - public ColumnSpec apply(ColumnSpec.DataType type) - { - return ColumnSpec.ck(String.format("%s%04d", prefix, counter++), type); - } - }); - } - - @SuppressWarnings("unchecked") - public static Generator> partitionColumnSpecGenerator(String prefix) - { - return fromValues(partitionKeyTypes) - .map(new Function, ColumnSpec>() - { - private int counter = 0; - - public ColumnSpec apply(ColumnSpec.DataType type) - { - - return ColumnSpec.pk(String.format("%s%04d", prefix, counter++), - type); - } - }); - } - - private static AtomicInteger tableCounter = new AtomicInteger(1); - - public static class Builder - { - private final String keyspace; - private final Supplier tableNameSupplier; - - private Generator> pkGenerator = partitionColumnSpecGenerator("pk"); - private Generator> ckGenerator = clusteringColumnSpecGenerator("ck"); - private Generator> regularGenerator = columnSpecGenerator("regular", ColumnSpec.Kind.REGULAR); - private Generator> staticGenerator = columnSpecGenerator("static", ColumnSpec.Kind.STATIC); - - private int minPks = 1; - private int maxPks = 1; - private int minCks = 0; - private int maxCks = 0; - private int minRegular = 0; - private int maxRegular = 0; - private int minStatic = 0; - private int maxStatic = 0; - - public Builder(String keyspace) - { - this(keyspace, () -> "table_" + tableCounter.getAndIncrement()); - } - - public Builder(String keyspace, Supplier tableNameSupplier) - { - this.keyspace = keyspace; - this.tableNameSupplier = tableNameSupplier; - } - - public Builder partitionKeyColumnCount(int numCols) - { - return partitionKeyColumnCount(numCols, numCols); - } - - public Builder partitionKeyColumnCount(int minCols, int maxCols) - { - this.minPks = minCols; - this.maxPks = maxCols; - return this; - } - - public Builder partitionKeySpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) - { - return partitionKeySpec(minCols, maxCols, Arrays.asList(columnTypes)); - } - - public Builder partitionKeySpec(int minCols, int maxCols, Collection> columnTypes) - { - this.minPks = minCols; - this.maxPks = maxCols; - this.pkGenerator = columnSpecGenerator(columnTypes, "pk", ColumnSpec.Kind.PARTITION_KEY); - return this; - } - - public Builder clusteringColumnCount(int numCols) - { - return clusteringColumnCount(numCols, numCols); - } - - public Builder clusteringColumnCount(int minCols, int maxCols) - { - this.minCks = minCols; - this.maxCks = maxCols; - return this; - } - - public Builder clusteringKeySpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) - { - return clusteringKeySpec(minCols, maxCols, Arrays.asList(columnTypes)); - } - - public Builder clusteringKeySpec(int minCols, int maxCols, Collection> columnTypes) - { - this.minCks = minCols; - this.maxCks = maxCols; - this.ckGenerator = columnSpecGenerator(columnTypes, "ck", ColumnSpec.Kind.CLUSTERING); - return this; - } - - public Builder regularColumnCount(int minCols, int maxCols) - { - this.minRegular = minCols; - this.maxRegular = maxCols; - return this; - } - - public Builder regularColumnCount(int numCols) - { - return regularColumnCount(numCols, numCols); - } - - public Builder regularColumnSpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) - { - return this.regularColumnSpec(minCols, maxCols, Arrays.asList(columnTypes)); - } - - public Builder regularColumnSpec(int minCols, int maxCols, Collection> columnTypes) - { - this.minRegular = minCols; - this.maxRegular = maxCols; - this.regularGenerator = columnSpecGenerator(columnTypes, "regular", ColumnSpec.Kind.REGULAR); - return this; - } - - public Builder staticColumnCount(int minCols, int maxCols) - { - this.minStatic = minCols; - this.maxStatic = maxCols; - return this; - } - - public Builder staticColumnCount(int numCols) - { - return staticColumnCount(numCols, numCols); - } - - public Builder staticColumnSpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) - { - return this.staticColumnSpec(minCols, maxCols, Arrays.asList(columnTypes)); - } - - public Builder staticColumnSpec(int minCols, int maxCols, Collection> columnTypes) - { - this.minStatic = minCols; - this.maxStatic = maxCols; - this.staticGenerator = columnSpecGenerator(columnTypes, "static", ColumnSpec.Kind.STATIC); - return this; - } - - private static class ColumnCounts - { - private final int pks; - private final int cks; - private final int regulars; - private final int statics; - - private ColumnCounts(int pks, int cks, int regulars, int statics) - { - this.pks = pks; - this.cks = cks; - this.regulars = regulars; - this.statics = statics; - } - } - - public Generator columnCountsGenerator() - { - return (rand) -> { - int pks = rand.nextInt(minPks, maxPks); - int cks = rand.nextInt(minCks, maxCks); - int regulars = rand.nextInt(minRegular, maxRegular); - int statics = rand.nextInt(minStatic, maxStatic); - - return new ColumnCounts(pks, cks, regulars, statics); - }; - } - - public Generator generator() - { - Generator columnCountsGenerator = columnCountsGenerator(); - - return columnCountsGenerator.flatMap(counts -> { - return rand -> { - List> pk = pkGenerator.generate(rand, counts.pks); - List> ck = ckGenerator.generate(rand, counts.cks); - return new SchemaSpec(keyspace, - tableNameSupplier.get(), - pk, - ck, - regularGenerator.generate(rand, counts.regulars), - staticGenerator.generate(rand, counts.statics)); - }; - }); - } - - public Surjections.Surjection surjection() - { - return generator().toSurjection(SCHEMAGEN_STREAM_ID); - } - } - - public static Surjections.Surjection defaultSchemaSpecGen(String table) - { - return new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, () -> table) - .partitionKeySpec(1, 3, - partitionKeyTypes) - .clusteringKeySpec(1, 3, - clusteringKeyTypes) - .regularColumnSpec(3, 5, - ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType(5, 256)) - .staticColumnSpec(3, 5, - ColumnSpec.int8Type, - ColumnSpec.int16Type, - ColumnSpec.int32Type, - ColumnSpec.int64Type, - ColumnSpec.floatType, - ColumnSpec.doubleType, - ColumnSpec.asciiType(4, 512), - ColumnSpec.asciiType(4, 2048)) - .surjection(); - } - - public static String DEFAULT_KEYSPACE_NAME = "harry"; - - private static final String DEFAULT_PREFIX = "table_"; - private static final AtomicInteger counter = new AtomicInteger(); - private static final Supplier tableNameSupplier = () -> DEFAULT_PREFIX + counter.getAndIncrement(); - - // simplest schema gen, nothing can go wrong with it - public static final Surjections.Surjection longOnlySpecBuilder = new Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) - .partitionKeySpec(1, 1, ColumnSpec.int64Type) - .clusteringKeySpec(1, 1, ColumnSpec.int64Type) - .regularColumnSpec(1, 10, ColumnSpec.int64Type) - .staticColumnSpec(1, 10, ColumnSpec.int64Type) - .surjection(); - - private static final ColumnSpec.DataType simpleStringType = ColumnSpec.asciiType(4, 10); - private static final Surjections.Surjection longAndStringSpecBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) - .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) - .clusteringKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) - .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) - .staticColumnSpec(1, 10, ColumnSpec.int64Type) - .surjection(); - - public static final Surjections.Surjection longOnlyWithReverseSpecBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) - .partitionKeySpec(1, 1, ColumnSpec.int64Type) - .clusteringKeySpec(1, 1, ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type)) - .regularColumnSpec(1, 10, ColumnSpec.int64Type) - .staticColumnSpec(1, 10, ColumnSpec.int64Type) - .surjection(); - - public static final Surjections.Surjection longAndStringSpecWithReversedLongBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) - .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) - .clusteringKeySpec(2, 2, ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), simpleStringType) - .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) - .staticColumnSpec(1, 10, ColumnSpec.int64Type) - .surjection(); - - public static final Surjections.Surjection longAndStringSpecWithReversedStringBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) - .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) - .clusteringKeySpec(2, 2, ColumnSpec.int64Type, ColumnSpec.ReversedType.getInstance(simpleStringType)) - .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) - .staticColumnSpec(1, 10, ColumnSpec.int64Type) - .surjection(); - - public static final Surjections.Surjection longAndStringSpecWithReversedBothBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) - .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) - .clusteringKeySpec(2, 2, ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), ColumnSpec.ReversedType.getInstance(simpleStringType)) - .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) - .staticColumnSpec(1, 10, ColumnSpec.int64Type) - .surjection(); - - public static final Surjections.Surjection withAllFeaturesEnabled = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) - .partitionKeySpec(1, 4, columnTypes) - .clusteringKeySpec(1, 4, clusteringKeyTypes) - .regularColumnSpec(1, 10, columnTypes) - .surjection(); - - public static final Surjections.Surjection[] PROGRESSIVE_GENERATORS = new Surjections.Surjection[]{ - longOnlySpecBuilder, - longAndStringSpecBuilder, - longOnlyWithReverseSpecBuilder, - longAndStringSpecWithReversedLongBuilder, - longAndStringSpecWithReversedStringBuilder, - longAndStringSpecWithReversedBothBuilder, - withAllFeaturesEnabled - }; - - // Create schema generators that would produce tables starting with just a few features, progressing to use more - public static Supplier progression(int switchAfter) - { - Supplier[] generators = new Supplier[PROGRESSIVE_GENERATORS.length]; - for (int i = 0; i < generators.length; i++) - generators[i] = PROGRESSIVE_GENERATORS[i].toSupplier(); - - return new Supplier() - { - private int counter = 0; - public SchemaSpec get() - { - int idx = (counter / switchAfter) % generators.length; - counter++; - SchemaSpec spec = generators[idx].get(); - int tries = 100; - while ((spec.pkGenerator.byteSize() != Long.BYTES) && tries > 0) - { - System.out.println("Skipping schema, since it doesn't have enough entropy bits available: " + spec.compile().cql()); - spec = generators[idx].get(); - tries--; - } - - spec.validate(); - - assert tries > 0 : String.format("Max number of tries exceeded on generator %d, can't generate a needed schema", idx); - return spec; - } - }; - } - - public static List> toColumns(Map config, ColumnSpec.Kind kind, boolean allowReverse) - { - if (config == null) - return Collections.EMPTY_LIST; - - List> columns = new ArrayList<>(config.size()); - - for (Map.Entry e : config.entrySet()) - { - ColumnSpec.DataType type = nameToTypeMap.get(e.getValue()); - assert type != null : "Can't parse the type"; - assert allowReverse || !type.isReversed() : String.format("%s columns aren't allowed to be reversed", type); - columns.add(new ColumnSpec<>(e.getKey(), type, kind)); - } - - return columns; - } - - public static SchemaSpec parse(String keyspace, - String table, - Map pks, - Map cks, - Map regulars, - Map statics) - { - return new SchemaSpec(keyspace, table, - toColumns(pks, ColumnSpec.Kind.PARTITION_KEY, false), - toColumns(cks, ColumnSpec.Kind.CLUSTERING, false), - toColumns(regulars, ColumnSpec.Kind.REGULAR, false), - toColumns(statics, ColumnSpec.Kind.STATIC, false)); - } - - public static int DEFAULT_SWITCH_AFTER = CassandraRelevantProperties.TEST_HARRY_SWITCH_AFTER.getInt(); - public static int GENERATORS_COUNT = PROGRESSIVE_GENERATORS.length; - public static int DEFAULT_RUNS = DEFAULT_SWITCH_AFTER * GENERATORS_COUNT; -} diff --git a/test/harry/main/org/apache/cassandra/harry/ddl/SchemaSpec.java b/test/harry/main/org/apache/cassandra/harry/ddl/SchemaSpec.java deleted file mode 100644 index 407059dfce..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/ddl/SchemaSpec.java +++ /dev/null @@ -1,531 +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.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 - { - SchemaSpec make(long seed, SystemUnderTest sut); - } - - public final DataGenerators.KeyGenerator pkGenerator; - public final DataGenerators.KeyGenerator ckGenerator; - - private final boolean isCompactStorage; - private final boolean disableReadRepair; - private final String compactionStrategy; - public final boolean trackLts; - - // These fields are immutable, and are safe as public - public final String keyspace; - public final String table; - - public final List> partitionKeys; - public final List> clusteringKeys; - public final List> regularColumns; - public final List> staticColumns; - public final List> allColumns; - public final Set> allColumnsSet; - - public final BitSet ALL_COLUMNS_BITSET; - public final int regularColumnsOffset; - public final int staticColumnsOffset; - public final BitSet regularColumnsMask; - public final BitSet regularAndStaticColumnsMask; - public final BitSet staticColumnsMask; - - public SchemaSpec(String keyspace, - String table, - List> partitionKeys, - List> clusteringKeys, - List> regularColumns, - List> staticColumns) - { - this(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, DataGenerators.createKeyGenerator(clusteringKeys), false, false, null, false); - } - - public SchemaSpec cloneWithName(String ks, - String table) - { - return new SchemaSpec(ks, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, ckGenerator, isCompactStorage, disableReadRepair, compactionStrategy, trackLts); - } - - public SchemaSpec trackLts() - { - return new SchemaSpec(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, ckGenerator, isCompactStorage, disableReadRepair, compactionStrategy, true); - } - - public SchemaSpec withCompactStorage() - { - return new SchemaSpec(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, ckGenerator, true, disableReadRepair, compactionStrategy, trackLts); - } - - public SchemaSpec withCompactionStrategy(String compactionStrategy) - { - return new SchemaSpec(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, ckGenerator, false, disableReadRepair, compactionStrategy, trackLts); - } - - public SchemaSpec withCkGenerator(DataGenerators.KeyGenerator ckGeneratorOverride, List> clusteringKeys) - { - return new SchemaSpec(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, ckGeneratorOverride, isCompactStorage, disableReadRepair, compactionStrategy, trackLts); - } - - public SchemaSpec withColumns(List> regularColumns, List> staticColumns) - { - return new SchemaSpec(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, ckGenerator, isCompactStorage, disableReadRepair, compactionStrategy, trackLts); - } - - public SchemaSpec(String keyspace, - String table, - List> partitionKeys, - List> clusteringKeys, - List> regularColumns, - List> staticColumns, - DataGenerators.KeyGenerator ckGenerator, - boolean isCompactStorage, - boolean disableReadRepair, - String compactionStrategy, - boolean trackLts) - { - assert !isCompactStorage || clusteringKeys.isEmpty() || regularColumns.size() <= 1 : - String.format("Compact storage %s. Clustering keys: %d. Regular columns: %d", isCompactStorage, clusteringKeys.size(), regularColumns.size()); - - this.keyspace = keyspace; - this.table = table; - this.isCompactStorage = isCompactStorage; - this.disableReadRepair = disableReadRepair; - this.compactionStrategy = compactionStrategy; - - this.partitionKeys = Collections.unmodifiableList(new ArrayList<>(partitionKeys)); - for (int i = 0; i < partitionKeys.size(); i++) - partitionKeys.get(i).setColumnIndex(i); - this.clusteringKeys = Collections.unmodifiableList(new ArrayList<>(clusteringKeys)); - for (int i = 0; i < clusteringKeys.size(); i++) - clusteringKeys.get(i).setColumnIndex(i); - this.staticColumns = Collections.unmodifiableList(new ArrayList<>(staticColumns)); - for (int i = 0; i < staticColumns.size(); i++) - staticColumns.get(i).setColumnIndex(i); - this.regularColumns = Collections.unmodifiableList(new ArrayList<>(regularColumns)); - for (int i = 0; i < regularColumns.size(); i++) - regularColumns.get(i).setColumnIndex(i); - - List> all = new ArrayList<>(); - for (ColumnSpec columnSpec : concat(partitionKeys, - clusteringKeys, - staticColumns, - regularColumns)) - { - all.add(columnSpec); - } - this.allColumns = Collections.unmodifiableList(all); - this.allColumnsSet = Collections.unmodifiableSet(new LinkedHashSet<>(all)); - - this.pkGenerator = DataGenerators.createKeyGenerator(partitionKeys); - if (ckGenerator == null) - ckGenerator = DataGenerators.createKeyGenerator(clusteringKeys); - this.ckGenerator = ckGenerator; - - this.ALL_COLUMNS_BITSET = BitSet.allSet(regularColumns.size()); - - this.staticColumnsOffset = partitionKeys.size() + clusteringKeys.size(); - this.regularColumnsOffset = staticColumnsOffset + staticColumns.size(); - - this.regularColumnsMask = regularColumnsMask(this); - this.regularAndStaticColumnsMask = regularAndStaticColumnsMask(this); - this.staticColumnsMask = staticColumnsMask(this); - this.trackLts = trackLts; - } - - - - public static BitSet allColumnsMask(SchemaSpec schema) - { - return BitSet.allSet(schema.allColumns.size()); - } - - public BitSet regularColumnsMask() - { - return this.regularColumnsMask; - } - - public BitSet regularAndStaticColumnsMask() - { - return this.regularAndStaticColumnsMask; - } - - public BitSet staticColumnsMask() - { - return this.staticColumnsMask; - } - - private static BitSet regularColumnsMask(SchemaSpec schema) - { - BitSet mask = BitSet.allUnset(schema.allColumns.size()); - for (int i = 0; i < schema.regularColumns.size(); i++) - mask.set(schema.regularColumnsOffset + i); - return mask; - } - - private static BitSet regularAndStaticColumnsMask(SchemaSpec schema) - { - BitSet mask = BitSet.allUnset(schema.allColumns.size()); - for (int i = 0; i < schema.staticColumns.size() + schema.regularColumns.size(); i++) - mask.set(schema.staticColumnsOffset + i); - return mask; - } - - private static BitSet staticColumnsMask(SchemaSpec schema) - { - BitSet mask = BitSet.allUnset(schema.allColumns.size()); - for (int i = 0; i < schema.staticColumns.size(); i++) - mask.set(schema.staticColumnsOffset + i); - return mask; - } - - public void validate() - { - assert pkGenerator.byteSize() == Long.BYTES : partitionKeys.toString(); - } - - public interface AddRelationCallback - { - void accept(ColumnSpec spec, Relation.RelationKind kind, Object value); - } - - public void inflateRelations(long pd, - List clusteringRelations, - AddRelationCallback consumer) - { - Object[] pk = inflatePartitionKey(pd); - for (int i = 0; i < pk.length; i++) - consumer.accept(partitionKeys.get(i), Relation.RelationKind.EQ, pk[i]); - - inflateRelations(clusteringRelations, consumer); - } - - public void inflateRelations(List clusteringRelations, - AddRelationCallback consumer) - { - for (Relation r : clusteringRelations) - consumer.accept(r.columnSpec, r.kind, r.value()); - } - - public Object[] inflatePartitionKey(long pd) - { - return pkGenerator.inflate(pd); - } - - public Object[] inflateClusteringKey(long cd) - { - return ckGenerator.inflate(cd); - } - - public Object[] inflateRegularColumns(long[] vds) - { - return DataGenerators.inflateData(regularColumns, vds); - } - - public Object[] inflateStaticColumns(long[] sds) - { - return DataGenerators.inflateData(staticColumns, sds); - } - - public long adjustPdEntropy(long descriptor) - { - return pkGenerator.adjustEntropyDomain(descriptor); - } - - public long adjustCdEntropy(long descriptor) - { - return ckGenerator.adjustEntropyDomain(descriptor); - } - - public long deflatePartitionKey(Object[] pk) - { - return pkGenerator.deflate(pk); - } - - public long deflateClusteringKey(Object[] ck) - { - return ckGenerator.deflate(ck); - } - - public long[] deflateStaticColumns(Object[] statics) - { - return DataGenerators.deflateData(staticColumns, statics); - } - - public long[] deflateRegularColumns(Object[] regulars) - { - return DataGenerators.deflateData(regularColumns, regulars); - } - - public CompiledStatement compile() - { - StringBuilder sb = new StringBuilder(); - - sb.append("CREATE TABLE IF NOT EXISTS "); - sb.append(keyspace) - .append(".") - .append(table) - .append(" ("); - - SeparatorAppender commaAppender = new SeparatorAppender(); - for (ColumnSpec cd : partitionKeys) - { - commaAppender.accept(sb); - sb.append(cd.toCQL()); - if (partitionKeys.size() == 1 && clusteringKeys.size() == 0) - sb.append(" PRIMARY KEY"); - } - - for (ColumnSpec cd : concat(clusteringKeys, - staticColumns, - regularColumns)) - { - commaAppender.accept(sb); - sb.append(cd.toCQL()); - } - - if (clusteringKeys.size() > 0 || partitionKeys.size() > 1) - { - sb.append(", ").append(getPrimaryKeyCql()); - } - - if (trackLts) - sb.append(", ").append("visited_lts list static"); - - sb.append(')'); - - Runnable appendWith = doOnce(() -> sb.append(" WITH")); - - if (isCompactStorage) - { - appendWith.run(); - sb.append(" COMPACT STORAGE AND"); - } - - if (disableReadRepair) - { - appendWith.run(); - sb.append(" read_repair = 'NONE' AND"); - } - - if (compactionStrategy != null) - { - appendWith.run(); - sb.append(" compaction = {'class': '").append(compactionStrategy).append("'} AND"); - } - - if (clusteringKeys.size() > 0) - { - appendWith.run(); - sb.append(getClusteringOrderCql()) - .append(';'); - } - - return new CompiledStatement(sb.toString()); - } - - private String getClusteringOrderCql() - { - StringBuilder sb = new StringBuilder(); - if (clusteringKeys.size() > 0) - { - sb.append(" CLUSTERING ORDER BY ("); - - SeparatorAppender commaAppender = new SeparatorAppender(); - for (ColumnSpec column : clusteringKeys) - { - commaAppender.accept(sb); - sb.append(column.name).append(' ').append(column.isReversed() ? "DESC" : "ASC"); - } - - sb.append(")"); - } - - return sb.toString(); - } - - private String getPrimaryKeyCql() - { - StringBuilder sb = new StringBuilder(); - sb.append("PRIMARY KEY ("); - if (partitionKeys.size() > 1) - { - sb.append('('); - SeparatorAppender commaAppender = new SeparatorAppender(); - for (ColumnSpec cd : partitionKeys) - { - commaAppender.accept(sb); - sb.append(cd.name); - } - sb.append(')'); - } - else - { - sb.append(partitionKeys.get(0).name); - } - - for (ColumnSpec cd : clusteringKeys) - sb.append(", ").append(cd.name); - - return sb.append(')').toString(); - } - - public String toString() - { - return String.format("schema {cql=%s, columns=%s}", compile().toString(), allColumns); - } - - private static Runnable doOnce(Runnable r) - { - return new Runnable() - { - boolean executed = false; - - public void run() - { - if (executed) - return; - - executed = true; - r.run(); - } - }; - } - - public static class SeparatorAppender implements Consumer - { - boolean isFirst = true; - private final String separator; - - public SeparatorAppender() - { - this(","); - } - - public SeparatorAppender(String separator) - { - this.separator = separator; - } - - public void accept(StringBuilder stringBuilder) - { - if (isFirst) - isFirst = false; - else - stringBuilder.append(separator); - } - - public void accept(StringBuilder stringBuilder, String s) - { - accept(stringBuilder); - stringBuilder.append(s); - } - - - public void reset() - { - isFirst = true; - } - } - - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SchemaSpec that = (SchemaSpec) o; - return Objects.equals(keyspace, that.keyspace) && - Objects.equals(table, that.table) && - Objects.equals(partitionKeys, that.partitionKeys) && - Objects.equals(clusteringKeys, that.clusteringKeys) && - Objects.equals(regularColumns, that.regularColumns); - } - - public int hashCode() - { - return Objects.hash(keyspace, table, partitionKeys, clusteringKeys, regularColumns); - } - - public static Iterable concat(Iterable... iterables) - { - assert iterables != null && iterables.length > 0; - if (iterables.length == 1) - return iterables[0]; - - return () -> { - return new Iterator() - { - int idx; - Iterator current; - boolean hasNext; - - { - idx = 0; - prepareNext(); - } - - private void prepareNext() - { - if (current != null && current.hasNext()) - { - hasNext = true; - return; - } - - while (idx < iterables.length) - { - current = iterables[idx].iterator(); - idx++; - if (current.hasNext()) - { - hasNext = true; - return; - } - } - - hasNext = false; - } - - public boolean hasNext() - { - return hasNext; - } - - public T next() - { - T next = current.next(); - prepareNext(); - return next; - } - }; - }; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/BatchOperationBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/BatchOperationBuilder.java deleted file mode 100644 index 50321fb398..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/BatchOperationBuilder.java +++ /dev/null @@ -1,32 +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.harry.dsl; - -public interface BatchOperationBuilder -{ - SingleOperationBuilder beginBatch(); - - /** - * Begin batch for a partition descriptor at a specific index. - * - * Imagine all partition descriptors were longs in an array. Index of a descriptor - * is a sequential number of the descriptor in this imaginary array. - */ - SingleOperationBuilder beginBatch(long pdIdx); -} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/BatchVisitBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/BatchVisitBuilder.java deleted file mode 100644 index 7de5ccf31e..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/BatchVisitBuilder.java +++ /dev/null @@ -1,139 +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.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, - PartitionVisitStateImpl partitionState, - long lts, - OpSelectors.PureRng rng, - OpSelectors.DescriptorSelector descriptorSelector, - SchemaSpec schema, - ValueHelper valueHelper, - Consumer appendToLog) - { - super(partitionState, lts, rng, descriptorSelector, schema, valueHelper, 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[] valueIdxs) - { - super.insert(rowIdx, valueIdxs); - return this; - } - - @Override - public BatchVisitBuilder deletePartition() - { - super.deletePartition(); - return this; - } - - @Override - public BatchVisitBuilder deleteRow() - { - super.deleteRow(); - return this; - } - - @Override - public BatchVisitBuilder deleteColumns() - { - super.deleteColumns(); - return this; - } - - @Override - public BatchVisitBuilder deleteRowRange() - { - super.deleteRowRange(); - return this; - } - - @Override - public BatchVisitBuilder deleteRowRange(int lowBoundRowIdx, int highBoundRowIdx, boolean isMinEq, boolean isMaxEq) - { - super.deleteRowRange(lowBoundRowIdx, highBoundRowIdx, isMinEq, isMaxEq); - return this; - } - - @Override - public BatchVisitBuilder deleteRowSlice() - { - super.deleteRowSlice(); - return this; - } - - // TODO: prevent from closing more than once - public HistoryBuilder endBatch() - { - super.end(); - return this.historyBuilder; - } - - /** - * Implements closeable to instruct users to end batch before using. - * - * Non-finished batches are _not_ appended to the history and will appear as gaps in history. - */ - @Override - public void close() - { - endBatch(); - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java index a5d5fdafe2..35631d55a0 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java @@ -1,191 +1,84 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Comparator; 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.Consumer; -import java.util.function.Function; -import java.util.function.LongSupplier; +import java.util.stream.Collectors; -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.ColumnSpec; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.Bijections; import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.IndexGenerators; +import org.apache.cassandra.harry.gen.InvertibleGenerator; +import org.apache.cassandra.harry.gen.ValueGenerators; import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.NoOpChecker; -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; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.util.IteratorsUtil; -/** - * 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. - * - * HistoryBuilder also allows hardcoding/overriding clustering keys, regular, and static values, but _not_ for - * partition keys as of now. - * - * Since clusterings are ordered according to their value, it is only possible to instruct generator to ensure - * such value is going to be present. This is done by: - * - * history.forPartition(1).ensureClustering(new Object[]{ "", "b", -1L, "c", "d" }); - * - * For regular and static columns, overrides are done on the top level, not per-partition, so you can simply do: - * - * history.valueOverrides().override(column.name, 1245, "overriden value"); - * - * history.visitPartition(1) - * .insert(1, new long[]{ 12345, 12345 }); - * - * This will insert "overriden value" for the 1st row of 1st partition, for two columns. In other words, the index - * 12345 will now be associated with this overriden value. But all other / random values will still be, random. - */ -public class HistoryBuilder implements Iterable, SingleOperationBuilder, BatchOperationBuilder +import static org.apache.cassandra.harry.SchemaSpec.cumulativeEntropy; +import static org.apache.cassandra.harry.SchemaSpec.forKeys; +import static org.apache.cassandra.harry.gen.InvertibleGenerator.fromType; + +// TODO: either create or replay timestamps out of order +public class HistoryBuilder implements SingleOperationBuilder, Model.Replay { - protected final OverridingCkGenerator ckGenerator; + protected final IndexedValueGenerators valueGenerators; + protected final IndexGenerators indexGenerators; - protected final SchemaSpec schema; - protected final TokenPlacementModel.ReplicationFactor rf; + protected int nextOpIdx = 0; - protected final OpSelectors.PureRng pureRng; - protected final OpSelectors.DescriptorSelector descriptorSelector; - protected final ValueHelper valueHelper; // TODO: would be great to have a very simple B-Tree here - protected final Map log; + protected final Map log; - // TODO: primitive array with a custom/noncopying growth strat - protected final Map partitionStates = new HashMap<>(); - protected final Set visitedPartitions = new HashSet<>(); - - /** - * A selector that is going to be used by the model checker. - */ - protected 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) + public static HistoryBuilder fromSchema(SchemaSpec schemaSpec, long seed, int population) + { + IndexedValueGenerators generators = valueGenerators(schemaSpec, seed, population); + return new HistoryBuilder(generators, IndexGenerators.withDefaults(generators)); + } + + public HistoryBuilder(ValueGenerators generators) + { + this((IndexedValueGenerators) generators, IndexGenerators.withDefaults(generators)); + } + + public HistoryBuilder(IndexedValueGenerators valueGenerators, + IndexGenerators indexGenerators) { - this.maxPartitionSize = maxPartitionSize; this.log = new HashMap<>(); - this.pureRng = new OpSelectors.PCGFast(seed); - - this.presetSelector = new PresetPdSelector(); - this.ckGenerator = OverridingCkGenerator.make(schema.ckGenerator); - this.valueHelper = new ValueHelper(schema, pureRng); - this.schema = schema.withCkGenerator(this.ckGenerator, this.ckGenerator.columns) - .withColumns(valueHelper.regularColumns, valueHelper.staticColumns); - this.rf = rf; - - // TODO: make clock pluggable - this.clock = new OffsetClock(ApproximateClock.START_VALUE, - interleaveWindowSize, - new JdkRandomEntropySource(seed)); - - 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); + this.valueGenerators = valueGenerators; + this.indexGenerators = indexGenerators; } - public ValueOverrides valueOverrides() + public IndexedValueGenerators valueGenerators() { - return valueHelper; - } - - public SchemaSpec schema() - { - return schema; + return valueGenerators; } public int size() @@ -193,483 +86,356 @@ public class HistoryBuilder implements Iterable, SingleO return log.size(); } - public OpSelectors.Clock clock() + @Override + public Iterator iterator() { - return clock; - } + return new Iterator<>() + { + long replayed = 0; - /** - * Visited partition descriptors _not_ in the order they were visited - */ - public List visitedPds() - { - return new ArrayList<>(visitedPartitions); + public boolean hasNext() + { + return replayed < nextOpIdx; + } + + public Visit next() + { + return log.get(replayed++); + } + }; } @Override - public Iterator iterator() + public Visit replay(long lts) { - return log.values().iterator(); - } - - protected SingleOperationVisitBuilder singleOpVisitBuilder() - { - long visitLts = clock.nextLts(); - return singleOpVisitBuilder(defaultSelector.pd(visitLts, schema), visitLts, (ps) -> {}); - } - - protected SingleOperationVisitBuilder singleOpVisitBuilder(long pd, long lts, Consumer setupPs) - { - PartitionVisitStateImpl partitionState = presetSelector.register(lts, pd, setupPs); - return new SingleOperationVisitBuilder(partitionState, lts, pureRng, descriptorSelector, schema, valueHelper, (visit) -> { - visitedPartitions.add(pd); - log.put(visit.lts, visit); - }); + return log.get(lts); } @Override - public HistoryBuilder insert() + public Operations.Operation replay(long lts, int opId) + { + return replay(lts).operations[opId]; + } + + SingleOperationVisitBuilder singleOpVisitBuilder() + { + long visitLts = nextOpIdx++; + return new SingleOperationVisitBuilder(visitLts, + valueGenerators, + indexGenerators, + (visit) -> log.put(visit.lts, visit)); + } + + ; + + public MultiOperationVisitBuilder multistep() + { + long visitLts = nextOpIdx++; + return new MultiOperationVisitBuilder(visitLts, + valueGenerators, + indexGenerators, + visit -> log.put(visit.lts, visit)); + } + + @Override + public SingleOperationBuilder custom(Runnable runnable, String tag) + { + singleOpVisitBuilder().custom(runnable, tag); + return this; + } + + @Override + public SingleOperationBuilder custom(OperationFactory factory) + { + singleOpVisitBuilder().custom(factory); + return this; + } + + @Override + public SingleOperationBuilder update() + { + singleOpVisitBuilder().update(); + return this; + } + + @Override + public SingleOperationBuilder update(int pdIdx) + { + singleOpVisitBuilder().update(pdIdx); + return this; + } + + @Override + public SingleOperationBuilder update(int pdIdx, int cdIdx) + { + singleOpVisitBuilder().update(pdIdx, cdIdx); + return this; + } + + @Override + public SingleOperationBuilder update(int pdIdx, int rowIdx, int[] valueIdxs, int[] sValueIdxs) + { + singleOpVisitBuilder().update(pdIdx, rowIdx, valueIdxs, sValueIdxs); + return this; + } + + @Override + public SingleOperationBuilder insert() { singleOpVisitBuilder().insert(); return this; } @Override - public HistoryBuilder insert(int rowId) + public SingleOperationBuilder insert(int pdIdx) { - singleOpVisitBuilder().insert(rowId); + singleOpVisitBuilder().insert(pdIdx); return this; } @Override - public HistoryBuilder insert(int rowId, long[] valueIdxs) + public SingleOperationBuilder insert(int pdIdx, int cdIdx) { - singleOpVisitBuilder().insert(rowId, valueIdxs); - return this; - } - - public SingleOperationBuilder insert(int rowIdx, long[] valueIdxs, long[] sValueIdxs) - { - singleOpVisitBuilder().insert(rowIdx, valueIdxs, sValueIdxs); + singleOpVisitBuilder().insert(pdIdx, cdIdx); return this; } @Override - public HistoryBuilder deletePartition() + public SingleOperationBuilder insert(int pdIdx, int rowIdx, int[] valueIdxs, int[] sValueIdxs) { - singleOpVisitBuilder().deletePartition(); + singleOpVisitBuilder().insert(pdIdx, rowIdx, valueIdxs, sValueIdxs); return this; } @Override - public HistoryBuilder deleteRow() + public SingleOperationBuilder deleteRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, + int nonEqFrom, boolean includeLowerBound, boolean includeUpperBound) { - singleOpVisitBuilder().deleteRow(); + singleOpVisitBuilder().deleteRowRange(pdIdx, lowerBoundRowIdx, upperBoundRowIdx, + nonEqFrom, includeLowerBound, includeUpperBound); return this; } @Override - public HistoryBuilder deleteRow(int rowIdx) + public SingleOperationBuilder select(int pdIdx, IdxRelation[] ckRelations, IdxRelation[] regularRelations, IdxRelation[] staticRelations) { - singleOpVisitBuilder().deleteRow(rowIdx); + singleOpVisitBuilder().select(pdIdx, ckRelations, regularRelations, staticRelations); return this; } @Override - public HistoryBuilder deleteColumns() + public SingleOperationBuilder selectRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, + int nonEqFrom, boolean includeLowerBound, boolean includeUpperBound) { - singleOpVisitBuilder().deleteColumns(); + singleOpVisitBuilder().selectRowRange(pdIdx, lowerBoundRowIdx, upperBoundRowIdx, + nonEqFrom, includeLowerBound, includeUpperBound); return this; } @Override - public HistoryBuilder deleteRowRange() + public SingleOperationBuilder selectPartition(int pdIdx) { - singleOpVisitBuilder().deleteRowRange(); + singleOpVisitBuilder().selectPartition(pdIdx); return this; } @Override - public HistoryBuilder deleteRowRange(int lowBoundRowIdx, int highBoundRowIdx, boolean isMinEq, boolean isMaxEq) + public SingleOperationBuilder selectPartition(int pdIdx, Operations.ClusteringOrderBy orderBy) { - singleOpVisitBuilder().deleteRowRange(lowBoundRowIdx, highBoundRowIdx, isMinEq, isMaxEq); + singleOpVisitBuilder().selectPartition(pdIdx, orderBy); return this; } @Override - public HistoryBuilder deleteRowSlice() + public SingleOperationBuilder selectRow(int pdIdx, int rowIdx) { - singleOpVisitBuilder().deleteRowSlice(); + singleOpVisitBuilder().selectRow(pdIdx, rowIdx); return this; } @Override - public BatchVisitBuilder beginBatch() + public SingleOperationBuilder selectRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean isEq) { - long visitLts = clock.nextLts(); - return batchVisitBuilder(defaultSelector.pd(visitLts, schema), visitLts); + singleOpVisitBuilder().selectRowSliceByLowerBound(pdIdx, lowerBoundRowIdx, nonEqFrom, isEq); + return this; } + @Override + public SingleOperationBuilder selectRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean isEq) + { + singleOpVisitBuilder().selectRowSliceByUpperBound(pdIdx, upperBoundRowIdx, nonEqFrom, isEq); + return this; + } + + @Override + public SingleOperationBuilder deletePartition(int pdIdx) + { + singleOpVisitBuilder().deletePartition(pdIdx); + return this; + } + + @Override + public SingleOperationBuilder deleteRow(int pdIdx, int rowIdx) + { + singleOpVisitBuilder().deleteRow(pdIdx, rowIdx); + return this; + } + + @Override + public SingleOperationBuilder deleteColumns(int pdIdx, int rowIdx, BitSet regularSelection, BitSet staticSelection) + { + singleOpVisitBuilder().deleteRow(pdIdx, rowIdx); + return this; + } + + @Override + public SingleOperationBuilder deleteRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean isEq) + { + singleOpVisitBuilder().deleteRowSliceByLowerBound(pdIdx, lowerBoundRowIdx, nonEqFrom, isEq); + return this; + } + + @Override + public SingleOperationBuilder deleteRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean isEq) + { + singleOpVisitBuilder().deleteRowSliceByUpperBound(pdIdx, upperBoundRowIdx, nonEqFrom, isEq); + return this; + } + + /** - * 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. + * Indexed bijection allows to decouple descriptor order from value order, which makes data generation simpler. + *

+ * For regular Harry bijections, this is done at no cost, since values are inflated in a way that preserves + * descriptor order, which means that idx order is consistent with descriptor order and consistent with value order. + *

+ * An indexed bijection allows order to be established via index, and use descriptor simply as a seed for random values. */ - @Override - public BatchVisitBuilder beginBatch(long pdIdx) + public interface IndexedBijection extends Bijections.Bijection { - long visitLts = clock.nextLts(); - return batchVisitBuilder(presetSelector.pdAtPosition(pdIdx), visitLts); - } + int idxFor(long descriptor); - protected BatchVisitBuilder batchVisitBuilder(long pd, long lts) - { - PartitionVisitStateImpl partitionState = presetSelector.register(lts, pd, (ps) -> {}); - return new BatchVisitBuilder(this, partitionState, lts, pureRng, descriptorSelector, schema, valueHelper, (visit) -> { - visitedPartitions.add(pd); - log.put(visit.lts, visit); - }); - } + long descriptorAt(int idx); - public SingleOperationBuilder visitPartition(long pdIdx) - { - long visitLts = clock.nextLts(); - long pd = presetSelector.pdAtPosition(pdIdx); - return singleOpVisitBuilder(pd, visitLts, (ps) -> {}); - } - - public SingleOperationBuilder visitPartition(long pdIdx, Consumer setupPs) - { - long visitLts = clock.nextLts(); - long pd = presetSelector.pdAtPosition(pdIdx); - return singleOpVisitBuilder(pd, visitLts, setupPs); - } - - public PartitionVisitState forPartition(long pdIdx) - { - long pd = defaultSelector.pdAtPosition(pdIdx, schema); - return partitionStates.computeIfAbsent(pd, (pd_) -> makePartitionVisitState(pd)); - } - - private PartitionVisitStateImpl makePartitionVisitState(long pd) - { - Long[] possibleCds = new Long[maxPartitionSize]; - for (int cdIdx = 0; cdIdx < possibleCds.length; cdIdx++) + @Override + default String toString(long pd) { - long cd = descriptorSelector.cd(pd, 0, cdIdx, schema); - possibleCds[cdIdx] = cd; + if (pd == MagicConstants.UNSET_DESCR) + return Integer.toString(MagicConstants.UNSET_IDX); + + if (pd == MagicConstants.NIL_DESCR) + return Integer.toString(MagicConstants.NIL_IDX); + + return Integer.toString(idxFor(pd)); } - Arrays.sort(possibleCds, Long::compare); - - long[] primitiveArray = new long[maxPartitionSize]; - for (int i = 0; i < possibleCds.length; i++) - primitiveArray[i] = possibleCds[i]; - - // TODO: can we have something more efficient than a tree set here? - return new PartitionVisitStateImpl(pd, primitiveArray, new TreeSet<>(), schema); } - public PresetPdSelector pdSelector() + public static IndexedValueGenerators valueGenerators(SchemaSpec schema, long seed) { - return presetSelector; + return valueGenerators(schema, seed, 1000); } - /** - * 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 + @SuppressWarnings({ "unchecked" }) + public static IndexedValueGenerators valueGenerators(SchemaSpec schema, long seed, int populationPerColumn) { - // TODO: implement a primitive long map? - private final Map ltsToPd = new HashMap<>(); + List> pkComparators = new ArrayList<>(); + List> ckComparators = new ArrayList<>(); + List> regularComparators = new ArrayList<>(); + List> staticComparators = new ArrayList<>(); - public PartitionVisitStateImpl register(long lts, long pd, Consumer setup) + EntropySource rng = new JdkRandomEntropySource(seed); + for (int i = 0; i < schema.partitionKeys.size(); i++) + pkComparators.add((Comparator) schema.partitionKeys.get(i).type.comparator()); + for (int i = 0; i < schema.clusteringKeys.size(); i++) + ckComparators.add((Comparator) schema.clusteringKeys.get(i).type.comparator()); + for (int i = 0; i < schema.regularColumns.size(); i++) + regularComparators.add((Comparator) schema.regularColumns.get(i).type.comparator()); + for (int i = 0; i < schema.staticColumns.size(); i++) + staticComparators.add((Comparator) schema.staticColumns.get(i).type.comparator()); + + Map, InvertibleGenerator> map = new HashMap<>(); + for (ColumnSpec column : IteratorsUtil.concat(schema.regularColumns, schema.staticColumns)) + map.computeIfAbsent(column, (a) -> (InvertibleGenerator) fromType(rng, populationPerColumn, column)); + + // TODO: empty gen + return new IndexedValueGenerators(new InvertibleGenerator<>(rng, cumulativeEntropy(schema.partitionKeys), populationPerColumn, forKeys(schema.partitionKeys), keyComparator(schema.partitionKeys)), + new InvertibleGenerator<>(rng, cumulativeEntropy(schema.clusteringKeys), populationPerColumn, forKeys(schema.clusteringKeys), keyComparator(schema.clusteringKeys)), + schema.regularColumns.stream() + .map(map::get) + .collect(Collectors.toList()), + schema.staticColumns.stream() + .map(map::get) + .collect(Collectors.toList()), + pkComparators, + ckComparators, + regularComparators, + staticComparators); + } + + public static class IndexedValueGenerators extends ValueGenerators + { + public IndexedValueGenerators(IndexedBijection pkGen, + IndexedBijection ckGen, + List> regularColumnGens, + List> staticColumnGens, + List> pkComparators, + List> ckComparators, + List> regularComparators, + List> staticComparators) { - 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)); - - PartitionVisitStateImpl partitionState = partitionStates.computeIfAbsent(pd, (pd_) -> { - PartitionVisitStateImpl partitionVisitState = makePartitionVisitState(pd); - setup.accept(partitionVisitState); - return partitionVisitState; - }); - partitionState.visitedLts.add(lts); - return partitionState; + super(pkGen, ckGen, + (List>) (List) regularColumnGens, + (List>) (List) staticColumnGens, + pkComparators, ckComparators, regularComparators, staticComparators); } - protected long pd(long lts) + @Override + public IndexedBijection pkGen() { - return ltsToPd.get(lts); + return (IndexedBijection) super.pkGen(); } - public long nextLts(long lts) + @Override + public IndexedBijection ckGen() { - long pd = pd(lts); - PartitionVisitStateImpl partitionState = partitionStates.get(pd); - NavigableSet visitedLts = partitionState.visitedLts.subSet(lts, false, Long.MAX_VALUE, false); - if (visitedLts.isEmpty()) - return -1; + return (IndexedBijection) super.ckGen(); + } + + @Override + public IndexedBijection regularColumnGen(int idx) + { + return (IndexedBijection) super.regularColumnGen(idx); + } + + @Override + public IndexedBijection staticColumnGen(int idx) + { + return (IndexedBijection) super.staticColumnGen(idx); + } + } + + + private static Comparator keyComparator(List> columns) + { + return (o1, o2) -> compareKeys(columns, o1, o2); + } + + public static int compareKeys(List> columns, Object[] v1, Object[] v2) + { + assert v1.length == v2.length : String.format("Values should be of same length: %d != %d\n%s\n%s", + v1.length, v2.length, Arrays.toString(v1), Arrays.toString(v2)); + + for (int i = 0; i < v1.length; i++) + { + int res; + ColumnSpec column = columns.get(i); + if (column.type.isReversed()) + res = column.type.comparator().reversed().compare(v1[i], v2[i]); else - return visitedLts.first(); - } - - public long prevLts(long lts) - { - long pd = pd(lts); - PartitionVisitStateImpl partitionState = partitionStates.get(pd); - NavigableSet visitedLts = partitionState.visitedLts.descendingSet().subSet(lts, false, 0L, false); - if (visitedLts.isEmpty()) - return -1; - else - return visitedLts.first(); - } - - public long maxLtsFor(long pd) - { - PartitionVisitStateImpl partitionState = partitionStates.get(pd); - if (partitionState == null) - return -1; - return partitionState.visitedLts.last(); - } - - public long minLtsFor(long pd) - { - PartitionVisitStateImpl partitionState = partitionStates.get(pd); - if (partitionState == null) - return -1; - return partitionState.visitedLts.first(); - } - - public long pdAtPosition(long pdIdx) - { - return defaultSelector.pdAtPosition(pdIdx, schema); - } - - 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; + res = column.type.comparator().compare(v1[i], v2[i]); + if (res != 0) + return res; } + 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 noOpChecker(SystemUnderTest.ConsistencyLevel cl, SystemUnderTest sut) - { - return new NoOpChecker(sut, 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.selectAllColumns(schema, pd, false)); - model.validate(Query.selectAllColumns(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.selectAllColumns(schema, pd, false)); - model.validate(Query.selectAllColumns(schema, pd, true)); - } - } - - public void validateAll(Model model, Function> queries) - { - for (Long pd : partitionStates.keySet()) - { - for (Query query : queries.apply(pd)) - model.validate(query); - } - } - - public ReplayingVisitor visitor(VisitExecutor executor) - { - LongIterator replay = clock.replayAll(); - return new ReplayingVisitor(executor, replay) - { - public Visit getVisit(long lts) - { - long idx = lts - clock.base; - Visit visit = log.get(idx); - assert visit != null : String.format("Could not find a visit for LTS %d", lts); - return visit; - } - - public void replayAll() - { - while (replay.hasNext()) - visit(); - } - }; - } - - public interface LongIterator extends LongSupplier - { - boolean hasNext(); - long getAsLong(); - } - - /** - * Non-monotonic version of OffsetClock. - */ - public class OffsetClock implements OpSelectors.Clock - { - private long lowerBound; - private long current; - - private final long base; - private final long batchSize; - private final Set returned; - private final EntropySource entropySource; - - private final List visitOrder; - - public OffsetClock(long base, long batchSize, EntropySource entropySource) - { - this.lowerBound = base; - this.base = base; - this.batchSize = batchSize; - this.returned = new HashSet<>(); - this.entropySource = entropySource; - this.visitOrder = new ArrayList<>(); - this.current = computeNext(); - } - - /** - * Visit Order - related methods - */ - public LongIterator replayAll() - { - return new LongIterator() - { - private int visitedUpTo; - - public boolean hasNext() - { - return visitedUpTo < visitOrder.size(); - } - - public long getAsLong() - { - return visitOrder.get(visitedUpTo++); - } - }; - } - - private long computeNext() - { - if (returned.size() == batchSize) - { - returned.clear(); - lowerBound += batchSize; - } - - long generated = entropySource.nextLong(lowerBound, lowerBound + batchSize); - while (returned.contains(generated)) - generated = entropySource.nextLong(lowerBound, lowerBound + batchSize); - - returned.add(generated); - return generated; - } - - @Override - public long rts(long lts) - { - return base + lts; - } - - @Override - public long lts(long rts) - { - return rts - base; - } - - - @Override - public long nextLts() - { - long ret = current; - current = computeNext(); - visitOrder.add(ret); - return ret; - } - - public long peek() - { - return current; - } - - public Configuration.ClockConfiguration toConfig() - { - throw new RuntimeException("Not implemented"); - } - } -} +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilderHelper.java b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilderHelper.java new file mode 100644 index 0000000000..281f62cc87 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilderHelper.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.util.BitSet; + +import static org.apache.cassandra.harry.Relations.RelationKind.EQ; +import static org.apache.cassandra.harry.Relations.RelationKind.GT; +import static org.apache.cassandra.harry.Relations.RelationKind.GTE; +import static org.apache.cassandra.harry.Relations.RelationKind.LT; + +/** + * Things that seemed like a good idea, but ultimately were not a good fit for the HistoryBuilder API + */ +public class HistoryBuilderHelper +{ + /** + * Perform a random insert to any row + */ + public static void insertRandomData(SchemaSpec schema, Generator pkGen, Generator ckGen, EntropySource rng, HistoryBuilder history) + { + insertRandomData(schema, pkGen.generate(rng), ckGen.generate(rng), rng, history); + } + + public static void insertRandomData(SchemaSpec schema, int partitionIdx, int rowIdx, EntropySource rng, HistoryBuilder history) + { + int[] vIdxs = new int[schema.regularColumns.size()]; + for (int i = 0; i < schema.regularColumns.size(); i++) + vIdxs[i] = rng.nextInt(history.valueGenerators().regularPopulation(i)); + int[] sIdxs = new int[schema.staticColumns.size()]; + for (int i = 0; i < schema.staticColumns.size(); i++) + sIdxs[i] = rng.nextInt(history.valueGenerators().staticPopulation(i)); + history.insert(partitionIdx, rowIdx, vIdxs, sIdxs); + } + + public static void insertRandomData(SchemaSpec schema, int pkIdx, EntropySource rng, HistoryBuilder history) + { + insertRandomData(schema, + pkIdx, + rng.nextInt(0, history.valueGenerators().ckPopulation()), + rng, + 0, + history); + } + + public static void insertRandomData(SchemaSpec schema, int partitionIdx, int rowIdx, EntropySource rng, double chanceOfUnset, HistoryBuilder history) + { + int[] vIdxs = new int[schema.regularColumns.size()]; + for (int i = 0; i < schema.regularColumns.size(); i++) + vIdxs[i] = rng.nextDouble() <= chanceOfUnset ? MagicConstants.UNSET_IDX : rng.nextInt(history.valueGenerators().regularPopulation(i)); + int[] sIdxs = new int[schema.staticColumns.size()]; + for (int i = 0; i < schema.staticColumns.size(); i++) + sIdxs[i] = rng.nextDouble() <= chanceOfUnset ? MagicConstants.UNSET_IDX : rng.nextInt(history.valueGenerators().staticPopulation(i)); + history.insert(partitionIdx, rowIdx, vIdxs, sIdxs); + } + + + public static void deleteRandomColumns(SchemaSpec schema, int partitionIdx, int rowIdx, EntropySource rng, SingleOperationBuilder history) + { + Generator regularMask = Generators.bitSet(schema.regularColumns.size()); + Generator staticMask = Generators.bitSet(schema.staticColumns.size()); + + history.deleteColumns(partitionIdx, + rowIdx, + regularMask.generate(rng), + staticMask.generate(rng)); + } + + private static final Generator relationKindGen = Generators.pick(LT, GT, EQ); + private static final Set lowBoundRelations = Set.of(GT, GTE, EQ); + + /** + * Generates random relations for regular and static columns for FILTERING and SAI queries. + * + * Will generate at most 2 relations per column: + * * generates a random relation + * * if this relation is EQ, that's the only relation that will lock this column + * * if relation is GT, next bound, if generated, will be LT + * * if relation is LT, next bound, if generated, will be GT + * + * @param rng - random number generator + * @param numColumns - number of columns in the generated set of relationships + * @param population - expected population / number of possible values for a given column + * @return a list of relations + */ + public static List generateValueRelations(EntropySource rng, int numColumns, Function population) + { + List relations = new ArrayList<>(); + Map> kindsMap = new HashMap<>(); + int remainingColumns = numColumns; + while (remainingColumns > 0) + { + int column = rng.nextInt(numColumns); + Set kinds = kindsMap.computeIfAbsent(column, c -> new HashSet<>()); + if (kinds.size() > 1 || kinds.contains(EQ)) + continue; + Relations.RelationKind kind; + if (kinds.size() == 1) + { + if (kinds.contains(LT)) kind = GT; + else kind = LT; + remainingColumns--; + } + else + // TODO: weights per relation? + kind = relationKindGen.generate(rng); + + if (kind == EQ) + remainingColumns--; + + kinds.add(kind); + + int regularIdx = rng.nextInt(population.apply(column)); + relations.add(new SingleOperationBuilder.IdxRelation(kind, regularIdx, column)); + if (rng.nextBoolean()) + break; + } + return relations; + } + + /** + * Generates random relations for regular and static columns for FILTERING and SAI queries. + * + * Will generate at most 2 relations per column. Low bound will always use values from low bound clustering, + * high bound will always use values from high bound. + * + * @param rng - random number generator + * @param numColumns - number of columns in the generated set of relationships + * @return a list of relations + */ + public static List generateClusteringRelations(EntropySource rng, int numColumns, Generator ckGen) + { + List relations = new ArrayList<>(); + Map> kindsMap = new HashMap<>(); + int remainingColumns = numColumns; + int lowBoundIdx = ckGen.generate(rng); + int highBoundIdx = ckGen.generate(rng); + while (remainingColumns > 0) + { + int column = rng.nextInt(numColumns); + Set kinds = kindsMap.computeIfAbsent(column, c -> new HashSet<>()); + if (kinds.size() > 1 || kinds.contains(EQ)) + continue; + Relations.RelationKind kind; + if (kinds.size() == 1) + { + if (kinds.contains(LT)) kind = GT; + else kind = LT; + remainingColumns--; + } + else + kind = relationKindGen.generate(rng); + + if (kind == EQ) + remainingColumns--; + + kinds.add(kind); + + relations.add(new SingleOperationBuilder.IdxRelation(kind, lowBoundRelations.contains(kind) ? lowBoundIdx : highBoundIdx, column)); + if (rng.nextBoolean()) + break; + } + return relations; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/MultiOperationVisitBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/MultiOperationVisitBuilder.java new file mode 100644 index 0000000000..7dbff3db2e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/MultiOperationVisitBuilder.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.dsl; + +import java.io.Closeable; +import java.util.function.Consumer; + +import org.apache.cassandra.harry.gen.IndexGenerators; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.util.BitSet; + +import static org.apache.cassandra.harry.dsl.HistoryBuilder.IndexedValueGenerators; + +public class MultiOperationVisitBuilder extends SingleOperationVisitBuilder implements Closeable +{ + MultiOperationVisitBuilder(long lts, IndexedValueGenerators valueGenerators, IndexGenerators indexGenerators, Consumer appendToLog) + { + super(lts, valueGenerators, indexGenerators, appendToLog); + } + + @Override + public MultiOperationVisitBuilder custom(Runnable runnable, String tag) + { + super.custom(runnable, tag); + return this; + } + + @Override + public MultiOperationVisitBuilder update(int pdIdx, int rowIdx, int[] valueIdxs, int[] sValueIdxs) + { + super.update(pdIdx, rowIdx, valueIdxs, sValueIdxs); + return this; + } + + @Override + public MultiOperationVisitBuilder insert(int partitionIdx, int rowIdx, int[] valueIdxs, int[] sValueIdxs) + { + super.insert(partitionIdx, rowIdx, valueIdxs, sValueIdxs); + return this; + } + + @Override + public MultiOperationVisitBuilder selectRowRange(int partitionIdx, int lowBoundRowIdx, int highBoundRowIdx, + int nonEqFrom, boolean includeLowerBound, boolean includeHighBound) + { + super.selectRowRange(partitionIdx, lowBoundRowIdx, highBoundRowIdx, nonEqFrom, includeLowerBound, includeHighBound); + return this; + } + + @Override + public MultiOperationVisitBuilder selectPartition(int partitionIdx) + { + super.selectPartition(partitionIdx); + return this; + } + + @Override + public MultiOperationVisitBuilder selectRow(int partitionIdx, int rowIdx) + { + super.selectRow(partitionIdx, rowIdx); + return this; + } + + @Override + public MultiOperationVisitBuilder selectRowSliceByLowerBound(int partitionIdx, int lowBoundRowIdx, int nonEqFrom, boolean isEq) + { + super.selectRowSliceByLowerBound(partitionIdx, lowBoundRowIdx, nonEqFrom, isEq); + return this; + } + + @Override + public MultiOperationVisitBuilder selectRowSliceByUpperBound(int partitionIdx, int highBoundRowIdx, int nonEqFrom, boolean isEq) + { + super.selectRowSliceByUpperBound(partitionIdx, highBoundRowIdx, nonEqFrom, isEq); + return this; + } + + @Override + public MultiOperationVisitBuilder deletePartition(int partitionIdx) + { + super.deletePartition(partitionIdx); + return this; + } + + @Override + public MultiOperationVisitBuilder deleteRow(int partitionIdx, int rowIdx) + { + super.deleteRow(partitionIdx, rowIdx); + return this; + } + + @Override + public MultiOperationVisitBuilder deleteColumns(int partitionIdx, int rowIdx, BitSet regularSelection, BitSet staticSelection) + { + super.deleteColumns(partitionIdx, rowIdx, regularSelection, staticSelection); + return this; + } + + @Override + public MultiOperationVisitBuilder deleteRowSliceByLowerBound(int partitionIdx, int lowBoundRowIdx, int nonEqFrom, boolean isEq) + { + super.deleteRowSliceByLowerBound(partitionIdx, lowBoundRowIdx, nonEqFrom, isEq); + return this; + } + + @Override + public MultiOperationVisitBuilder deleteRowSliceByUpperBound(int partitionIdx, int highBoundRowIdx, int nonEqFrom, boolean isEq) + { + super.deleteRowSliceByUpperBound(partitionIdx, highBoundRowIdx, nonEqFrom, isEq); + return this; + } + + @Override + public MultiOperationVisitBuilder deleteRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, + int nonEqFrom, boolean includeLowerBound, boolean includeUpperBound) + { + super.deleteRowRange(pdIdx, lowerBoundRowIdx, upperBoundRowIdx, + nonEqFrom, includeLowerBound, includeUpperBound); + return this; + } + + @Override + public MultiOperationVisitBuilder select(int partitionIdx, + IdxRelation[] ckRelations, + IdxRelation[] regularRelations, + IdxRelation[] staticRelations) + { + super.select(partitionIdx, ckRelations, regularRelations, staticRelations); + return this; + } + + @Override + Visit build() + { + throw new IllegalStateException("Shuold not be called directly, use auto-close"); + } + + Visit buildInternal() + { + return super.build(); + } + + + @Override + int size() + { + return super.size(); + } + + public void close() + { + buildInternal(); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/OverridingBijection.java b/test/harry/main/org/apache/cassandra/harry/dsl/OverridingBijection.java deleted file mode 100644 index fe645d2980..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/OverridingBijection.java +++ /dev/null @@ -1,84 +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.harry.dsl; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.cassandra.harry.gen.Bijections; - -class OverridingBijection implements Bijections.Bijection -{ - protected final Bijections.Bijection delegate; - protected final Map descriptorToValue; - protected final Map valueToDescriptor; - - public OverridingBijection(Bijections.Bijection delegate) - { - this.delegate = delegate; - descriptorToValue = new HashMap<>(); - valueToDescriptor = new HashMap<>(); - } - - public void override(long descriptor, T value) - { - T old = descriptorToValue.get(descriptor); - if (old != null) - throw new IllegalStateException(String.format("Can't override %d twice. Was already overriden to %s", descriptor, old)); - - T orig = delegate.inflate(descriptor); - if (!orig.equals(value)) - { - descriptorToValue.put(descriptor, value); - valueToDescriptor.put(value, descriptor); - } - } - - @Override - public T inflate(long descriptor) - { - Object v = descriptorToValue.get(descriptor); - if (v != null) - { - return (T) v; - } - return delegate.inflate(descriptor); - } - - @Override - public long deflate(T value) - { - Long descriptor = valueToDescriptor.get(value); - if (descriptor != null) - return descriptor; - return delegate.deflate(value); - } - - @Override - public int byteSize() - { - return delegate.byteSize(); - } - - @Override - public int compare(long l, long r) - { - return delegate.compare(l, r); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/OverridingCkGenerator.java b/test/harry/main/org/apache/cassandra/harry/dsl/OverridingCkGenerator.java deleted file mode 100644 index 92aa00f695..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/OverridingCkGenerator.java +++ /dev/null @@ -1,153 +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.harry.dsl; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.gen.Bijections; -import org.apache.cassandra.harry.gen.DataGenerators; - -/** - * A class that helps to override parts of clustering key. The tricky part about CK overrides is that Harry model makes - * an assumption about the ordering of clustering keys, which means clusterings have to be sorted in the same way their - * descriptors are. This, combined with reverse types, makes managing this state somewhat tricky at times. - * - * Additionally, Relation in delete/select query receives individual CK descriptors (i.e. after they have been sliced), - * while most other queries usually operate on inflated clustering key. All of this is required for efficient _stateless_ - * validation, but makes overrides a bit less intuitive. - * - * To summarise: overrides for inflating are done for individual clustering key columns. Overrides for deflating a clustering - * operate on an entire key. Main reason for this is to allow having same string in several rows for the same column. - */ - -@SuppressWarnings({"rawtypes", "unchecked"}) -public class OverridingCkGenerator extends DataGenerators.KeyGenerator -{ - private final DataGenerators.KeyGenerator delegate; - private final KeyPartOverride[] columnValueOverrides; - private final List> columnSpecOverrides; - private final Map valueToDescriptor; - - // Had to be a static method because you can not call super after you have initialised any fields - public static OverridingCkGenerator make(DataGenerators.KeyGenerator delegate) - { - KeyPartOverride[] columnValueOverrides = new KeyPartOverride[delegate.columns.size()]; - List> columnSpecOverrides = new ArrayList<>(); - for (int i = 0; i < delegate.columns.size(); i++) - { - columnValueOverrides[i] = new KeyPartOverride<>(delegate.columns.get(i).generator()); - columnSpecOverrides.add(delegate.columns.get(i).override(columnValueOverrides[i])); - } - assert columnValueOverrides.length == columnSpecOverrides.size(); - return new OverridingCkGenerator(delegate, columnValueOverrides, columnSpecOverrides); - } - - private OverridingCkGenerator(DataGenerators.KeyGenerator delegate, - KeyPartOverride[] columnValueOverrides, - List> columnSpecOverrides) - { - super(columnSpecOverrides); - this.columnValueOverrides = columnValueOverrides; - this.columnSpecOverrides = columnSpecOverrides; - this.delegate = delegate; - this.valueToDescriptor = new HashMap<>(); - } - - public void override(long descriptor, Object[] value) - { - long[] slices = delegate.slice(descriptor); - for (int i = 0; i < slices.length; i++) - columnValueOverrides[i].override(slices[i], value[i]); - - // We _always_ deflate clustering key as a package, since we can not validate a clustering key without all components anyways. - valueToDescriptor.put(new ArrayWrapper(value), descriptor); - } - - @Override - public Object[] inflate(long descriptor) - { - return DataGenerators.inflateKey(columnSpecOverrides, descriptor, slice(descriptor)); - } - - @Override - public long deflate(Object[] value) - { - Long descriptor = valueToDescriptor.get(new ArrayWrapper(value)); - if (descriptor != null) - return descriptor; - - return delegate.deflate(value); - } - - @Override - public int byteSize() - { - return delegate.byteSize(); - } - - @Override - public int compare(long l, long r) - { - return delegate.byteSize(); - } - - @Override - public long[] slice(long descriptor) - { - return delegate.slice(descriptor); - } - - @Override - public long stitch(long[] parts) - { - return delegate.stitch(parts); - } - - @Override - public long minValue(int idx) - { - return delegate.minValue(idx); - } - - @Override - public long maxValue(int idx) - { - return delegate.maxValue(idx); - } - - public static class KeyPartOverride extends OverridingBijection - { - public KeyPartOverride(Bijections.Bijection delegate) - { - super(delegate); - } - - // We do not use deflate for key part overrides - @Override - public long deflate(T value) - { - throw new IllegalStateException(); - } - } - -} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitState.java b/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitState.java deleted file mode 100644 index 37558274b1..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitState.java +++ /dev/null @@ -1,40 +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.harry.dsl; - -public interface PartitionVisitState -{ - /** - * Informs a generator that a specific clustering key has to be generated. - * - * Since Harry model has a few constraints, we can not override to an arbitrary clustering to an arbitrary value, since - * Harry ensures that clustering descriptors are sorted the same way clusterings themselves are sorted. - * - * Gladly, most of the time we need to override just one or a couple of values to trigger some edge condition, - * which means that we can simply instruct Harry to produce a given handcrafted value. - * - * When using `ensureClustering`, you can not reliably know in advance where specifically in the row this value is - * going to sort. - * - * If you need arbitrary overrides, you will have to produce _all_ clusterings possible for the given partition. - */ - void ensureClustering(Object[] overrides); - void overrideClusterings(Object[][] overrides); - long pd(); -} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitStateImpl.java b/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitStateImpl.java deleted file mode 100644 index d61210515b..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitStateImpl.java +++ /dev/null @@ -1,115 +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.harry.dsl; - -import java.util.Arrays; -import java.util.NavigableSet; - -import org.apache.cassandra.harry.ddl.SchemaSpec; - -public class PartitionVisitStateImpl implements PartitionVisitState -{ - final long pd; - final long[] possibleCds; - final NavigableSet visitedLts; - final SchemaSpec schema; - private final OverridingCkGenerator ckGenerator; - - PartitionVisitStateImpl(long pd, long[] possibleCds, NavigableSet visitedLts, SchemaSpec schema) - { - this.pd = pd; - this.possibleCds = possibleCds; - this.visitedLts = visitedLts; - this.schema = schema; - this.ckGenerator = (OverridingCkGenerator) schema.ckGenerator; - } - - - /** - * Ensures that exactly one of the clustering keys will have given values. - */ - @Override - public void ensureClustering(Object[] overrides) - { - long cd = findCdForOverride(overrides); - ckGenerator.override(cd, overrides); - } - - @Override - public void overrideClusterings(Object[][] overrides) - { - assert possibleCds.length == overrides.length; - Arrays.sort(overrides, this::compareCds); - for (int i = 0; i < overrides.length; i++) - ckGenerator.override(possibleCds[i], overrides[i]); - } - - @Override - public long pd() - { - return pd; - } - - long findCdForOverride(Object[] ck) - { - int low = 0; - int high = possibleCds.length - 1; - - while (low <= high) - { - int mid = (low + high) >>> 1; - long midEl = possibleCds[mid]; - int cmp = compareCds(ck, midEl); - - if (cmp < 0) - low = mid + 1; - else if (cmp > 0) - high = mid - 1; - else - throw new IllegalStateException("This value is already present"); - } - - return possibleCds[Math.min(possibleCds.length - 1, low)]; - } - - private int compareCds(Object[] v1, long cd2) - { - Object[] v2 = schema.ckGenerator.inflate(cd2); - return compareCds(v1, v2); - } - - private int compareCds(Object[] v1, Object[] v2) - { - assert v1.length == v2.length : String.format("Values should be of same length: %d != %d\n%s\n%s", - v1.length, v2.length, Arrays.toString(v1), Arrays.toString(v2)); - - for (int i = 0; i < v1.length; i++) - { - int res = ((Comparable) v2[i]).compareTo(v1[i]); - if (res != 0) - { - if (schema.clusteringKeys.get(i).type.isReversed()) - res = res * -1; - - return res; - } - } - return 0; - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java index 60d8e4ea53..830c3ad4ef 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java @@ -1,120 +1,73 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Consumer; +import java.util.function.Function; -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; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.op.Visit; public class ReplayingHistoryBuilder extends HistoryBuilder { - private final ReplayingVisitor visitor; - private final SystemUnderTest sut; - public final DataTracker tracker; + private final CQLVisitExecutor executor; - public ReplayingHistoryBuilder(long seed, - int maxPartitionSize, - int interleaveWindowSize, - DataTracker tracker, - SystemUnderTest sut, - SchemaSpec schema, - TokenPlacementModel.ReplicationFactor rf, - SystemUnderTest.ConsistencyLevel writeCl) + public ReplayingHistoryBuilder(ValueGenerators generators, Function executorFactory) { - super(seed, maxPartitionSize, interleaveWindowSize, schema, rf); - this.visitor = visitor(tracker, sut, writeCl); - this.tracker = tracker; - this.sut = sut; - } + super((IndexedValueGenerators) generators); + this.executor = executorFactory.apply(this); + } - @Override - protected SingleOperationVisitBuilder singleOpVisitBuilder(long pd, long lts, Consumer setupPs) + SingleOperationVisitBuilder singleOpVisitBuilder() { - PartitionVisitStateImpl partitionState = presetSelector.register(lts, pd, setupPs); - return new SingleOperationVisitBuilder(partitionState, lts, pureRng, descriptorSelector, schema, valueHelper, (visit) -> { - log.put(lts, visit); - }) { + long visitLts = nextOpIdx++; + HistoryBuilder this_ = this; + return new SingleOperationVisitBuilder(visitLts, + valueGenerators, + indexGenerators, + (visit) -> log.put(visit.lts, visit)) { @Override - void end() + Visit build() { - super.end(); - visitor.replayAll(); + Visit visit = super.build(); + CQLVisitExecutor.executeVisit(visit, executor, this_); + return visit; } }; } @Override - public BatchVisitBuilder beginBatch() + public MultiOperationVisitBuilder multistep() { - 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) - { - PartitionVisitStateImpl partitionState = presetSelector.register(lts, pd, (ps) -> {}); - return new BatchVisitBuilder(this, partitionState, lts, pureRng, descriptorSelector, schema, valueHelper, (visit) -> { - log.put(lts, visit); - }) { + long visitLts = nextOpIdx++; + HistoryBuilder this_ = this; + return new MultiOperationVisitBuilder(visitLts, + valueGenerators, + indexGenerators, + visit -> log.put(visit.lts, visit)) { @Override - public HistoryBuilder endBatch() + Visit buildInternal() { - super.endBatch(); - visitor.replayAll(); - return ReplayingHistoryBuilder.this; + Visit visit = super.buildInternal(); + CQLVisitExecutor.executeVisit(visit, executor, this_); + return visit; } }; } - - 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); - } - -} +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java index a9b98b69ca..49a73268a3 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java @@ -1,52 +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. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Relations; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.util.ThrowingRunnable; + public interface SingleOperationBuilder { - /** - * Perform an insert operation to _some_ row - */ + default SingleOperationBuilder customThrowing(ThrowingRunnable runnable, String tag) + { + return custom(runnable.toRunnable(), tag); + } + SingleOperationBuilder custom(Runnable runnable, String tag); + SingleOperationBuilder custom(OperationFactory factory); + + SingleOperationBuilder update(); + SingleOperationBuilder update(int pdIdx); + SingleOperationBuilder update(int pdIdx, int cdIdx); + SingleOperationBuilder update(int pdIdx, int cdIdx, int[] valueIdxs, int[] sValueIdxs); + SingleOperationBuilder insert(); + SingleOperationBuilder insert(int pdIdx); + SingleOperationBuilder insert(int pdIdx, int cdIdx); + SingleOperationBuilder insert(int pdIdx, int cdIdx, int[] valueIdxs, int[] sValueIdxs); - /** - * Perform an insert operation to a _specific_ row. Rows are ordered by clustering key and - * numbered from 0 to maxRows - */ - SingleOperationBuilder insert(int rowIdx); + // TODO: selection + SingleOperationBuilder selectRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, + int nonEqFrom, boolean includeLowBound, boolean includeHighBound); + SingleOperationBuilder selectPartition(int pdIdx); + SingleOperationBuilder selectPartition(int pdIdx, Operations.ClusteringOrderBy orderBy); + SingleOperationBuilder selectRow(int pdIdx, int cdIdx); + SingleOperationBuilder selectRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean isEq); + SingleOperationBuilder selectRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean isEq); - /** - * Insert _specific values_ into _specific_ row. Rows are ordered by clustering key and - * numbered from 0 to maxRows - */ - SingleOperationBuilder insert(int rowIdx, long[] valueIdxs); - SingleOperationBuilder insert(int rowIdx, long[] valueIdxs, long[] sValueIdxs); + SingleOperationBuilder deletePartition(int pdIdx); + SingleOperationBuilder deleteRow(int pdIdx, int cdIdx); + SingleOperationBuilder deleteColumns(int pdIdx, int cdIdx, BitSet regularSelection, BitSet staticSelection); + SingleOperationBuilder deleteRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean isEq); + SingleOperationBuilder deleteRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean isEq); + SingleOperationBuilder deleteRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, + int nonEqFrom, boolean includeLowBound, boolean includeHighBound); - SingleOperationBuilder deletePartition(); + SingleOperationBuilder select(int pdIdx, + IdxRelation[] ckRelations, + IdxRelation[] regularRelations, + IdxRelation[] staticRelations); - SingleOperationBuilder deleteRow(); - SingleOperationBuilder deleteRow(int rowIdx); + class IdxRelation + { + public final Relations.RelationKind kind; + public final int idx; + public final int column; - SingleOperationBuilder deleteColumns(); + public IdxRelation(Relations.RelationKind kind, int idx, int column) + { + this.kind = kind; + this.idx = idx; + this.column = column; + } + } - SingleOperationBuilder deleteRowRange(); - SingleOperationBuilder deleteRowRange(int lowBoundRowIdx, int highBoundRowIdx, boolean isMinEq, boolean isMaxEq); - - SingleOperationBuilder deleteRowSlice(); + interface OperationFactory + { + Operations.Operation make(long lts, long opId); + } } diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java index 33eca3238c..b4f2ff908b 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java @@ -1,295 +1,577 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.gen.IndexGenerators; +import org.apache.cassandra.harry.gen.rng.PCGFastPure; +import org.apache.cassandra.harry.gen.rng.PureRng; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; 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; + +import static org.apache.cassandra.harry.dsl.HistoryBuilder.*; +import static org.apache.cassandra.harry.op.Operations.Kind; +import static org.apache.cassandra.harry.op.Operations.Operation; +import static org.apache.cassandra.harry.op.Operations.WriteOp; class SingleOperationVisitBuilder implements SingleOperationBuilder { + private static final Logger logger = LoggerFactory.getLogger(SingleOperationVisitBuilder.class); + // TODO: singleton collection for this op class - private final List operations; - private final PartitionVisitStateImpl partitionState; + protected final List operations; - private final long lts; - private final long pd; + protected final long lts; - private final OpSelectors.PureRng rng; + protected final Consumer appendToLog; + protected final SeedableEntropySource rngSupplier = new SeedableEntropySource(); + protected final PureRng seedSelector; - private final OpSelectors.DescriptorSelector descriptorSelector; - private final ValueHelper valueHelper; - private final SchemaSpec schema; + protected int opIdCounter; - private final Consumer appendToLog; - private final WithEntropySource rngSupplier = new WithEntropySource(); + protected final IndexedValueGenerators valueGenerators; + protected final IndexGenerators indexGenerators; - private int opIdCounter; - - public SingleOperationVisitBuilder(PartitionVisitStateImpl partitionState, - long lts, - OpSelectors.PureRng rng, - OpSelectors.DescriptorSelector descriptorSelector, - SchemaSpec schema, - ValueHelper valueHelper, - Consumer appendToLog) + SingleOperationVisitBuilder(long lts, + IndexedValueGenerators valueGenerators, + IndexGenerators indexGenerators, + Consumer appendToLog) { this.operations = new ArrayList<>(); - this.partitionState = partitionState; - - this.pd = partitionState.pd; this.lts = lts; - this.rng = rng; - - this.descriptorSelector = descriptorSelector; - this.valueHelper = valueHelper; - this.schema = schema; - this.appendToLog = appendToLog; this.opIdCounter = 0; + + this.valueGenerators = valueGenerators; + this.indexGenerators = indexGenerators; + + this.seedSelector = new PureRng.PCGFast(lts); } @Override - public SingleOperationVisitBuilder insert() - { - int clusteringOffset = rngSupplier.withSeed(lts).nextInt(0, partitionState.possibleCds.length - 1); - return insert(clusteringOffset); - } - - @Override - public SingleOperationVisitBuilder insert(int rowIdx) + public SingleOperationBuilder insert() { 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(), schema); - } + long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + return rngSupplier.computeWithSeed(seed, rng -> { + int pdIdx = indexGenerators.pkIdxGen.generate(rng); + int cdIdx = indexGenerators.ckIdxGen.generate(rng); + + int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; + for (int i = 0; i < valueIdxs.length; i++) + valueIdxs[i] = indexGenerators.regularIdxGens[i].generate(rng); + int[] sValueIdxs = new int[indexGenerators.staticIdxGens.length]; + for (int i = 0; i < sValueIdxs.length; i++) + sValueIdxs[i] = indexGenerators.staticIdxGens[i].generate(rng); + return insert(pdIdx, cdIdx, valueIdxs, sValueIdxs); }); - end(); - return this; } @Override - public SingleOperationVisitBuilder insert(int rowIdx, long[] valueIdxs) + public SingleOperationBuilder insert(int pdIdx) { - assert valueIdxs.length == valueHelper.regularColumns.size(); int opId = opIdCounter++; - long cd = partitionState.possibleCds[rowIdx]; - operations.add(new GeneratingVisitor.GeneratedWriteOp(lts, pd, cd, opId, - OpSelectors.OperationKind.INSERT) - { - public long[] vds() - { - long[] vds = new long[valueIdxs.length]; - for (int i = 0; i < valueHelper.regularColumns.size(); i++) - { - vds[i] = valueHelper.descriptorGenerators - .get(valueHelper.regularColumns.get(i).name) - .inflate(valueIdxs[i]); - } - return vds; - } + long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + return rngSupplier.computeWithSeed(seed, rng -> { + int cdIdx = indexGenerators.ckIdxGen.generate(rng); + + int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; + for (int i = 0; i < valueIdxs.length; i++) + valueIdxs[i] = indexGenerators.regularIdxGens[i].generate(rng); + int[] sValueIdxs = new int[indexGenerators.staticIdxGens.length]; + for (int i = 0; i < sValueIdxs.length; i++) + sValueIdxs[i] = indexGenerators.staticIdxGens[i].generate(rng); + return insert(pdIdx, cdIdx, valueIdxs, sValueIdxs); }); - end(); - return this; } @Override - public SingleOperationBuilder insert(int rowIdx, long[] valueIdxs, long[] sValueIdxs) + public SingleOperationBuilder insert(int pdIdx, int cdIdx) { - assert valueIdxs.length == valueHelper.regularColumns.size(); - assert sValueIdxs.length == valueHelper.staticColumns.size(); int opId = opIdCounter++; - long cd = partitionState.possibleCds[rowIdx]; - operations.add(new GeneratingVisitor.GeneratedWriteWithStaticOp(lts, pd, cd, opId, - OpSelectors.OperationKind.INSERT_WITH_STATICS) - { + long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + return rngSupplier.computeWithSeed(seed, rng -> { + int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; + for (int i = 0; i < valueIdxs.length; i++) + valueIdxs[i] = indexGenerators.regularIdxGens[i].generate(rng); + int[] sValueIdxs = new int[indexGenerators.staticIdxGens.length]; + for (int i = 0; i < sValueIdxs.length; i++) + sValueIdxs[i] = indexGenerators.staticIdxGens[i].generate(rng); + return insert(pdIdx, cdIdx, valueIdxs, sValueIdxs); + }); + } + + @Override + public SingleOperationBuilder insert(int pdIdx, int cdIdx, int[] valueIdxs, int[] sValueIdxs) + { + return write(pdIdx, cdIdx, valueIdxs, sValueIdxs, Kind.INSERT); + } + + @Override + public SingleOperationBuilder custom(Runnable runnable, String tag) + { + // TODO: assert that custom op is always alone in visit + operations.add(new Operations.CustomRunnableOperation(lts, opIdCounter, runnable) { @Override - public long[] vds() + public String toString() { - long[] vds = new long[valueIdxs.length]; - for (int i = 0; i < valueHelper.regularColumns.size(); i++) - { - vds[i] = valueHelper.descriptorGenerators - .get(valueHelper.regularColumns.get(i).name) - .inflate(valueIdxs[i]); - } - return vds; + return String.format("%s (%s)", Kind.CUSTOM, tag); } + }); + build(); + return this; + } + @Override + public SingleOperationBuilder custom(OperationFactory factory) + { + operations.add(factory.make(lts, opIdCounter++)); + build(); + return this; + } + + @Override + public SingleOperationBuilder update() + { + int opId = opIdCounter++; + long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + return rngSupplier.computeWithSeed(seed, rng -> { + int pdIdx = indexGenerators.pkIdxGen.generate(rng); + int cdIdx = indexGenerators.ckIdxGen.generate(rng); + + int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; + for (int i = 0; i < valueIdxs.length; i++) + valueIdxs[i] = indexGenerators.regularIdxGens[i].generate(rng); + int[] sValueIdxs = new int[indexGenerators.staticIdxGens.length]; + for (int i = 0; i < sValueIdxs.length; i++) + sValueIdxs[i] = indexGenerators.staticIdxGens[i].generate(rng); + return update(pdIdx, cdIdx, valueIdxs, sValueIdxs); + }); + } + + @Override + public SingleOperationBuilder update(int pdIdx) + { + int opId = opIdCounter++; + long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + return rngSupplier.computeWithSeed(seed, rng -> { + int cdIdx = indexGenerators.ckIdxGen.generate(rng); + + int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; + for (int i = 0; i < valueIdxs.length; i++) + valueIdxs[i] = indexGenerators.regularIdxGens[i].generate(rng); + int[] sValueIdxs = new int[indexGenerators.staticIdxGens.length]; + for (int i = 0; i < sValueIdxs.length; i++) + sValueIdxs[i] = indexGenerators.staticIdxGens[i].generate(rng); + return update(pdIdx, cdIdx, valueIdxs, sValueIdxs); + }); + } + + @Override + public SingleOperationBuilder update(int pdIdx, int cdIdx) + { + int opId = opIdCounter++; + long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + return rngSupplier.computeWithSeed(seed, rng -> { + int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; + for (int i = 0; i < valueIdxs.length; i++) + valueIdxs[i] = indexGenerators.regularIdxGens[i].generate(rng); + int[] sValueIdxs = new int[indexGenerators.staticIdxGens.length]; + for (int i = 0; i < sValueIdxs.length; i++) + sValueIdxs[i] = indexGenerators.staticIdxGens[i].generate(rng); + return update(pdIdx, cdIdx, valueIdxs, sValueIdxs); + }); + } + + @Override + public SingleOperationBuilder update(int pdIdx, int cdIdx, int[] valueIdxs, int[] sValueIdxs) + { + return write(pdIdx, cdIdx, valueIdxs, sValueIdxs, Kind.UPDATE); + } + + private SingleOperationBuilder write(int pdIdx, int cdIdx, int[] valueIdxs, int[] sValueIdxs, Kind kind) + { + assert valueIdxs.length == valueGenerators.regularColumnCount(); + assert sValueIdxs.length == valueGenerators.staticColumnCount(); + + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + long cd = valueGenerators.ckGen().descriptorAt(cdIdx); + + opIdCounter++; + long[] vds = new long[valueIdxs.length]; + for (int i = 0; i < valueGenerators.regularColumnCount(); i++) + { + int valueIdx = valueIdxs[i]; + if (valueIdx == MagicConstants.UNSET_IDX) + vds[i] = MagicConstants.UNSET_DESCR; + else + vds[i] = valueGenerators.regularColumnGen(i).descriptorAt(valueIdx); + } + + long[] sds = new long[sValueIdxs.length]; + for (int i = 0; i < sValueIdxs.length; i++) + { + int valueIdx = sValueIdxs[i]; + if (valueIdx == MagicConstants.UNSET_IDX) + sds[i] = MagicConstants.UNSET_DESCR; + else + sds[i] = valueGenerators.staticColumnGen(i).descriptorAt(valueIdx); + } + + operations.add(new WriteOp(lts, pd, cd, vds, sds, kind) { @Override - public long[] sds() + public String toString() { - long[] sds = new long[sValueIdxs.length]; - for (int i = 0; i < valueHelper.staticColumns.size(); i++) - { - sds[i] = valueHelper.descriptorGenerators - .get(valueHelper.staticColumns.get(i).name) - .inflate(sValueIdxs[i]); - } - return sds; + return String.format("%s (%d, %d, %s, %s)", + kind, pdIdx, cdIdx, Arrays.toString(valueIdxs), Arrays.toString(sValueIdxs)); } }); - end(); + build(); return this; } @Override - public SingleOperationVisitBuilder deletePartition() + public SingleOperationVisitBuilder deleteRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, + int nonEqFrom, boolean includeLowerBound, boolean includeUpperBound) { - int opId = opIdCounter++; - operations.add(new GeneratingVisitor.GeneratedDeleteOp(lts, pd, opId, OpSelectors.OperationKind.DELETE_PARTITION, - Query.selectAllColumns(schema, pd, false))); - end(); - return this; - } + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); - @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; - } + long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); + long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); - @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; - } + Relations.RelationKind[] lowerBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; + Relations.RelationKind[] upperBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; - @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) + rngSupplier.doWithSeed(opId, rng -> { + for (int i = 0; i < Math.min(nonEqFrom + 1, valueGenerators.ckColumnCount()); i++) { - try + if (i < nonEqFrom) + lowerBoundRelations[i] = Relations.RelationKind.EQ; + else { - 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(schema, pd, cd1, cd2, queryDescriptor, isMinEq, isMaxEq, false); - break; - } - catch (IllegalArgumentException retry) - { - continue; + lowerBoundRelations[i] = includeLowerBound ? Relations.RelationKind.GTE : Relations.RelationKind.GT; + upperBoundRelations[i] = includeUpperBound ? Relations.RelationKind.LTE : Relations.RelationKind.LT; } } - 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(schema, 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) + operations.add(new Operations.DeleteRange(lts, pd, + lowerBoundCd, upperBoundCd, + lowerBoundRelations, upperBoundRelations) { + @Override + public String toString() { - try - { - int cdIdx = rng.nextInt(partitionState.possibleCds.length); - long cd = partitionState.possibleCds[cdIdx]; + return String.format("DELETE (%d, >%s%d, <%s%d) - (%d)", + pdIdx, + includeLowerBound ? "=" : "", lowerBoundRowIdx, + includeUpperBound ? "=" : "", upperBoundRowIdx, + nonEqFrom); + } + }); + build(); + return this; + } - boolean isGt = rng.nextBoolean(); - boolean isEquals = rng.nextBoolean(); - query = Query.clusteringSliceQuery(schema, pd, cd, queryDescriptor, isGt, isEquals, false); - break; - } - catch (IllegalArgumentException retry) + @Override + public SingleOperationBuilder deletePartition(int pdIdx) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + opIdCounter++; + + operations.add(new Operations.DeletePartition(lts, pd) { + @Override + public String toString() + { + return String.format("DELETE_PARTITION (%d)", pdIdx); + } + }); + build(); + return this; + } + + @Override + public SingleOperationBuilder deleteRow(int pdIdx, int rowIdx) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + opIdCounter++; + long cd = valueGenerators.ckGen().descriptorAt(rowIdx); + operations.add(new Operations.DeleteRow(lts, pd, cd) { + @Override + public String toString() + { + return String.format("DELETE_ROW (%d, %s)", pdIdx, rowIdx); + } + }); + build(); + return this; + } + + @Override + public SingleOperationBuilder deleteColumns(int pdIdx, int rowIdx, BitSet regularSelection, BitSet staticSelection) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + opIdCounter++; + long cd = valueGenerators.ckGen().descriptorAt(rowIdx); + operations.add(new Operations.DeleteColumns(lts, pd, cd, regularSelection, staticSelection) { + @Override + public String toString() + { + return String.format("DELETE_COLUMNS (%d, %d, %s, %s)", pdIdx, rowIdx, regularSelection, staticSelection); + } + }); + build(); + return this; + } + + @Override + public SingleOperationBuilder deleteRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean includeBound) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); + + Relations.RelationKind[] lowerBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; + + int opId = opIdCounter++; + rngSupplier.doWithSeed(opId, rng -> { + for (int i = 0; i < Math.min(nonEqFrom + 1, valueGenerators.ckColumnCount()); i++) + { + if (i < nonEqFrom) + lowerBoundRelations[i] = Relations.RelationKind.EQ; + else + lowerBoundRelations[i] = includeBound ? Relations.RelationKind.GTE : Relations.RelationKind.GT; + } + }); + + operations.add(new Operations.DeleteRange(lts, pd, + lowerBoundCd, MagicConstants.UNSET_DESCR, + lowerBoundRelations, null) { + @Override + public String toString() + { + return String.format("DELETE (%d, >%s%d) - (%d)", + pdIdx, includeBound ? "=" : "", lowerBoundRowIdx, nonEqFrom); + } + }); + build(); + return this; + } + + @Override + public SingleOperationBuilder deleteRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean includeBound) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); + + Relations.RelationKind[] upperBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; + + int opId = opIdCounter++; + rngSupplier.doWithSeed(opId, rng -> { + for (int i = 0; i < Math.min(nonEqFrom + 1, valueGenerators.ckColumnCount()); i++) + { + if (i < nonEqFrom) + upperBoundRelations[i] = Relations.RelationKind.EQ; + else + upperBoundRelations[i] = includeBound ? Relations.RelationKind.LTE : Relations.RelationKind.LT; + } + }); + + operations.add(new Operations.DeleteRange(lts, pd, + MagicConstants.UNSET_DESCR, upperBoundCd, + null, upperBoundRelations) { + @Override + public String toString() + { + return String.format("DELETE (%d, <%s%d) - (%d)", + pdIdx, includeBound ? "=" : "", upperBoundRowIdx, nonEqFrom); + } + }); + build(); + return this; + } + + + @Override + public SingleOperationVisitBuilder selectRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, + int nonEqFrom, boolean includeLowerBound, boolean includeUpperBound) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); + long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); + + Relations.RelationKind[] lowerBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; + Relations.RelationKind[] upperBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; + + int opId = opIdCounter++; + rngSupplier.doWithSeed(opId, rng -> { + for (int i = 0; i < Math.min(nonEqFrom + 1, valueGenerators.ckColumnCount()); i++) + { + if (i < nonEqFrom) + lowerBoundRelations[i] = Relations.RelationKind.EQ; + else { - continue; + lowerBoundRelations[i] = includeLowerBound ? Relations.RelationKind.GTE : Relations.RelationKind.GT; + upperBoundRelations[i] = includeUpperBound ? Relations.RelationKind.LTE : Relations.RelationKind.LT; } } - operations.add(new GeneratingVisitor.GeneratedDeleteOp(lts, pd, opId, OpSelectors.OperationKind.DELETE_SLICE, query)); }); - end(); + + operations.add(new Operations.SelectRange(lts, pd, + lowerBoundCd, upperBoundCd, + lowerBoundRelations, upperBoundRelations)); + build(); + return this; + } + + @Override + public SingleOperationBuilder select(int pdIdx, IdxRelation[] ckIdxRelations, IdxRelation[] regularIdxRelations, IdxRelation[] staticIdxRelations) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + opIdCounter++; + + Relations.Relation[] ckRelations = new Relations.Relation[ckIdxRelations.length]; + for (int i = 0; i < ckRelations.length; i++) + { + Invariants.checkState(ckIdxRelations[i].column < valueGenerators.ckColumnCount()); + ckRelations[i] = new Relations.Relation(ckIdxRelations[i].kind, + valueGenerators.ckGen().descriptorAt(ckIdxRelations[i].idx), + ckIdxRelations[i].column); + } + + Relations.Relation[] regularRelations = new Relations.Relation[regularIdxRelations.length]; + for (int i = 0; i < regularRelations.length; i++) + { + Invariants.checkState(regularIdxRelations[i].column < valueGenerators.regularColumnCount()); + regularRelations[i] = new Relations.Relation(regularIdxRelations[i].kind, + valueGenerators.regularColumnGen(regularIdxRelations[i].column).descriptorAt(regularIdxRelations[i].idx), + regularIdxRelations[i].column); + } + + Relations.Relation[] staticRelations = new Relations.Relation[staticIdxRelations.length]; + for (int i = 0; i < staticRelations.length; i++) + { + Invariants.checkState(staticIdxRelations[i].column < valueGenerators.staticColumnCount()); + staticRelations[i] = new Relations.Relation(staticIdxRelations[i].kind, + valueGenerators.staticColumnGen(staticIdxRelations[i].column).descriptorAt(staticIdxRelations[i].idx), + staticIdxRelations[i].column); + } + + operations.add(new Operations.SelectCustom(lts, pd, ckRelations, regularRelations, staticRelations)); + build(); + return this; + } + + @Override + public SingleOperationBuilder selectPartition(int pdIdx) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + opIdCounter++; + + operations.add(new Operations.SelectPartition(lts, pd)); + build(); + return this; + } + + @Override + public SingleOperationBuilder selectPartition(int pdIdx, Operations.ClusteringOrderBy orderBy) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + opIdCounter++; + + operations.add(new Operations.SelectPartition(lts, pd, orderBy)); + build(); + return this; + } + + @Override + public SingleOperationBuilder selectRow(int pdIdx, int rowIdx) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + opIdCounter++; + long cd = valueGenerators.ckGen().descriptorAt(rowIdx); + operations.add(new Operations.SelectRow(lts, pd, cd)); + build(); + return this; + } + + @Override + public SingleOperationBuilder selectRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean includeBound) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); + + Relations.RelationKind[] lowerBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; + + int opId = opIdCounter++; + rngSupplier.doWithSeed(opId, rng -> { + for (int i = 0; i < Math.min(nonEqFrom + 1, valueGenerators.ckColumnCount()); i++) + { + if (i < nonEqFrom) + lowerBoundRelations[i] = Relations.RelationKind.EQ; + else + lowerBoundRelations[i] = includeBound ? Relations.RelationKind.GTE : Relations.RelationKind.GT; + } + }); + + operations.add(new Operations.SelectRange(lts, pd, + lowerBoundCd, MagicConstants.UNSET_DESCR, + lowerBoundRelations, null)); + build(); + return this; + } + + @Override + public SingleOperationBuilder selectRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean includeBound) + { + long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); + + Relations.RelationKind[] upperBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; + + int opId = opIdCounter++; + rngSupplier.doWithSeed(opId, rng -> { + for (int i = 0; i < Math.min(nonEqFrom + 1, valueGenerators.ckColumnCount()); i++) + { + if (i < nonEqFrom) + upperBoundRelations[i] = Relations.RelationKind.EQ; + else + upperBoundRelations[i] = includeBound ? Relations.RelationKind.LTE : Relations.RelationKind.LT; + } + }); + + operations.add(new Operations.SelectRange(lts, pd, + MagicConstants.UNSET_DESCR, upperBoundCd, + null, upperBoundRelations)); + build(); return this; } @@ -298,28 +580,12 @@ class SingleOperationVisitBuilder implements SingleOperationBuilder return this.operations.size(); } - void end() + Visit build() { - VisitExecutor.Operation[] ops = new VisitExecutor.Operation[operations.size()]; + Operation[] ops = new Operation[operations.size()]; operations.toArray(ops); - ReplayingVisitor.Visit visit = new ReplayingVisitor.Visit(lts, pd, ops); + Visit visit = new Visit(lts, ops); appendToLog.accept(visit); - } - - private static class WithEntropySource - { - private final EntropySource entropySource = new JdkRandomEntropySource(0); - - public void withSeed(long seed, Consumer rng) - { - entropySource.seed(seed); - rng.accept(entropySource); - } - - public EntropySource withSeed(long seed) - { - entropySource.seed(seed); - return entropySource; - } + return visit; } } diff --git a/test/harry/main/org/apache/cassandra/harry/util/TestRunner.java b/test/harry/main/org/apache/cassandra/harry/dsl/TestRunner.java similarity index 77% rename from test/harry/main/org/apache/cassandra/harry/util/TestRunner.java rename to test/harry/main/org/apache/cassandra/harry/dsl/TestRunner.java index 1941137436..1bbf6b56c8 100644 --- a/test/harry/main/org/apache/cassandra/harry/util/TestRunner.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/TestRunner.java @@ -1,22 +1,22 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -package org.apache.cassandra.harry.util; +package org.apache.cassandra.harry.dsl; import java.util.function.BiConsumer; import java.util.function.BiFunction; diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/ValueDescriptorIndexGenerator.java b/test/harry/main/org/apache/cassandra/harry/dsl/ValueDescriptorIndexGenerator.java deleted file mode 100644 index fd100f869c..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/ValueDescriptorIndexGenerator.java +++ /dev/null @@ -1,87 +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.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 -{ - public static int UNSET = Integer.MIN_VALUE; - - private final OpSelectors.PureRng rng; - private final long columnHash; - private final long mask; - - public ValueDescriptorIndexGenerator(ColumnSpec columnSpec, long seed) - { - this(columnSpec, new OpSelectors.PCGFast(seed)); - } - - 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) - { - if (idx == UNSET) - return DataGenerators.UNSET_DESCR; - - return rng.randomNumber(idx, columnHash) & mask; - } - - /** - * Returns a supplier that would uniformly pick from at most {@param values} values. - * - * @param values number of possible values - */ - public LongSupplier toSupplier(EntropySource orig, int values, float chanceOfUnset) - { - EntropySource derived = orig.derive(); - if (chanceOfUnset > 0) - { - assert chanceOfUnset < 1.0; - return () -> { - if (orig.nextFloat() < chanceOfUnset) - return DataGenerators.UNSET_DESCR; - return inflate(derived.nextInt(values)); - }; - } - - return () -> inflate(derived.nextInt(values)); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/ValueHelper.java b/test/harry/main/org/apache/cassandra/harry/dsl/ValueHelper.java deleted file mode 100644 index fcce222543..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/dsl/ValueHelper.java +++ /dev/null @@ -1,74 +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.harry.dsl; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; - -class ValueHelper implements ValueOverrides -{ - protected Map> overrides = new HashMap<>(); - protected Map descriptorGenerators = new HashMap<>(); - - protected final List> regularColumns; - protected final List> staticColumns; - - @SuppressWarnings("rawtypes,unchecked") - public ValueHelper(SchemaSpec orig, OpSelectors.PureRng rng) - { - this.regularColumns = new ArrayList<>(); - for (ColumnSpec regular : orig.regularColumns) - { - OverridingBijection override = new OverridingBijection<>(regular.generator()); - regular = regular.override(override); - this.regularColumns.add(regular); - this.overrides.put(regular.name, override); - this.descriptorGenerators.put(regular.name, new ValueDescriptorIndexGenerator(regular, rng)); - } - - this.staticColumns = new ArrayList<>(); - for (ColumnSpec static_ : orig.staticColumns) - { - OverridingBijection override = new OverridingBijection<>(static_.generator()); - static_ = static_.override(override); - this.staticColumns.add(static_); - this.overrides.put(static_.name, override); - this.descriptorGenerators.put(static_.name, new ValueDescriptorIndexGenerator(static_, rng)); - } - } - - @Override - @SuppressWarnings("unchecked,rawtypes") - public void override(String columnName, int idx, Object override) - { - OverridingBijection gen = overrides.get(columnName); - if (gen == null) - throw new IllegalStateException(String.format("Overrides for %s are not supported", columnName)); - if (idx == ValueDescriptorIndexGenerator.UNSET) - throw new IllegalStateException("Can't override an UNSET value"); - - gen.override(descriptorGenerators.get(columnName).inflate(idx), override); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/execution/CQLTesterVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/CQLTesterVisitExecutor.java new file mode 100644 index 0000000000..0f301d8cb8 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/CQLTesterVisitExecutor.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.execution; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.model.Model; + +import static org.apache.cassandra.harry.MagicConstants.*; + +public class CQLTesterVisitExecutor extends CQLVisitExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(CQLTesterVisitExecutor.class); + private final Function execute; + + public CQLTesterVisitExecutor(SchemaSpec schema, + DataTracker dataTracker, + Model model, + Function execute) + { + super(schema, dataTracker, model, new QueryBuildingVisitExecutor(schema, QueryBuildingVisitExecutor.WrapQueries.UNLOGGED_BATCH)); + this.execute = execute; + } + + @Override + public List executeWithResult(Visit visit, CompiledStatement statement) + { + List actual = new ArrayList<>(); + // TODO: Have never tested with multiple + Invariants.checkState(visit.operations.length == 1); + for (UntypedResultSet.Row row : execute.apply(statement)) + actual.add(resultSetToRow(schema, (Operations.SelectStatement) visit.operations[0], row)); + return actual; + } + + protected void executeWithoutResult(Visit visit, CompiledStatement statement) + { + execute.apply(statement); + } + + public static ResultSetRow resultSetToRow(SchemaSpec schema, Operations.SelectStatement select, UntypedResultSet.Row row) + { + // TODO: do we want to use selection? + Operations.Selection selection = Operations.Selection.fromBitSet(select.selection(), schema); + + long pd = UNKNOWN_DESCR; + if (selection.selectsAllOf(schema.partitionKeys)) + { + Object[] partitionKey = new Object[schema.partitionKeys.size()]; + for (int i = 0; i < schema.partitionKeys.size(); i++) + { + ColumnSpec column = schema.partitionKeys.get(i); + partitionKey[i] = column.type.asServerType().compose(row.getBytes(column.name)); + } + + pd = schema.valueGenerators.pkGen().deflate(partitionKey); + } + + + long cd = UNKNOWN_DESCR; + if (selection.selectsAllOf(schema.clusteringKeys)) + { + Object[] clusteringKey = new Object[schema.clusteringKeys.size()]; + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec column = schema.clusteringKeys.get(i); + if (row.has(column.name)) + { + clusteringKey[i] = column.type.asServerType().compose(row.getBytes(column.name)); + } + else + { + for (int j = 0; j < schema.clusteringKeys.size(); j++) + { + Invariants.checkState(!row.has(schema.clusteringKeys.get(j).name), + "All elements of clustering key should have been null"); + } + clusteringKey = NIL_KEY; + break; + } + } + + // Clusterings can not be set to nil, so if we do not see, we assume it is unset + if (clusteringKey == NIL_KEY) + cd = UNSET_DESCR; + else + cd = schema.valueGenerators.ckGen().deflate(clusteringKey); + } + + long[] regularColumns = new long[schema.regularColumns.size()]; + for (int i = 0; i < schema.regularColumns.size(); i++) + { + ColumnSpec column = schema.regularColumns.get(i); + if (selection.selects(column)) + { + if (row.has(column.name)) + { + Object value = column.type.asServerType().compose(row.getBytes(column.name)); + regularColumns[i] = schema.valueGenerators.regularColumnGen(i).deflate(value); + } + else + { + regularColumns[i] = NIL_DESCR; + } + } + else + { + regularColumns[i] = UNKNOWN_DESCR; + } + } + + long[] staticColumns = new long[schema.staticColumns.size()]; + for (int i = 0; i < schema.staticColumns.size(); i++) + { + ColumnSpec column = schema.staticColumns.get(i); + if (selection.selects(column)) + { + if (row.has(column.name)) + { + Object value = column.type.asServerType().compose(row.getBytes(column.name)); + staticColumns[i] = schema.valueGenerators.staticColumnGen(i).deflate(value); + } + else + { + staticColumns[i] = NIL_DESCR; + } + } + else + { + staticColumns[i] = UNKNOWN_DESCR; + } + } + + long[] regularLts = LTS_UNKNOWN; + if (selection.includeTimestamps()) + throw new IllegalStateException("not implemented for CQL Tester"); + long[] staticLts = LTS_UNKNOWN; + if (selection.includeTimestamps()) + throw new IllegalStateException("not implemented for CQL Tester"); + + return new ResultSetRow(pd, cd, staticColumns, staticLts, regularColumns, regularLts); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/execution/CQLVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/CQLVisitExecutor.java new file mode 100644 index 0000000000..1922f0011a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/CQLVisitExecutor.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.execution; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; + +/** + * + * TODO: Transactional results ; LET + */ +public abstract class CQLVisitExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(QueryBuildingVisitExecutor.class); + protected final SchemaSpec schema; + + protected final DataTracker dataTracker; + protected final Model model; + private final QueryBuildingVisitExecutor queryBuilder; + + public CQLVisitExecutor(SchemaSpec schema, DataTracker dataTracker, Model model, QueryBuildingVisitExecutor queryBuilder) + { + this.schema = schema; + this.dataTracker = dataTracker; + this.model = model; + this.queryBuilder = queryBuilder; + } + + public static void replay(CQLVisitExecutor executor, Model.Replay replay) + { + for (Visit visit : replay) + { + if (visit.lts % 1000 == 0) + logger.debug("Replaying visit {}", visit.lts); + executeVisit(visit, executor, replay); + } + } + + public static void executeVisit(Visit visit, CQLVisitExecutor executor, Model.Replay replay) + { + try + { + executor.execute(visit); + } + catch (Throwable t) + { + // TODO (required): remove when CASSANDRA-19869 is fixed + if (t.getMessage() != null && t.getMessage().contains("class org.apache.cassandra.db.ReadQuery$1 cannot be cast to class org.apache.cassandra.db.SinglePartitionReadQuery$Group")) + return; + + replayAfterFailure(visit, executor, replay); + + throw t; + } + } + + public enum ResultDumpMode + { + PARTITION, + LAST_50 + } + + public static void replayAfterFailure(Visit visit, CQLVisitExecutor executor, Model.Replay replay) + { + QueryBuildingVisitExecutor queryBuilder = executor.queryBuilder; + logger.error("Caught an exception at {} while replaying {}\ncluster.schemaChange(\"{}\");\nOperations _for this partition_ up to this visit:", + visit, queryBuilder.compile(visit), + queryBuilder.schema.compile()); + + // Configurable yet hardcoded for a person who is trying to generate repro + ResultDumpMode mode = ResultDumpMode.PARTITION; + long minLts = 0; + if (mode == ResultDumpMode.LAST_50) + minLts = Math.max(0, visit.lts - 20); + for (Visit rereplay : replay) + { + if (rereplay.lts < minLts) + continue; + + // Skip if this visit was for a different partition + if (mode == ResultDumpMode.PARTITION && !intersects(visit.visitedPartitions, rereplay.visitedPartitions)) + continue; + + String str = ""; + CompiledStatement cs = queryBuilder.compile(rereplay); + if (cs != null) + str = cs.dump(ConsistencyLevel.QUORUM); + logger.info("/*{}*/ {}", rereplay, str); + + if (rereplay.lts > visit.lts) + return; + } + } + + private static boolean intersects(Set set1, Set set2) + { + for (Object o : set1) + { + if (set2.contains(o)) + return true; + } + return false; + } + + public void execute(Visit visit) + { + dataTracker.begin(visit); + QueryBuildingVisitExecutor.BuiltQuery compiledStatement = queryBuilder.compile(visit); + // All operations are not touching any data + if (compiledStatement == null) + { + Invariants.checkArgument(Arrays.stream(visit.operations).allMatch(op -> op.kind() == Operations.Kind.CUSTOM)); + return; + } + + List selects = compiledStatement.selects; + if (selects.isEmpty()) + { + executeMutatingVisit(visit, compiledStatement); + } + else + { + Invariants.checkState(selects.size() == 1); + executeValidatingVisit(visit, selects, compiledStatement); + } + dataTracker.end(visit); + } + + // Lives in a separate method so that it is easier to override it + protected void executeMutatingVisit(Visit visit, CompiledStatement statement) + { + executeWithoutResult(visit, statement); + } + + // Lives in a separate method so that it is easier to override it + protected void executeValidatingVisit(Visit visit, List selects, CompiledStatement compiledStatement) + { + // TODO: Have not tested with multiple + List resultSetRow = executeWithResult(visit, compiledStatement); + try + { + model.validate(selects.get(0), resultSetRow); + } + catch (Throwable t) + { + throw new AssertionError(String.format("Caught an exception while validating %s:\n%s", selects.get(0), compiledStatement), + t); + } + } + + protected abstract List executeWithResult(Visit visit, CompiledStatement statement); + protected abstract void executeWithoutResult(Visit visit, CompiledStatement statement); + +} diff --git a/test/harry/main/org/apache/cassandra/harry/execution/CompiledStatement.java b/test/harry/main/org/apache/cassandra/harry/execution/CompiledStatement.java new file mode 100644 index 0000000000..8e84a64b61 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/CompiledStatement.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.execution; + +import java.net.InetAddress; +import java.util.UUID; + +import org.apache.cassandra.distributed.api.ConsistencyLevel; + +public class CompiledStatement +{ + private final String cql; + private final Object[] bindings; + + public CompiledStatement(String cql, Object... bindings) + { + this.cql = cql; + this.bindings = bindings; + } + + public String cql() + { + return cql; + } + + public CompiledStatement withSchema(String oldKs, String oldTable, String newKs, String newTable) + { + return new CompiledStatement(cql.replace(oldKs + "." + oldTable, + newKs + "." + newTable), + bindings); + } + + public CompiledStatement withFiltering() + { + return new CompiledStatement(cql.replace(";", + " ALLOW FILTERING;"), + bindings); + } + + public Object[] bindings() + { + return bindings; + } + + public static CompiledStatement create(String cql, Object... bindings) + { + return new CompiledStatement(cql, bindings); + } + + public String toString() + { + return "CompiledStatement{" + + "cql=execute(\"" + cql.replace("\n", "\" + \n\"") + '\"' + + ", " + bindingsToString(bindings) + + '}'; + } + + public String dump(ConsistencyLevel cl) + { + return String.format("cluster.coordinator(1).execute(\"%s\", ConsistencyLevel.%s, %s);", + cql.replace("\n", "\" + \n\""), + cl.toString(), + bindingsToString(bindings)); + } + + public static String bindingsToString(Object... bindings) + { + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (Object binding : bindings) + { + if (isFirst) + isFirst = false; + else + sb.append(","); + + if (binding instanceof String) + sb.append("\"").append(binding).append("\""); + else if (binding instanceof Short) + sb.append("(short)").append(binding); + else if (binding instanceof Byte) + sb.append("(byte)").append(binding); + else if (binding instanceof Float) + sb.append("(float)").append(binding); + else if (binding instanceof Double) + sb.append("(double)").append(binding); + else if (binding instanceof Long) + sb.append(binding).append("L"); + else if (binding instanceof Integer) + sb.append("(int)").append(binding); + else if (binding instanceof Boolean) + sb.append(binding); + else if (binding instanceof UUID) + sb.append("java.util.UUID.fromString(\"").append(binding).append("\")"); + else if (binding instanceof java.sql.Timestamp) + sb.append("new java.sql.Timestamp(").append(((java.sql.Timestamp) binding).getTime()).append("L)"); + else if (binding instanceof java.sql.Time) + sb.append("new java.sql.Time(").append(((java.sql.Time) binding).getTime()).append("L)"); + else if (binding instanceof java.util.Date) + { + sb.append("new java.util.Date(") + .append(((java.util.Date) binding).getTime()) + .append("L)"); + } + else if (binding instanceof java.math.BigInteger) + sb.append("new java.math.BigInteger(\"").append(binding).append("\")"); + else if (binding instanceof java.math.BigDecimal) + sb.append("new java.math.BigDecimal(\"").append(binding).append("\")"); + else if (binding instanceof java.net.InetAddress) + { + byte[] address = ((InetAddress) binding).getAddress(); + sb.append("java.net.InetAddress.getByAddress(new byte[]{"); + for (int i = 0; i < address.length; i++) { + sb.append(address[i]); + if (i < address.length - 1) sb.append(", "); + } + sb.append("})"); + } + else + sb.append(binding); + // TODO: byte arrays + } + return sb.toString(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/execution/DataTracker.java b/test/harry/main/org/apache/cassandra/harry/execution/DataTracker.java new file mode 100644 index 0000000000..f2a8c9332f --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/DataTracker.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.execution; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.model.Model; + +import static org.apache.cassandra.harry.op.Operations.Kind.CUSTOM; +import static org.apache.cassandra.harry.op.Operations.Kind.SELECT_CUSTOM; +import static org.apache.cassandra.harry.op.Operations.Kind.SELECT_PARTITION; +import static org.apache.cassandra.harry.op.Operations.Kind.SELECT_RANGE; +import static org.apache.cassandra.harry.op.Operations.Kind.SELECT_ROW; + +/** + * Data tracker tracks every operation that was started and finished. + * + * In principle, it should allow operation to get timed out and remain in the "undecided" state. + * However, there is no implementation as of now that supports this. Right now, every operation + * should be started or should be reliably failed / invisible. + */ +public interface DataTracker +{ + void begin(Visit visit); + void end(Visit visit); + + Iterable potentialVisits(long pd); + boolean isFinished(long lts); + boolean allFinished(); + + Set OPS_WITHOUT_EFFECT = Set.of(SELECT_CUSTOM, SELECT_PARTITION, SELECT_ROW, SELECT_RANGE, CUSTOM); + + /** + * Data tracker that only allows partition visits to be done _in sequence_ + */ + class SequentialDataTracker implements DataTracker + { + private final AtomicLong started = new AtomicLong(); + private final AtomicLong finished = new AtomicLong(); + + private Map> partitionVisits = new HashMap<>(); + + public void begin(Visit visit) + { + long prev = started.get(); + Invariants.checkState(prev == 0 || visit.lts == (prev + 1)); + started.set(visit.lts); + for (int i = 0; i < visit.operations.length; i++) + { + Operations.Operation operation = visit.operations[i]; + + // SELECT statements have no effect on the model + if (OPS_WITHOUT_EFFECT.contains(operation.kind())) + continue; + + Operations.PartitionOperation partitionOp = (Operations.PartitionOperation) operation; + partitionVisits.computeIfAbsent(partitionOp.pd, pd_ -> new ArrayList<>()) + .add(new Model.LtsOperationPair(visit.lts, i)); + } + } + + public void end(Visit visit) + { + long current = started.get(); + Invariants.checkState(current == visit.lts, + "Current stated %d, current visit: %d", current, visit.lts); + finished.set(visit.lts); + } + + public Iterable potentialVisits(long pd) + { + Iterable res = partitionVisits.get(pd); + if (res != null) + return res; + + return Collections.emptyList(); + } + + public boolean isFinished(long lts) + { + return finished.get() >= lts; + } + + @Override + public boolean allFinished() + { + return started.get() == finished.get(); + } + } + + // TODO: optimize for sequential accesses + + /** + * Data tracker able to track LTS out of order + */ + class SimpleDataTracker implements DataTracker + { + private final Set started = new HashSet<>(); + private final Set finished = new HashSet<>(); + + private Map> partitionVisits = new HashMap<>(); + + public void begin(Visit visit) + { + started.add(visit.lts); + for (int i = 0; i < visit.operations.length; i++) + { + Operations.Operation operation = visit.operations[i]; + + // SELECT statements have no effect on the model + if (OPS_WITHOUT_EFFECT.contains(operation.kind())) + continue; + + Operations.PartitionOperation partitionOp = (Operations.PartitionOperation) operation; + partitionVisits.computeIfAbsent(partitionOp.pd, pd_ -> new ArrayList<>()) + .add(new Model.LtsOperationPair(visit.lts, i)); + } + } + + public void end(Visit visit) + { + finished.add(visit.lts); + } + + public Iterable potentialVisits(long pd) + { + Iterable res = partitionVisits.get(pd); + if (res != null) + return res; + + return Collections.emptyList(); + } + + public boolean isFinished(long lts) + { + return finished.contains(lts); + } + + @Override + public boolean allFinished() + { + return started.size() == finished.size(); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/execution/InJvmDTestVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/InJvmDTestVisitExecutor.java new file mode 100644 index 0000000000..8362a3111b --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/InJvmDTestVisitExecutor.java @@ -0,0 +1,424 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.execution; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +import com.google.common.base.Throwables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.ICluster; +import org.apache.cassandra.exceptions.RequestTimeoutException; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.utils.AssertionUtils; + +import static org.apache.cassandra.harry.MagicConstants.LTS_UNKNOWN; +import static org.apache.cassandra.harry.MagicConstants.NIL_DESCR; +import static org.apache.cassandra.harry.MagicConstants.NIL_KEY; +import static org.apache.cassandra.harry.MagicConstants.UNKNOWN_DESCR; +import static org.apache.cassandra.harry.MagicConstants.UNSET_DESCR; +import static org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor.*; + +public class InJvmDTestVisitExecutor extends CQLVisitExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(InJvmDTestVisitExecutor.class); + + protected final ICluster cluster; + protected final ConsistencyLevelSelector consistencyLevel; + + protected final NodeSelector nodeSelector; + protected final PageSizeSelector pageSizeSelector; + protected final RetryPolicy retryPolicy; + + protected InJvmDTestVisitExecutor(SchemaSpec schema, + DataTracker dataTracker, + Model model, + ICluster cluster, + + NodeSelector nodeSelector, + PageSizeSelector pageSizeSelector, + RetryPolicy retryPolicy, + ConsistencyLevelSelector consistencyLevel, + WrapQueries wrapQueries) + { + super(schema, dataTracker, model, new QueryBuildingVisitExecutor(schema, wrapQueries)); + this.cluster = cluster; + this.consistencyLevel = consistencyLevel; + + this.nodeSelector = nodeSelector; + this.pageSizeSelector = pageSizeSelector; + this.retryPolicy = retryPolicy; + } + + @Override + protected List executeWithResult(Visit visit, CompiledStatement statement) + { + while (true) + { + try + { + ConsistencyLevel consistencyLevel = this.consistencyLevel.consistencyLevel(visit); + int pageSize = PageSizeSelector.NO_PAGING; + if (consistencyLevel != ConsistencyLevel.NODE_LOCAL) + pageSize = pageSizeSelector.pages(visit); + return executeWithResult(visit, nodeSelector.select(visit.lts), pageSize, statement, consistencyLevel); + } + catch (Throwable t) + { + if (retryPolicy.retry(t)) + continue; + throw t; + } + } + } + + protected List executeWithResult(Visit visit, int node, int pageSize, CompiledStatement statement, ConsistencyLevel consistencyLevel) + { + Invariants.checkState(visit.operations.length == 1); + Object[][] rows; + if (consistencyLevel == ConsistencyLevel.NODE_LOCAL) + rows = cluster.get(node).executeInternal(statement.cql(), statement.bindings()); + else + { + if (pageSize == PageSizeSelector.NO_PAGING) + rows = cluster.coordinator(node).execute(statement.cql(), consistencyLevel, statement.bindings()); + else + rows = iterToArr(cluster.coordinator(node) + .executeWithPaging(statement.cql(), consistencyLevel, pageSize, statement.bindings())); + } + + if (logger.isTraceEnabled()) + logger.trace("{} returned {} results", statement, rows.length); + + return rowsToResultSet(schema, (Operations.SelectStatement) visit.operations[0], rows); + } + + protected static Object[][] iterToArr(Iterator iter) + { + List tmp = new ArrayList<>(); + while (iter.hasNext()) + tmp.add(iter.next()); + return tmp.toArray(new Object[tmp.size()][]); + } + + @Override + protected void executeWithoutResult(Visit visit, CompiledStatement statement) + { + while (true) + { + try + { + ConsistencyLevel consistencyLevel = this.consistencyLevel.consistencyLevel(visit); + executeWithoutResult(visit, nodeSelector.select(visit.lts), statement, consistencyLevel); + return; + } + catch (Throwable t) + { + if (retryPolicy.retry(t)) + continue; + throw t; + } + } + } + + protected void executeWithoutResult(Visit visit, int node, CompiledStatement statement, ConsistencyLevel consistencyLevel) + { + if (consistencyLevel == ConsistencyLevel.NODE_LOCAL) + cluster.get(node).executeInternal(statement.cql(), statement.bindings()); + else + cluster.coordinator(node).execute(statement.cql(), consistencyLevel, statement.bindings()); + } + + public static List rowsToResultSet(SchemaSpec schema, Operations.SelectStatement select, Object[][] result) + { + List rs = new ArrayList<>(); + for (Object[] res : result) + rs.add(rowToResultSet(schema, select, res)); + return rs; + } + + public static ResultSetRow rowToResultSet(SchemaSpec schema, Operations.SelectStatement select, Object[] result) + { + long[] staticColumns = new long[schema.staticColumns.size()]; + long[] regularColumns = new long[schema.regularColumns.size()]; + Arrays.fill(staticColumns, UNKNOWN_DESCR); + Arrays.fill(regularColumns, UNKNOWN_DESCR); + // TODO: with timestamps + long[] staticLts = LTS_UNKNOWN; + long[] regularLts = LTS_UNKNOWN; + + long pd = UNKNOWN_DESCR; + Operations.Selection selection = Operations.Selection.fromBitSet(select.selection(), schema); + if (selection.selectsAllOf(schema.partitionKeys)) + { + Object[] partitionKey = new Object[schema.partitionKeys.size()]; + for (int i = 0; i < schema.partitionKeys.size(); i++) + partitionKey[i] = result[selection.indexOf(schema.partitionKeys.get(i))]; + + pd = schema.valueGenerators.pkGen().deflate(partitionKey); + } + + // Deflate logic for clustering key is a bit more involved, since CK can be nil in case of a single static row. + long cd = UNKNOWN_DESCR; + if (selection.selectsAllOf(schema.clusteringKeys)) + { + Object[] clusteringKey = new Object[schema.clusteringKeys.size()]; + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + Object v = result[selection.indexOf(schema.clusteringKeys.get(i))]; + if (v == null) + { + for (int j = 0; j < schema.clusteringKeys.size(); j++) + { + Invariants.checkState(result[selection.indexOf(schema.clusteringKeys.get(j))] == null, + "All elements of clustering key should have been null"); + } + clusteringKey = NIL_KEY; + break; + } + clusteringKey[i] = v; + } + + // Clusterings can not be set to nil, so if we do not see, we assume it is unset + if (clusteringKey == NIL_KEY) + cd = UNSET_DESCR; + else + cd = schema.valueGenerators.ckGen().deflate(clusteringKey); + } + + for (int i = 0; i < schema.regularColumns.size(); i++) + { + ColumnSpec column = schema.regularColumns.get(i); + if (selection.selects(column)) + { + Object v = result[selection.indexOf(schema.regularColumns.get(i))]; + if (v == null) + regularColumns[i] = NIL_DESCR; + else + regularColumns[i] = schema.valueGenerators.regularColumnGen(i).deflate(v); + } + else + { + regularLts[i] = UNKNOWN_DESCR; + } + } + + for (int i = 0; i < schema.staticColumns.size(); i++) + { + ColumnSpec column = schema.staticColumns.get(i); + if (selection.selects(column)) + { + Object v = result[selection.indexOf(schema.staticColumns.get(i))]; + if (v == null) + staticColumns[i] = NIL_DESCR; + else + staticColumns[i] = schema.valueGenerators.staticColumnGen(i).deflate(v); + } + else + { + staticColumns[i] = UNKNOWN_DESCR; + } + } + + if (selection.includeTimestamps()) + { + long[] slts = new long[schema.staticColumns.size()]; + Arrays.fill(slts, MagicConstants.NO_TIMESTAMP); + for (int i = 0, sltsBase = schema.allColumnInSelectOrder.size(); i < slts.length && sltsBase + i < result.length; i++) + { + Object v = result[schema.allColumnInSelectOrder.size() + i]; + if (v != null) + slts[i] = (long) v; + } + + long[] lts = new long[schema.regularColumns.size()]; + Arrays.fill(lts, MagicConstants.NO_TIMESTAMP); + for (int i = 0, ltsBase = schema.allColumnInSelectOrder.size() + slts.length; i < lts.length && ltsBase + i < result.length; i++) + { + Object v = result[ltsBase + i]; + if (v != null) + lts[i] = (long) v; + } + } + + return new ResultSetRow(pd, cd, staticColumns, staticLts, regularColumns, regularLts); + } + + public interface NodeSelector + { + int select(long lts); + } + + public interface PageSizeSelector + { + int NO_PAGING = -1; + int pages(Visit visit); + } + + public interface ConsistencyLevelSelector + { + ConsistencyLevel consistencyLevel(Visit visit); + } + + public interface RetryPolicy + { + RetryPolicy RETRY_ON_TIMEOUT = (t) -> { + return t.getMessage().contains("timed out") || + AssertionUtils.isInstanceof(RequestTimeoutException.class) + .matches(Throwables.getRootCause(t)); + }; + boolean retry(Throwable t); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + protected ConsistencyLevelSelector consistencyLevel = v -> ConsistencyLevel.QUORUM; + + protected NodeSelector nodeSelector = null; + protected PageSizeSelector pageSizeSelector = (lts) -> 1; + protected RetryPolicy retryPolicy = RetryPolicy.RETRY_ON_TIMEOUT; + protected WrapQueries wrapQueries = WrapQueries.UNLOGGED_BATCH; + + public Builder wrapQueries(WrapQueries wrapQueries) + { + this.wrapQueries = wrapQueries; + return this; + } + + public Builder consistencyLevel(ConsistencyLevel consistencyLevel) + { + this.consistencyLevel = v -> consistencyLevel; + return this; + } + + public Builder consistencyLevel(ConsistencyLevelSelector consistencyLevel) + { + this.consistencyLevel = consistencyLevel; + return this; + } + + public Builder nodeSelector(NodeSelector nodeSelector) + { + this.nodeSelector = nodeSelector; + return this; + } + + public Builder pageSizeSelector(PageSizeSelector pageSizeSelector) + { + this.pageSizeSelector = pageSizeSelector; + return this; + } + + public Builder retryPolicy(RetryPolicy retryPolicy) + { + this.retryPolicy = retryPolicy; + return this; + } + + protected void setDefaults(SchemaSpec schema, ICluster cluster) + { + if (nodeSelector == null) + { + this.nodeSelector = new NodeSelector() + { + final int nodes = cluster.size(); + long counter = 0; + + @Override + public int select(long lts) + { + return (int) (counter++ % nodes + 1); + } + }; + } + } + + public InJvmDTestVisitExecutor build(SchemaSpec schema, Model.Replay replay, ICluster cluster) + { + setDefaults(schema, cluster); + DataTracker tracker = new DataTracker.SequentialDataTracker(); + Model model = new QuiescentChecker(schema.valueGenerators, tracker, replay); + return new InJvmDTestVisitExecutor(schema, tracker, model, cluster, + nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, wrapQueries); + } + + public InJvmDTestVisitExecutor build(SchemaSpec schema, ICluster cluster, Function overrides) + { + setDefaults(schema, cluster); + return overrides.apply(this); + } + + /** + * WARNING: highly experimental + */ + public InJvmDTestVisitExecutor doubleWriting(SchemaSpec schema, Model.Replay replay, ICluster cluster, String secondTable) + { + setDefaults(schema, cluster); + DataTracker tracker = new DataTracker.SequentialDataTracker(); + Model model = new QuiescentChecker(schema.valueGenerators, tracker, replay); + return new InJvmDTestVisitExecutor(schema, tracker, model, cluster, + nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, wrapQueries) + { + @Override + protected List executeWithResult(Visit visit, int node, int pageSize, CompiledStatement statement, ConsistencyLevel consistencyLevel) + { + List rows = super.executeWithResult(visit, node, pageSize, statement, consistencyLevel); + List secondOpinion = super.executeWithResult(visit, node, pageSize, statement.withSchema(schema.keyspace, schema.table, + schema.keyspace, secondTable), + consistencyLevel); + if (!rows.equals(secondOpinion)) + { + logger.debug("Second opinion: "); + for (ResultSetRow resultSetRow : secondOpinion) + logger.debug(resultSetRow.toString(schema.valueGenerators)); + } + return rows; + } + + @Override + protected void executeWithoutResult(Visit visit, int node, CompiledStatement statement, ConsistencyLevel consistencyLevel) + { + super.executeWithoutResult(visit, node, statement, consistencyLevel); + super.executeWithoutResult(visit, node, statement.withSchema(schema.keyspace, schema.table, + schema.keyspace, secondTable), + consistencyLevel); + } + }; + } + } + +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/execution/QueryBuildingVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/QueryBuildingVisitExecutor.java new file mode 100644 index 0000000000..a330007a2a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/QueryBuildingVisitExecutor.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.execution; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.cql.DeleteHelper; +import org.apache.cassandra.harry.cql.SelectHelper; +import org.apache.cassandra.harry.cql.WriteHelper; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; + +// TODO: this class can be substantially improved by removing internal mutable state +public class QueryBuildingVisitExecutor extends VisitExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(QueryBuildingVisitExecutor.class); + protected final SchemaSpec schema; + protected final WrapQueries wrapQueries; + + public QueryBuildingVisitExecutor(SchemaSpec schema, WrapQueries wrapQueries) + { + this.schema = schema; + this.wrapQueries = wrapQueries; + } + + public BuiltQuery compile(Visit visit) + { + beginLts(visit.lts); + for (Operations.Operation op : visit.operations) + { + if (logger.isTraceEnabled()) + logger.trace("{} {}", visit.lts, op); + operation(op); + } + + // TODO: try inducing timeouts and checking non-propagation or discovery + endLts(visit.lts); + + if (!statements.isEmpty()) + { + Object[] bindingsArray = new Object[bindings.size()]; + bindings.toArray(bindingsArray); + BuiltQuery query = new BuiltQuery(selects, + wrapQueries.wrap(visit, String.join("\n ", statements)), + bindingsArray); + clear(); + return query; + } + + Invariants.checkState(bindings.isEmpty() && visitedPds.isEmpty() && selects.isEmpty()); + return null; + } + + public static class BuiltQuery extends CompiledStatement + { + protected final List selects; + public BuiltQuery(List selects, String cql, Object... bindings) + { + super(cql, bindings); + this.selects = selects; + } + } + + /** + * Per-LTS state + */ + private final List statements = new ArrayList<>(); + private final List bindings = new ArrayList<>(); + private final Set visitedPds = new HashSet<>(); + + private List selects = null; + + protected void beginLts(long lts) + { + statements.clear(); + bindings.clear(); + visitedPds.clear(); + selects = new ArrayList<>(); + } + + protected void endLts(long lts) + { + if (statements.isEmpty()) + { + Invariants.checkState(bindings.isEmpty() && visitedPds.isEmpty() && selects.isEmpty()); + return; + } + + assert visitedPds.size() == 1 : String.format("Token aware only works with a single value per token, but got %s", visitedPds); + } + + private void clear() + { + statements.clear(); + bindings.clear(); + visitedPds.clear(); + selects = null; + } + + protected void operation(Operations.Operation operation) + { + if (operation instanceof Operations.PartitionOperation) + visitedPds.add(((Operations.PartitionOperation) operation).pd()); + CompiledStatement statement; + switch (operation.kind()) + { + case UPDATE: + statement = WriteHelper.inflateUpdate((Operations.WriteOp) operation, schema, operation.lts()); + break; + case INSERT: + statement = WriteHelper.inflateInsert((Operations.WriteOp) operation, schema, operation.lts()); + break; + case DELETE_RANGE: + statement = DeleteHelper.inflateDelete((Operations.DeleteRange) operation, schema, operation.lts()); + break; + case DELETE_PARTITION: + statement = DeleteHelper.inflateDelete((Operations.DeletePartition) operation, schema, operation.lts()); + break; + case DELETE_ROW: + statement = DeleteHelper.inflateDelete((Operations.DeleteRow) operation, schema, operation.lts()); + break; + case DELETE_COLUMNS: + statement = DeleteHelper.inflateDelete((Operations.DeleteColumns) operation, schema, operation.lts()); + break; + case SELECT_PARTITION: + statement = SelectHelper.select((Operations.SelectPartition) operation, schema); + selects.add((Operations.SelectStatement) operation); + break; + case SELECT_ROW: + statement = SelectHelper.select((Operations.SelectRow) operation, schema); + selects.add((Operations.SelectStatement) operation); + break; + case SELECT_RANGE: + statement = SelectHelper.select((Operations.SelectRange) operation, schema); + selects.add((Operations.SelectStatement) operation); + break; + case SELECT_CUSTOM: + statement = SelectHelper.select((Operations.SelectCustom) operation, schema); + selects.add((Operations.SelectStatement) operation); + break; + + case CUSTOM: + ((Operations.CustomRunnableOperation) operation).execute(); + return; + default: + throw new IllegalArgumentException(); + } + statements.add(statement.cql()); + Collections.addAll(bindings, statement.bindings()); + } + + private static final String wrapInUnloggedBatchFormat = "BEGIN UNLOGGED BATCH\n" + + " %s\n" + + "APPLY BATCH;"; + + private static final String wrapInTxnFormat = "BEGIN TRANSACTION\n" + + " %s\n" + + "COMMIT TRANSACTION;"; + + public interface WrapQueries + { + WrapQueries UNLOGGED_BATCH = (visit, compiled) -> { + if (visit.operations.length == 1) + return compiled; + return String.format(wrapInUnloggedBatchFormat, compiled); + }; + + WrapQueries TRANSACTION = (visit, compiled) -> String.format(wrapInTxnFormat, compiled); + + + String wrap(Visit visit, String compiled); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/execution/ResultSetRow.java b/test/harry/main/org/apache/cassandra/harry/execution/ResultSetRow.java new file mode 100644 index 0000000000..3bc40dbb1e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/ResultSetRow.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.execution; + +import org.apache.cassandra.harry.gen.Bijections; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.util.StringUtils; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.IntFunction; + +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 ResultSetRow(long pd, + long cd, + long[] sds, + long[] slts, + long[] vds, + long[] lts) + { + this.pd = pd; + this.cd = cd; + this.vds = vds; + this.lts = lts; + this.sds = sds; + this.slts = slts; + } + + 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)); + } + + @Override + public int hashCode() + { + int result = Objects.hash(pd, cd); + 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); + } + + 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) + ")" + + ")"; + } + + private static String toString(long[] descriptors, IntFunction> gens) + { + String[] idxs = new String[descriptors.length]; + for (int i = 0; i < descriptors.length; i++) + idxs[i] = descrToIdx(gens.apply(i), descriptors[i]); + return String.join(",", idxs); + } + + private static String descrToIdx(Bijections.Bijection gen, long descr) + { + return gen.toString(descr); + } + + public String toString(ValueGenerators valueGenerators) + { + return "resultSetRow(" + + valueGenerators.pkGen().toString(pd) + + ", " + descrToIdx(valueGenerators.ckGen(), cd) + + (sds == null ? "" : ", statics(" + toString(sds, valueGenerators::staticColumnGen) + ")") + + (slts == null ? "" : ", slts(" + StringUtils.toString(slts) + ")") + + ", values(" + toString(vds, valueGenerators::regularColumnGen) + ")" + + ", lts(" + StringUtils.toString(lts) + ")" + + ")"; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/execution/RingAwareInJvmDTestVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/RingAwareInJvmDTestVisitExecutor.java new file mode 100644 index 0000000000..b9a6e0565d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/RingAwareInJvmDTestVisitExecutor.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.execution; + + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.ICluster; +import org.apache.cassandra.distributed.api.ICoordinator; +import org.apache.cassandra.distributed.api.IInstance; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.model.TokenPlacementModel; +import org.apache.cassandra.harry.util.ByteUtils; +import org.apache.cassandra.harry.util.TokenUtil; + +/** + * Executes all modifications queries with NODE_LOCAL. Executes all validation queries + * with NODE_LOCAL _on each node_. + */ +public class RingAwareInJvmDTestVisitExecutor extends InJvmDTestVisitExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(InJvmDTestVisitExecutor.class); + + private final TokenPlacementModel.ReplicationFactor rf; + + private RingAwareInJvmDTestVisitExecutor(SchemaSpec schema, + DataTracker dataTracker, + Model model, + ICluster cluster, + NodeSelector nodeSelector, + PageSizeSelector pageSizeSelector, + RetryPolicy retryPolicy, + ConsistencyLevelSelector consistencyLevel, + TokenPlacementModel.ReplicationFactor rf, + QueryBuildingVisitExecutor.WrapQueries wrapQueries) + { + super(schema, dataTracker, model, cluster, nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, QueryBuildingVisitExecutor.WrapQueries.UNLOGGED_BATCH); + this.rf = rf; + } + + protected TokenPlacementModel.ReplicatedRanges getRing() + { + IInstance node = ((Cluster)cluster).firstAlive(); + ICoordinator coordinator = node.coordinator(); + List other = TokenPlacementModel.peerStateToNodes(coordinator.execute("select peer, tokens, data_center, rack from system.peers", ConsistencyLevel.ONE)); + List self = TokenPlacementModel.peerStateToNodes(coordinator.execute("select broadcast_address, tokens, data_center, rack from system.local", ConsistencyLevel.ONE)); + List all = new ArrayList<>(); + all.addAll(self); + all.addAll(other); + all.sort(TokenPlacementModel.Node::compareTo); + return rf.replicate(all); + } + + public List getReplicasFor(long pd) + { + return getRing().replicasFor(token(pd)); + } + + protected long token(long pd) + { + return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(schema.valueGenerators.pkGen().inflate(pd)))); + } + + @Override + protected void executeValidatingVisit(Visit visit, List selects, CompiledStatement statement) + { + try + { + for (TokenPlacementModel.Replica replica : getReplicasFor(selects.get(0).pd)) + { + IInstance instance = cluster + .stream() + .filter((n) -> n.config().broadcastAddress().toString().contains(replica.node().id())) + .findFirst() + .get(); + ConsistencyLevel consistencyLevel = this.consistencyLevel.consistencyLevel(visit); + int pageSize = PageSizeSelector.NO_PAGING; + if (consistencyLevel != ConsistencyLevel.NODE_LOCAL) + pageSize = pageSizeSelector.pages(visit); + List resultSetRow = executeWithResult(visit, instance.config().num(), pageSize, statement, consistencyLevel); + model.validate(selects.get(0), resultSetRow); + } + } + catch (Throwable t) + { + throw new AssertionError(String.format("Caught an exception while validating %s:\n%s", selects.get(0), statement), t); + } + } + + @Override + protected void executeWithoutResult(Visit visit, CompiledStatement statement) + { + try + { + Invariants.checkState(visit.visitedPartitions.size() == 1, + "Ring aware executor can only read and write one partition at a time"); + for (TokenPlacementModel.Replica replica : getReplicasFor(visit.visitedPartitions.iterator().next().longValue())) + { + IInstance instance = cluster + .stream() + .filter((n) -> n.config().broadcastAddress().toString().contains(replica.node().id())) + .findFirst() + .get(); + executeWithoutResult(visit, instance.config().num(), statement, consistencyLevel.consistencyLevel(visit)); + } + } + catch (Throwable t) + { + throw new AssertionError(String.format("Caught an exception while validating %s", statement), t); + } + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder extends InJvmDTestVisitExecutor.Builder + { + protected TokenPlacementModel.ReplicationFactor rf; + + public Builder replicationFactor(TokenPlacementModel.ReplicationFactor rf) + { + this.rf = rf; + return this; + } + + @Override + public Builder wrapQueries(QueryBuildingVisitExecutor.WrapQueries wrapQueries) + { + super.wrapQueries(wrapQueries); + return this; + } + + @Override + public Builder consistencyLevel(ConsistencyLevel consistencyLevel) + { + super.consistencyLevel(consistencyLevel); + return this; + } + + @Override + public Builder nodeSelector(NodeSelector nodeSelector) + { + super.nodeSelector(nodeSelector); + return this; + } + + @Override + public Builder pageSizeSelector(PageSizeSelector pageSizeSelector) + { + super.pageSizeSelector(pageSizeSelector); + return this; + } + + @Override + public Builder retryPolicy(RetryPolicy retryPolicy) + { + super.retryPolicy(retryPolicy); + return this; + } + + @Override + protected void setDefaults(SchemaSpec schema, ICluster cluster) + { + super.setDefaults(schema, cluster); + if (this.rf == null) + { + this.rf = new TokenPlacementModel.SimpleReplicationFactor(Math.min(3, cluster.size())); + } + } + + public RingAwareInJvmDTestVisitExecutor build(SchemaSpec schema, Model.Replay replay, ICluster cluster) + { + DataTracker tracker = new DataTracker.SequentialDataTracker(); + Model model = new QuiescentChecker(schema.valueGenerators, tracker, replay); + return build(schema, tracker, model, cluster); + } + + public RingAwareInJvmDTestVisitExecutor build(SchemaSpec schema, DataTracker tracker, Model model, ICluster cluster) + { + setDefaults(schema, cluster); + return new RingAwareInJvmDTestVisitExecutor(schema, tracker, model, cluster, + nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, rf, wrapQueries); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/ValueOverrides.java b/test/harry/main/org/apache/cassandra/harry/execution/VisitExecutor.java similarity index 69% rename from test/harry/main/org/apache/cassandra/harry/dsl/ValueOverrides.java rename to test/harry/main/org/apache/cassandra/harry/execution/VisitExecutor.java index df1e61e0f3..5d19a29ff7 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/ValueOverrides.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/VisitExecutor.java @@ -16,9 +16,14 @@ * limitations under the License. */ -package org.apache.cassandra.harry.dsl; +package org.apache.cassandra.harry.execution; -public interface ValueOverrides +import org.apache.cassandra.harry.op.Operations; + +// Abstract class and not interface becasue methods are protected +public abstract class VisitExecutor { - void override(String column, int idx, Object override); + protected abstract void beginLts(long lts); + protected abstract void endLts(long lts); + protected abstract void operation(Operations.Operation operation); } diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java b/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java index 9b8d8ada26..b41487303b 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java @@ -18,6 +18,7 @@ package org.apache.cassandra.harry.gen; +import java.util.Comparator; import java.util.Date; import java.util.UUID; @@ -52,7 +53,10 @@ public class Bijections // TODO: byteSize is great, but you know what's better? Bit size! For example, for `boolean`, we only need a single bit. int byteSize(); - + default int population() + { + return byteSize() * Byte.SIZE; + } /** * Compare as if we were comparing the values in question */ @@ -77,9 +81,19 @@ public class Bijections { return false; } + + default Comparator descriptorsComparator() + { + return Long::compare; + } + + default String toString(long pd) + { + return Long.toString(pd); + } } - protected static long minForSize(int size) + public static long minForSize(int size) { long min = 1L << (size * Byte.SIZE - 1); @@ -89,7 +103,7 @@ public class Bijections return min; } - protected static long maxForSize(int size) + public static long maxForSize(int size) { long max = Bytes.bytePatternFor(size) >>> 1; @@ -454,4 +468,4 @@ public class Bijections return Long.BYTES; } } -} \ No newline at end of file +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/BooleanGenerator.java b/test/harry/main/org/apache/cassandra/harry/gen/BooleanGenerator.java deleted file mode 100644 index 53a1529ac5..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/gen/BooleanGenerator.java +++ /dev/null @@ -1,29 +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.harry.gen; - -public class BooleanGenerator implements Generator -{ - public static BooleanGenerator INSTANCE = new BooleanGenerator(); - - public Boolean generate(EntropySource rng) - { - return rng.nextBoolean(); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Collections.java b/test/harry/main/org/apache/cassandra/harry/gen/Collections.java deleted file mode 100644 index 22ff5b6565..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/gen/Collections.java +++ /dev/null @@ -1,249 +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.harry.gen; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.gen.rng.RngUtils; - -// TODO: collections are currently not deflatable and/or checkable with a model -public class Collections -{ - public static ColumnSpec.DataType> mapColumn(ColumnSpec.DataType k, - ColumnSpec.DataType v, - int maxSize) - { - return new ColumnSpec.DataType>(String.format("map<%s,%s>", k.toString(), v.toString())) - { - private final Bijections.Bijection> gen = mapGen(k.generator(), v.generator(), maxSize); - - public Bijections.Bijection> generator() - { - return gen; - } - - public int maxSize() - { - return Long.BYTES; - } - }; - } - - public static ColumnSpec.DataType> listColumn(ColumnSpec.DataType v, - int maxSize) - { - return new ColumnSpec.DataType>(String.format("set<%s>", v.toString())) - { - private final Bijections.Bijection> gen = listGen(v.generator(), maxSize); - - public Bijections.Bijection> generator() - { - return gen; - } - - public int maxSize() - { - return Long.BYTES; - } - }; - } - - - public static ColumnSpec.DataType> setColumn(ColumnSpec.DataType v, - int maxSize) - { - return new ColumnSpec.DataType>(String.format("set<%s>", v.toString())) - { - private final Bijections.Bijection> gen = setGen(v.generator(), maxSize); - - public Bijections.Bijection> generator() - { - return gen; - } - - public int maxSize() - { - return Long.BYTES; - } - }; - } - - - public static Bijections.Bijection> mapGen(Bijections.Bijection keyGen, - Bijections.Bijection valueGen, - int maxSize) - { - return new MapGenerator<>(keyGen, valueGen, maxSize); - } - - public static Bijections.Bijection> listGen(Bijections.Bijection valueGen, - int maxSize) - { - return new ListGenerator<>(valueGen, maxSize); - } - - public static Bijections.Bijection> setGen(Bijections.Bijection valueGen, - int maxSize) - { - return new SetGenerator<>(valueGen, maxSize); - } - - public static class MapGenerator implements Bijections.Bijection> - { - public final Bijections.Bijection keyGen; - public final Bijections.Bijection valueGen; - public int maxSize; - - public MapGenerator(Bijections.Bijection keyGen, - Bijections.Bijection valueGen, - int maxSize) - { - this.keyGen = keyGen; - this.valueGen = valueGen; - this.maxSize = maxSize; - } - - public Map inflate(long descriptor) - { - long rnd = RngUtils.next(descriptor); - int count = RngUtils.asInt(rnd, 0, maxSize); - Map m = new HashMap<>(); - for (int i = 0; i < count; i++) - { - rnd = RngUtils.next(rnd); - K key = keyGen.inflate(rnd); - rnd = RngUtils.next(rnd); - V value = valueGen.inflate(rnd); - m.put(key, value); - } - - return m; - } - - // At least for non-frozen ones - public long deflate(Map value) - { - throw new UnsupportedOperationException(); - } - - public int byteSize() - { - return Long.BYTES; - } - - public int compare(long l, long r) - { - throw new UnsupportedOperationException(); - } - } - - public static class ListGenerator implements Bijections.Bijection> - { - public final Bijections.Bijection valueGen; - public int maxSize; - - public ListGenerator(Bijections.Bijection valueGen, - int maxSize) - { - this.valueGen = valueGen; - this.maxSize = maxSize; - } - - public List inflate(long descriptor) - { - long rnd = RngUtils.next(descriptor); - int count = RngUtils.asInt(rnd, 0, maxSize); - List m = new ArrayList<>(); - for (int i = 0; i < count; i++) - { - rnd = RngUtils.next(rnd); - V value = valueGen.inflate(rnd); - m.add(value); - } - - return m; - } - - // At least for non-frozen ones - public long deflate(List value) - { - throw new UnsupportedOperationException(); - } - - public int byteSize() - { - return Long.BYTES; - } - - public int compare(long l, long r) - { - throw new UnsupportedOperationException(); - } - } - - public static class SetGenerator implements Bijections.Bijection> - { - public final Bijections.Bijection valueGen; - public int maxSize; - - public SetGenerator(Bijections.Bijection valueGen, - int maxSize) - { - this.valueGen = valueGen; - this.maxSize = maxSize; - } - - public Set inflate(long descriptor) - { - long rnd = RngUtils.next(descriptor); - int count = RngUtils.asInt(rnd, 0, maxSize); - Set m = new HashSet<>(); - for (int i = 0; i < count; i++) - { - rnd = RngUtils.next(rnd); - V value = valueGen.inflate(rnd); - m.add(value); - } - - return m; - } - - // At least for non-frozen ones - public long deflate(Set value) - { - throw new UnsupportedOperationException(); - } - - public int byteSize() - { - return Long.BYTES; - } - - public int compare(long l, long r) - { - throw new UnsupportedOperationException(); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/DataGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/DataGenerators.java deleted file mode 100644 index 39c0c80baf..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/gen/DataGenerators.java +++ /dev/null @@ -1,511 +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.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; - // Empty value, for the types that support it - public static long EMPTY_VALUE = Long.MIN_VALUE + 1; - - public static Object[] inflateData(List> columns, long[] descriptors) - { - // This can be not true depending on how we implement subselections - assert columns.size() == descriptors.length; - Object[] data = new Object[descriptors.length]; - for (int i = 0; i < descriptors.length; i++) - { - ColumnSpec columnSpec = columns.get(i); - if (descriptors[i] == UNSET_DESCR) - data[i] = UNSET_VALUE; - else if (descriptors[i] == NIL_DESCR) - data[i] = null; - else - data[i] = columnSpec.inflate(descriptors[i]); - } - return data; - } - - public static long[] deflateData(List> columns, Object[] data) - { - // This can be not true depending on how we implement subselections - assert columns.size() == data.length; - long[] descriptors = new long[data.length]; - for (int i = 0; i < descriptors.length; i++) - { - ColumnSpec columnSpec = columns.get(i); - if (data[i] == null) - descriptors[i] = NIL_DESCR; - else if (data[i] == UNSET_VALUE) - descriptors[i] = UNSET_DESCR; - else - descriptors[i] = columnSpec.deflate(data[i]); - } - return descriptors; - } - - public static int[] requiredBytes(List> columns) - { - switch (columns.size()) - { - case 0: - throw new RuntimeException("Can't inflate empty data column set as it is not inversible"); - case 1: - return new int[]{ Math.min(columns.get(0).type.maxSize(), Long.SIZE) }; - default: - class Pair - { - final int idx, maxSize; - - Pair(int idx, int maxSize) - { - this.idx = idx; - this.maxSize = maxSize; - } - } - int[] bytes = new int[Math.min(KeyGenerator.MAX_UNIQUE_PREFIX_COLUMNS, columns.size())]; - Pair[] sorted = new Pair[bytes.length]; - for (int i = 0; i < sorted.length; i++) - sorted[i] = new Pair(i, columns.get(i).type.maxSize()); - - int remainingBytes = Long.BYTES; - int slotSize = remainingBytes / bytes.length; - // first pass: give it at most a slot number of bytes - for (int i = 0; i < sorted.length; i++) - { - int size = sorted[i].maxSize; - int allotedSize = Math.min(size, slotSize); - remainingBytes -= allotedSize; - bytes[sorted[i].idx] = allotedSize; - } - - // sliced evenly - if (remainingBytes == 0) - return bytes; - - // second pass: try to occupy remaining bytes - // it is possible to improve the second pass and separate additional bytes evenly, but it is - // questionable how much it'll bring since it does not change the total amount of entropy. - for (int i = 0; i < sorted.length; i++) - { - if (remainingBytes == 0) - break; - Pair p = sorted[i]; - if (bytes[p.idx] < p.maxSize) - { - int allotedSize = Math.min(p.maxSize - bytes[p.idx], remainingBytes); - remainingBytes -= allotedSize; - bytes[p.idx] += allotedSize; - } - } - - return bytes; - } - } - - public static Object[] inflateKey(List> columns, long descriptor, long[] slices) - { - assert columns.size() >= slices.length : String.format("Columns: %s. Slices: %s", columns, Arrays.toString(slices)); - assert columns.size() > 0 : "Can't deflate from empty columnset"; - - Object[] res = new Object[columns.size()]; - for (int i = 0; i < slices.length; i++) - { - ColumnSpec spec = columns.get(i); - res[i] = spec.inflate(slices[i]); - } - - // The rest can be random, since prefix is always fixed - long current = descriptor; - for (int i = slices.length; i < columns.size(); i++) - { - current = RngUtils.next(current); - res[i] = columns.get(i).inflate(current); - } - - return res; - } - - public static long[] deflateKey(List> columns, Object[] values) - { - assert columns.size() == values.length : String.format("%s != %s", columns.size(), values.length); - assert columns.size() > 0 : "Can't deflate from empty columnset"; - - int fixedPart = Math.min(KeyGenerator.MAX_UNIQUE_PREFIX_COLUMNS, columns.size()); - - long[] slices = new long[fixedPart]; - boolean allNulls = true; - for (int i = 0; i < fixedPart; i++) - { - ColumnSpec spec = columns.get(i); - Object value = values[i]; - if (value != null) - allNulls = false; - - slices[i] = value == null ? NIL_DESCR : spec.deflate(value); - } - - if (allNulls) - return null; - - return slices; - } - - public static KeyGenerator createKeyGenerator(List> columns) - { - switch (columns.size()) - { - case 0: - return EMPTY_KEY_GEN; - case 1: - return new SinglePartKeyGenerator(columns); - default: - return new MultiPartKeyGenerator(columns); - } - } - - private static final KeyGenerator EMPTY_KEY_GEN = new KeyGenerator(Collections.emptyList()) - { - private final long[] EMPTY_SLICED = new long[0]; - private final Object[] EMPTY_INFLATED = new Object[0]; - - public long[] slice(long descriptor) - { - return EMPTY_SLICED; - } - - public long stitch(long[] parts) - { - return 0; - } - - public long minValue(int idx) - { - return 0; - } - - public long maxValue(int idx) - { - return 0; - } - - @Override - public Object[] inflate(long descriptor) - { - return EMPTY_INFLATED; - } - - @Override - public long deflate(Object[] value) - { - return 0; - } - - public long adjustEntropyDomain(long descriptor) - { - return 0; - } - - public int byteSize() - { - return 0; - } - - public int compare(long l, long r) - { - return 0; - } - }; - - public static abstract class KeyGenerator implements Bijections.Bijection - { - // Maximum number of columns that uniquely identify the value (i.e. use entropy bits). - // Subsequent columns will have random data in them. - public static final int MAX_UNIQUE_PREFIX_COLUMNS = 4; - @VisibleForTesting - public final List> columns; - - protected KeyGenerator(List> columns) - { - this.columns = columns; - } - - public abstract long[] slice(long descriptor); - - public abstract long stitch(long[] parts); - - public long minValue() - { - return Bijections.minForSize(byteSize()); - } - - public long maxValue() - { - return Bijections.maxForSize(byteSize()); - } - - /** - * Min value for a segment: 0, possibly with an inverted 0 sign for stitching. - */ - public abstract long minValue(int idx); - public abstract long maxValue(int idx); - } - - public static class SinglePartKeyGenerator extends KeyGenerator - { - private final Bijections.Bijection keyGen; - private final int totalSize; - - public SinglePartKeyGenerator(List> columns) - { - super(columns); - assert columns.size() == 1; - this.keyGen = columns.get(0).generator(); - this.totalSize = keyGen.byteSize(); - } - - public long[] slice(long descriptor) - { - if (shouldInvertSign()) - descriptor ^= Bytes.signMaskFor(byteSize()); - - descriptor = adjustEntropyDomain(descriptor); - return new long[]{ descriptor }; - } - - public long stitch(long[] parts) - { - long descriptor = parts[0]; - - if (shouldInvertSign()) - descriptor ^= Bytes.signMaskFor(byteSize()); - - return adjustEntropyDomain(descriptor); - } - - public long minValue(int idx) - { - assert idx == 0; - return keyGen.minValue(); - } - - public long maxValue(int idx) - { - assert idx == 0; - return keyGen.maxValue(); - } - - public Object[] inflate(long descriptor) - { - long[] sliced = slice(descriptor); - return new Object[]{ keyGen.inflate(sliced[0]) }; - } - - public boolean shouldInvertSign() - { - return totalSize != Long.BYTES && !keyGen.unsigned(); - } - - public long deflate(Object[] value) - { - Object v = value[0]; - if (v == null) - return NIL_DESCR; - long descriptor = keyGen.deflate(v); - return stitch(new long[] { descriptor }); - } - - public int byteSize() - { - return totalSize; - } - - public int compare(long l, long r) - { - return Long.compare(l, r); - } - } - - public static class MultiPartKeyGenerator extends KeyGenerator - { - @VisibleForTesting - public final int[] sizes; - protected final int totalSize; - - public MultiPartKeyGenerator(List> columns) - { - super(columns); - assert columns.size() > 1 : "It makes sense to use a multipart generator if you have more than one column, but you have " + columns.size(); - - this.sizes = requiredBytes(columns); - int total = 0; - for (int size : sizes) - total += size; - - this.totalSize = total; - } - - public long deflate(Object[] values) - { - long[] stiched = DataGenerators.deflateKey(columns, values); - if (stiched == null) - return NIL_DESCR; - return stitch(stiched); - } - - public Object[] inflate(long descriptor) - { - return DataGenerators.inflateKey(columns, descriptor, slice(descriptor)); - } - - // Checks whether we need to invert a slice sign to preserve order of the sliced descriptor - public boolean shouldInvertSign(int idx) - { - Bijections.Bijection gen = columns.get(idx).generator(); - - int maxSliceSize = gen.byteSize(); - int actualSliceSize = sizes[idx]; - - if (idx == 0) - { - // We consume a sign of a descriptor (long, long), (int, int), etc. - if (totalSize == Long.BYTES) - { - // If we use only 3 bytes for a 4-byte int, or 4 bytes for a 8-byte int, - // they're effectively unsigned/byte-ordered, so their order won't match - if (maxSliceSize > actualSliceSize) - return true; - // Sign of the current descriptor should match the sign of the slice. - // For example, (tinyint, double) or (double, tinyint). In the first case (tinyint first), - // sign of the first component is going to match the sign of the descriptor. - // In the second case (double first), double is 7-bit, but its most significant bit - // does not hold a sign, so we have to invert it to match sign of the descriptor. - else - return gen.unsigned(); - } - // We do not consume a sign of a descriptor (float, tinyint), (int, tinyint), etc, - // so we have to only invert signs of the values, since their order doesn't match. - else - { - assert maxSliceSize == actualSliceSize; - return !gen.unsigned(); - } - } - else if (gen.unsigned()) - return false; - else - // We invert sign of all subsequent chunks if they have enough entropy to have a sign bit set - return maxSliceSize == actualSliceSize; - } - - public long[] slice(long descriptor) - { - long[] pieces = new long[columns.size()]; - 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; - } - - // The rest can be random, since prefix is always fixed - long current = descriptor; - for (int i = sizes.length; i < columns.size(); i++) - { - current = RngUtils.next(current); - pieces[i] = columns.get(i).generator().adjustEntropyDomain(current); - } - - return pieces; - } - - public long stitch(long[] parts) - { - long stitched = 0; - int consumed = 0; - for (int i = sizes.length - 1; i >= 0; i--) - { - int size = sizes[i]; - long piece = parts[i]; - - if (shouldInvertSign(i)) - piece ^= Bytes.signMaskFor(size); - - piece &= Bytes.bytePatternFor(size); - stitched |= piece << (consumed * Byte.SIZE); - consumed += size; - } - return stitched; - } - - public long minValue(int idx) - { - long res = columns.get(idx).generator().minValue(); - // Inverting sign is important for range queries and RTs, since we're - // making boundaries that'll be stitched later. - if (shouldInvertSign(idx)) - res ^= Bytes.signMaskFor(sizes[idx]); - return res; - } - - public long maxValue(int idx) - { - long res = columns.get(idx).generator().maxValue(); - if (shouldInvertSign(idx)) - res ^= Bytes.signMaskFor(sizes[idx]); - return res; - } - - public int byteSize() - { - return totalSize; - } - - public int compare(long l, long r) - { - return Long.compare(l, r); - } - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java b/test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java index d6087893bc..0f9308516f 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java @@ -42,6 +42,7 @@ public interface EntropySource int nextInt(int max); int nextInt(int min, int max); float nextFloat(); + double nextDouble(); /** * Code is adopted from a similar method in JDK 17, and has to be removed as soon as we migrate to JDK 17. diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Generators.java b/test/harry/main/org/apache/cassandra/harry/gen/Generators.java index ed966a96ed..ca2e5f2749 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/Generators.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/Generators.java @@ -18,19 +18,316 @@ package org.apache.cassandra.harry.gen; +import java.math.BigDecimal; +import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.function.Supplier; +import accord.utils.Invariants; +import org.apache.cassandra.harry.util.BitSet; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.TimeUUID; public class Generators { + public static Generator bitSet(int size) + { + return rng -> { + BitSet bitSet = BitSet.allUnset(size); + for (int i = 0; i < size; i++) + if (rng.nextBoolean()) + bitSet.set(i); + return bitSet; + }; + } + + public static Generator ascii(int minLength, int maxLength) + { + return new StringGenerator(minLength, maxLength, 0, 127); + } + + public static Generator utf8(int minLength, int maxLength) + { + return rng -> { + int length = rng.nextInt(minLength, maxLength); + int[] codePoints = new int[length]; + for (int i = 0; i < length; i++) + { + int next; + // Exclude surrogate range, generate values before and after it + if (rng.nextBoolean()) + next = rng.nextInt(0x0000, 0xD800); + else + next = rng.nextInt(0xD801, 0xDFFF); + codePoints[i] = next; + } + + return new String(codePoints, 0, codePoints.length); + }; + } + + public static Generator englishAlphabet(int minLength, int maxLength) + { + return new StringGenerator(minLength, maxLength, 97, 122); + } + + public static Generator int8() + { + return rng -> (byte) rng.nextInt(); + } + + public static Generator int16() + { + return rng -> (short) rng.nextInt(); + } + + public static Generator int32() + { + return EntropySource::nextInt; + } + + public static Generator int32(int min, int max) + { + return rng -> rng.nextInt(min, max); + } + + public static Generator int64() + { + return new LongGenerator(); + } + + public static Generator int64(long min, long max) + { + return rng -> rng.nextLong(min, max); + } + + public static Generator bool() + { + return EntropySource::nextBoolean; + } + + public static Generator doubles() + { + return EntropySource::nextDouble; + } + + public static Generator floats() + { + return EntropySource::nextFloat; + } + + public static Generator inetAddr() + { + return new InetAddressGenerator(); + } + + public static Generator inetAddr(Generator delegate) + { + return new UniqueGenerator<>(delegate, 10); + } + + public static Generator bytes(int minSize, int maxSize) + { + return rng -> { + int size = rng.nextInt(minSize, maxSize); + byte[] bytes = new byte[size]; + for (int i = 0; i < size; ) + for (long v = rng.next(), + n = Math.min(size - i, Long.SIZE / Byte.SIZE); + n-- > 0; v >>= Byte.SIZE) + bytes[i++] = (byte) v; + return ByteBuffer.wrap(bytes); + }; + } + + public static Generator uuidGen() + { + return rng -> { + long msb = rng.next(); + // Adopted from JDK code, UUID#randomUUID + // randomBytes[6] &= 0x0f; /* clear version */ + msb &= ~(0xFL << 12); + // randomBytes[6] |= 0x40; /* set to version 4 */ + msb |= (0x40L << 8); + long lsb = rng.next(); + // randomBytes[8] &= 0x3f; /* clear variant */ + lsb &= ~(0x3L << 62); + // randomBytes[8] |= 0x80; /* set to IETF variant */ + lsb |= (0x2L << 62); + return new UUID(msb, lsb); + }; + } + + public static Generator bigInt() + { + return rng -> BigInteger.valueOf(rng.next()); + } + + public static Generator bigDecimal() + { + return rng -> BigDecimal.valueOf(rng.next()); + } + + public static Generator timeuuid() + { + return new Generator() + { + public TimeUUID generate(EntropySource rng) + { + return TimeUUID.atUnixMicrosWithLsb(rng.nextLong(0, Long.MAX_VALUE), + makeClockSeqAndNode(rng)); + } + + private long makeClockSeqAndNode(EntropySource rng) + { + long clock = rng.nextInt(); + + long lsb = 0; + lsb |= 0x8000000000000000L; // variant (2 bits) + lsb |= (clock & 0x0000000000003FFFL) << 48; // clock sequence (14 bits) + lsb |= makeNode(rng); // 6 bytes + return lsb; + } + + private long makeNode(EntropySource rng) + { + // ideally, we'd use the MAC address, but java doesn't expose that. + long v = rng.next(); + byte[] hash = new byte[] { (byte) (0xff & v), + (byte) (0xff & (v << 8)), + (byte) (0xff & (v << 16)), + (byte) (0xff & (v << 24)), + (byte) (0xff & (v << 32)), + (byte) (0xff & (v << 40)) + }; + long node = 0; + for (int i = 0; i < 6; i++) + node |= (0x00000000000000ff & (long)hash[i]) << (5-i)*8; + assert (0xff00000000000000L & node) == 0; + + return node | 0x0000010000000000L; + } + }; + } + + public static TrackingGenerator tracking(Generator delegate) + { + return new TrackingGenerator<>(delegate); + } + + public static class TrackingGenerator implements Generator + { + private final Set generated; + private final Generator delegate; + public TrackingGenerator(Generator delegate) + { + this.generated = new HashSet<>(); + this.delegate = delegate; + } + + public Iterable generated() + { + return generated; + } + + @Override + public T generate(EntropySource rng) + { + T next = delegate.generate(rng); + generated.add(next); + return next; + } + } + + public static Generator unique(Generator delegate) + { + return new UniqueGenerator<>(delegate, 100); + } + + + /** + * WARNING: uses hash code as a proxy for equality + */ + public static class UniqueGenerator implements Generator + { + private final Set hashCodes = new HashSet<>(); + private final Generator delegate; + private final int maxSteps; + + public UniqueGenerator(Generator delegate, int maxSteps) + { + this.delegate = delegate; + this.maxSteps = maxSteps; + } + + /** + * + */ + public void clear() + { + hashCodes.clear(); + } + + public T generate(EntropySource rng) + { + for (int i = 0; i < maxSteps; i++) + { + T v = delegate.generate(rng); + int hashCode = v.hashCode(); + Invariants.checkState(hashCode != System.identityHashCode(v), "hashCode was not overridden for type %s", v.getClass()); + if (hashCodes.contains(hashCode)) + continue; + hashCodes.add(hashCode); + return v; + } + + throw new IllegalStateException(String.format("Could not generate a unique value within %d from %s", maxSteps, delegate)); + } + } + + public static final class StringGenerator implements Generator + { + private final int minLength; + private final int maxLength; + private final int minChar; + private final int maxChar; + + public StringGenerator(int minLength, int maxLength, int minChar, int maxChar) + { + this.minLength = minLength; + this.maxLength = maxLength; + this.minChar = minChar; + this.maxChar = maxChar; + } + + @Override + public String generate(EntropySource rng) + { + int length = rng.nextInt(minLength, maxLength); + int[] codePoints = new int[length]; + for (int i = 0; i < length; i++) + codePoints[i] = rng.nextInt(minChar, maxChar); + return new String(codePoints, 0, codePoints.length); + } + } + + public static final class LongGenerator implements Generator + { + @Override + public Long generate(EntropySource rng) + { + return rng.next(); + } + } + public static class InetAddrAndPortGenerator implements Generator { private final int port; @@ -49,9 +346,9 @@ public class Generators { int orig = rng.nextInt(); byte[] bytes = new byte[]{ (byte) (orig & 0xff), - (byte) (orig << 8 & 0xff), - (byte) (orig << 16 & 0xff), - (byte) (orig << 24 & 0xff) }; + (byte) ((orig >> 8) & 0xff), + (byte) ((orig >> 16) & 0xff), + (byte) ((orig >> 24) & 0xff) }; try { return InetAddressAndPort.getByAddressOverrideDefaults(InetAddress.getByAddress(bytes), bytes, port); @@ -63,6 +360,27 @@ public class Generators } } + public static class InetAddressGenerator implements Generator + { + @Override + public InetAddress generate(EntropySource rng) + { + int orig = rng.nextInt(); + byte[] bytes = new byte[]{ (byte) (orig & 0xff), + (byte) ((orig >> 8) & 0xff), + (byte) ((orig >> 16) & 0xff), + (byte) ((orig >> 24) & 0xff) }; + try + { + return InetAddress.getByAddress(bytes); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + } + public static Generator pick(List ts) { if (ts.isEmpty()) @@ -75,6 +393,29 @@ public class Generators return pick(Arrays.asList(ts)); } + public static Generator> list(int minSize, int maxSize, Generator gen) + { + return rng -> { + List objects = new ArrayList<>(); + int size = rng.nextInt(minSize, maxSize); + for (int i = 0; i < size; i++) + objects.add(gen.generate(rng)); + + return objects; + }; + } + + public static Generator zipArray(Generator... gens) + { + return rng -> { + Object[] objects = new Object[gens.length]; + for (int i = 0; i < objects.length; i++) + objects[i] = gens[i].generate(rng); + + return objects; + }; + } + public static Generator> subsetGenerator(List list) { return subsetGenerator(list, 0, list.size() - 1); diff --git a/test/harry/main/org/apache/cassandra/harry/gen/IndexGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/IndexGenerators.java new file mode 100644 index 0000000000..9aeadf3573 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/IndexGenerators.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.cassandra.harry.MagicConstants; + +public class IndexGenerators +{ + private final ValueGenerators valueGenerators; + public final Generator pkIdxGen; + public final Generator ckIdxGen; + public final Generator[] regularIdxGens; + public final Generator[] staticIdxGens; + + public static IndexGenerators withDefaults(ValueGenerators valueGenerators) + { + Generator[] regularIdxGens = new Generator[valueGenerators.regularColumnGens.size()]; + Generator[] staticIdxGens = new Generator[valueGenerators.staticColumnGens.size()]; + + for (int i = 0; i < regularIdxGens.length; i++) + { + int column = i; + regularIdxGens[i] = (rng) -> rng.nextInt(valueGenerators.regularPopulation(column)); + } + + for (int i = 0; i < staticIdxGens.length; i++) + { + int column = i; + staticIdxGens[i] = (rng) -> rng.nextInt(valueGenerators.staticPopulation(column)); + } + + return new IndexGenerators(valueGenerators, + // TODO: distribution for visits + Generators.int32(0, valueGenerators.pkPopulation()), + Generators.int32(0, valueGenerators.ckPopulation()), + regularIdxGens, + staticIdxGens); + } + + public IndexGenerators(ValueGenerators valueGenerators, + Generator pkIdxGen, + Generator ckIdxGen, + Generator[] regularIdxGens, + Generator[] staticIdxGens) + { + this.valueGenerators = valueGenerators; + this.pkIdxGen = pkIdxGen; + this.ckIdxGen = ckIdxGen; + this.regularIdxGens = regularIdxGens; + this.staticIdxGens = staticIdxGens; + + } + + public IndexGenerators roundRobinPk() + { + Generator pkIdxGen = new Generator() + { + int offset = 0; + @Override + public Integer generate(EntropySource rng) + { + int next = offset++; + + if (offset < 0) + offset = 0; + + return next % valueGenerators.pkPopulation(); + } + }; + + return new IndexGenerators(valueGenerators, + pkIdxGen, + ckIdxGen, + regularIdxGens, + staticIdxGens); + } + + public IndexGenerators trackPk() + { + if (pkIdxGen instanceof Generators.TrackingGenerator) + return this; + + return new IndexGenerators(valueGenerators, + Generators.tracking(pkIdxGen), + ckIdxGen, + regularIdxGens, + staticIdxGens); + } + + public IndexGenerators withChanceOfUnset(double chanceOfUnset) + { + Generator[] regularIdxGens = new Generator[valueGenerators.regularColumnGens.size()]; + Generator[] staticIdxGens = new Generator[valueGenerators.staticColumnGens.size()]; + + for (int i = 0; i < regularIdxGens.length; i++) + { + int column = i; + regularIdxGens[i] = (rng) -> rng.nextDouble() <= chanceOfUnset ? MagicConstants.UNSET_IDX : rng.nextInt(valueGenerators.regularPopulation(column)); + } + + for (int i = 0; i < staticIdxGens.length; i++) + { + int column = i; + staticIdxGens[i] = (rng) -> rng.nextDouble() <= chanceOfUnset ? MagicConstants.UNSET_IDX : rng.nextInt(valueGenerators.staticPopulation(column)); + } + + return new IndexGenerators(valueGenerators, + // TODO: distribution for visits + Generators.int32(0, valueGenerators.pkPopulation()), + Generators.int32(0, valueGenerators.ckPopulation()), + regularIdxGens, + staticIdxGens); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/InvertibleGenerator.java b/test/harry/main/org/apache/cassandra/harry/gen/InvertibleGenerator.java new file mode 100644 index 0000000000..8c023e5c8c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/InvertibleGenerator.java @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import accord.utils.Invariants; +import org.agrona.collections.IntHashSet; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; +import org.apache.cassandra.utils.ArrayUtils; + +/** + * Invertible generator allows you to provide _any_ data type. Harry is based on the idea that descriptors + * can be inflated into values, and values can be turned back into descriptors. Descriptors follow the sorting + * order of the values they were generated from. This makes _writing_ these generators a bit more complex. + * There is a library of lightweight generators available for simple cases. + * + * InvertibleGenerator decouples descriptor order from value order, and allows descriptor to be used simply as + * a seed for generating values. Since it tracks all descriptors it generated values from in a sorted order, + * it can always turn the given value back into a descriptor by inflating log(population) values and comparing them + * to the searched value. In other words, it trades memory required for storing map of values to CPU required + * to re-compute the value order. + * + * TODO (expected): custom invertible generator for bool, u8, u16, u32, etc, for efficiency. + * TODO (expected): implement support for tuple/vector/udt, and other multi-cell types. + */ +public class InvertibleGenerator implements HistoryBuilder.IndexedBijection +{ + public static long MAX_ENTROPY = 1L << 63; + + private static final boolean PARANOIA = true; + + // TODO (required): switch to use a primitive array; will need to implement a sort comparator for primitive types + private final List allocatedDescriptors; + + private final Generator gen; + private final Comparator comparator; + + // To avoid erased types + public static InvertibleGenerator fromType(EntropySource rng, int population, ColumnSpec spec) + { + return new InvertibleGenerator<>(rng, spec.type.typeEntropy(), population, spec.gen, spec.type.comparator()); + } + + public InvertibleGenerator(EntropySource rng, + /* unsigned */ long typeEntropy, + int population, + Generator gen, + Comparator comparator) + { + Invariants.checkState(population > 0, + "Population should be strictly positive %d", population); + Invariants.checkState(Long.compareUnsigned(typeEntropy, 0) > 0, + "Type entropy should be strictly positive, but was %d: %s", typeEntropy, gen); + + // We can / will generate at most that many values + if (Long.compareUnsigned(typeEntropy, Integer.MAX_VALUE) > 0) + typeEntropy = Integer.MAX_VALUE; + + population = (int) Math.min(typeEntropy, population); + + this.gen = gen; + this.comparator = comparator; + this.allocatedDescriptors = new ArrayList<>(); + + // Generate a population of _unique_ values. We do not want to store all values, only their hashes. + IntHashSet hashes = new IntHashSet(population); + while (allocatedDescriptors.size() < population) + { + long candidate = rng.next(); + + // Should never allocate these, however improbable that is + if (MagicConstants.MAGIC_DESCRIPTOR_VALS.contains(candidate)) + continue; + + Object inflated = inflate(candidate); + int hash = ArrayUtils.hashCode(inflated); + Invariants.checkState(hash != System.identityHashCode(inflated), "hashCode was not overridden for type %s", inflated.getClass()); + + if (hashes.add(hash)) + allocatedDescriptors.add(candidate); + } + hashes.clear(); + + allocatedDescriptors.sort(this::compare); + + // Check there are no duplicates, and items are properly sorted. + if (PARANOIA) + { + T prev = inflate(allocatedDescriptors.get(0)); + for (int i = 1; i < allocatedDescriptors.size(); i++) + { + T current = inflate(allocatedDescriptors.get(i)); + Invariants.checkState( comparator.compare(current, prev) > 0, + () -> String.format("%s should be strictly after %s", prev, current)); + } + } + } + + @Override + public int idxFor(long descriptor) + { + return Collections.binarySearch(allocatedDescriptors, descriptor, this.descriptorsComparator()); + } + + @Override + public long descriptorAt(int idx) + { + return allocatedDescriptors.get(idx); + } + + @Override + public T inflate(long descriptor) + { + Invariants.checkState(!MagicConstants.MAGIC_DESCRIPTOR_VALS.contains(descriptor), + String.format("Should not be able to inflate %d, as it's magic value", descriptor)); + return SeedableEntropySource.computeWithSeed(descriptor, gen::generate); + } + + @Override + public long deflate(T value) + { + final int idx = binarySearch(value); + if (PARANOIA) + { + if (idx < 0) + { + for (long descriptor : allocatedDescriptors) + { + Object expected = inflate(descriptor); + if (value.getClass().isArray()) + { + Object[] valueArr = (Object[]) value; + Object[] expectedArr = (Object[]) expected; + Invariants.checkState(comparator.compare((T) expected, value) != 0, + "%s was found: %s", Arrays.toString(expectedArr), Arrays.toString(valueArr)); + + } + else + { + Invariants.checkState(comparator.compare((T) expected, value) != 0, + "%s was found: %s", expected, value); + } + + } + } + else + { + long res = allocatedDescriptors.get(idx); + Object expected = inflate(res); + if (value.getClass().isArray()) + { + Object[] valueArr = (Object[]) value; + Object[] expectedArr = (Object[]) expected; + + Invariants.checkState(comparator.compare((T) expected, value) == 0, + "%s != %s", Arrays.toString(expectedArr), Arrays.toString(valueArr)); + + } + else + { + Invariants.checkState(comparator.compare((T) expected, value) == 0, + "%s != %s", expected, value); + } + + return res; + } + } + + if (idx < 0) + { + int start = Math.max(0, idx - 2); + List nearby = new ArrayList<>(); + for (int i = start; i < start + 2; i++) + nearby.add(inflate(allocatedDescriptors.get(i))); + throw new IllegalStateException(String.format("Could not find: %s\nNearby objects: %s", + ArrayUtils.toString(value), nearby.stream().map(ArrayUtils::toString).collect(Collectors.toList()))); + } + + return allocatedDescriptors.get(idx); + } + + + @Override + public int byteSize() + { + return Long.BYTES; + } + + private int binarySearch(T key) + { + int low = 0, mid = allocatedDescriptors.size(), high = mid - 1, result = -1; + while (low <= high) + { + mid = (low + high) >>> 1; + result = comparator.compare(key, inflate(allocatedDescriptors.get(mid))); + if (result > 0) + low = mid + 1; + else if (result == 0) + return mid; + else + high = mid - 1; + } + return -mid - (result < 0 ? 1 : 2); + } + + + @Override + public int compare(long d1, long d2) + { + if (d1 == d2) + return 0; + T v1 = inflate(d1); + T v2 = inflate(d2); + return comparator.compare(v1, v2); + } + + /** + * Returns a number of allocated descriptors + */ + @Override + public int population() + { + return allocatedDescriptors.size(); + } + + public Comparator descriptorsComparator() + { + // TODO: this can be cached + Map descriptorToIdx = new HashMap<>(); + for (int i = 0; i < allocatedDescriptors.size(); i++) + descriptorToIdx.put(allocatedDescriptors.get(i), i); + return Comparator.comparingInt(descriptorToIdx::get); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/OperationsGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/OperationsGenerators.java new file mode 100644 index 0000000000..33b831bfb6 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/OperationsGenerators.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.op.Operations; + +public class OperationsGenerators +{ + public static Generator lts() + { + return new Generator<>() + { + long counter = 0; + + @Override + public Long generate(EntropySource rng) + { + return counter++; + } + }; + } + + // TODO: distributions + public static Generator sequentialPd(SchemaSpec schema) + { + // TODO: switch away from Indexed generators here + HistoryBuilder.IndexedValueGenerators valueGenerators = (HistoryBuilder.IndexedValueGenerators) schema.valueGenerators; + int population = valueGenerators.pkPopulation(); + + return new Generator<>() + { + int counter = 0; + + @Override + public Long generate(EntropySource rng) + { + return valueGenerators.pkGen().descriptorAt(counter++ % population); + } + }; + } + + public static Generator sequentialCd(SchemaSpec schema) + { + // TODO: switch away from Indexed generators here + HistoryBuilder.IndexedValueGenerators valueGenerators = (HistoryBuilder.IndexedValueGenerators) schema.valueGenerators; + int population = valueGenerators.ckPopulation(); + + return new Generator<>() + { + int counter = 0; + + @Override + public Long generate(EntropySource rng) + { + return valueGenerators.ckGen().descriptorAt(counter++ % population); + } + }; + } + + public static Generator writeOp(SchemaSpec schema) + { + return writeOp(schema, sequentialPd(schema), sequentialCd(schema)); + } + + // TODO: chance of unset + public static Generator writeOp(SchemaSpec schema, + Generator pdGen, + Generator cdGen) + { + // TODO: switch away from Indexed generators here + HistoryBuilder.IndexedValueGenerators valueGenerators = (HistoryBuilder.IndexedValueGenerators) schema.valueGenerators; + + return (rng) -> { + long pd = pdGen.generate(rng); + long cd = cdGen.generate(rng); + long[] vds = new long[schema.regularColumns.size()]; + for (int i = 0; i < schema.regularColumns.size(); i++) + { + int idx = rng.nextInt(valueGenerators.regularPopulation(i)); + vds[i] = valueGenerators.regularColumnGen(i).descriptorAt(idx); + } + long[] sds = new long[schema.staticColumns.size()]; + for (int i = 0; i < schema.staticColumns.size(); i++) + { + int idx = rng.nextInt(valueGenerators.staticPopulation(i)); + sds[i] = valueGenerators.staticColumnGen(i).descriptorAt(idx); + } + return lts -> new Operations.WriteOp(lts, pd, cd, vds, sds, Operations.Kind.INSERT); + }; + } + + public interface ToOp + { + Operations.Operation toOp(long lts); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/SchemaGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/SchemaGenerators.java new file mode 100644 index 0000000000..c0e0c6e569 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/SchemaGenerators.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.gen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; + +public class SchemaGenerators +{ + public static Generator schemaSpecGen(String ks, String prefix, int expectedValues) + { + return schemaSpecGen(ks, prefix, expectedValues, SchemaSpec.optionsBuilder()); + } + + public static Generator schemaSpecGen(String ks, String prefix, int expectedValues, SchemaSpec.Options options) + { + return new Generator<>() + { + final Generator>> regularGen = Generators.list(1, 10, regularColumnSpecGen()); + final Generator>> staticGen = Generators.list(1, 10, staticColumnSpecGen()); + final Generator>> ckGen = Generators.list(1, 10, ckColumnSpecGen()); + final Generator>> pkGen = Generators.list(1, 10, pkColumnSpecGen()); + + int counter = 0; + public SchemaSpec generate(EntropySource rng) + { + int idx = counter++; + return new SchemaSpec(rng.next(), + expectedValues, + ks, + prefix + idx, + pkGen.generate(rng), + ckGen.generate(rng), + regularGen.generate(rng), + staticGen.generate(rng), + options); + }; + }; + } + + public static Generator> regularColumnSpecGen() + { + return columnSpecGen("regular", ColumnSpec.Kind.REGULAR, Generators.pick(ColumnSpec.TYPES)); + } + + public static Generator> staticColumnSpecGen() + { + return columnSpecGen("static", ColumnSpec.Kind.STATIC, Generators.pick(ColumnSpec.TYPES)); + } + + public static Generator> pkColumnSpecGen() + { + return columnSpecGen("pk", ColumnSpec.Kind.PARTITION_KEY, Generators.pick(ColumnSpec.TYPES)); + } + + public static Generator> ckColumnSpecGen() + { + List> forwardAndReverse = new ArrayList<>(); + forwardAndReverse.addAll(ColumnSpec.TYPES); + forwardAndReverse.addAll(ColumnSpec.ReversedType.cache.values()); + return columnSpecGen("ck", ColumnSpec.Kind.CLUSTERING, Generators.pick(forwardAndReverse)); + } + + public static Generator> columnSpecGen(String prefix, ColumnSpec.Kind kind, Generator> typeGen) + { + return new Generator>() + { + int counter = 0; + public ColumnSpec generate(EntropySource rng) + { + ColumnSpec.DataType type = typeGen.generate(rng); + Generator gen = TypeAdapters.defaults.get(type.asServerType()); + int idx = counter++; + return new ColumnSpec(prefix + idx, type, gen, kind); + } + }; + } + + public static Generator trivialSchema(String ks, String table, int population) + { + return (rng) -> { + return new SchemaSpec(rng.next(), + population, + ks, table, + Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.int64Type, Generators.int64())), + Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.int64Type, Generators.int64(), false)), + Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int64Type)), + List.of(ColumnSpec.staticColumn("s1", ColumnSpec.int64Type))); + }; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/TypeAdapters.java b/test/harry/main/org/apache/cassandra/harry/gen/TypeAdapters.java new file mode 100644 index 0000000000..808ff5e45e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/TypeAdapters.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Date; +import java.util.HashMap; +import java.util.Map; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.ByteType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.DoubleType; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.ReversedType; +import org.apache.cassandra.db.marshal.ShortType; +import org.apache.cassandra.db.marshal.TimeType; +import org.apache.cassandra.db.marshal.TimeUUIDType; +import org.apache.cassandra.db.marshal.TimestampType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.schema.ColumnMetadata; + +/** + * A class that helps to translate Cassandra's AbstractType instances to Harry Generators + */ +public class TypeAdapters +{ + public static final Map, Generator> defaults = new HashMap<>() {{ + put(ByteType.instance, Generators.int8()); + put(ShortType.instance, Generators.int16()); + put(Int32Type.instance, Generators.int32()); + put(LongType.instance, Generators.int64()); + put(BooleanType.instance, Generators.bool()); // this type has extremely small entryop + put(FloatType.instance, Generators.floats()); + put(DoubleType.instance, Generators.doubles()); + put(BytesType.instance, Generators.bytes(10, 20)); + put(AsciiType.instance, Generators.englishAlphabet(5, 10)); + put(UTF8Type.instance, Generators.utf8(10, 20)); + put(UUIDType.instance, Generators.uuidGen()); + put(TimeUUIDType.instance, Generators.timeuuid()); + put(TimestampType.instance, Generators.int64(0, Long.MAX_VALUE).map(Date::new)); + put(TimeType.instance, Generators.int64(0, Long.MAX_VALUE)); + put(IntegerType.instance, Generators.bigInt()); + put(DecimalType.instance, Generators.bigDecimal()); + put(InetAddressType.instance, Generators.inetAddr()); + + for (AbstractType type : new ArrayList<>(keySet())) + put(ReversedType.getInstance(type), get(type)); + }}; + + public static Generator forKeys(ImmutableList columns) + { + return forKeys(columns, defaults); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static Generator forKeys(ImmutableList columns, Map, Generator> typeToGen) + { + Generator[] gens = new Generator[columns.size()]; + for (int i = 0; i < gens.length; i++) + { + gens[i] = forValues(columns.get(i), typeToGen); + } + return Generators.zipArray(gens); + } + + public static Generator forValues(ColumnMetadata column) + { + return forValues(column, defaults); + } + + private static Generator forValues(ColumnMetadata column, Map, Generator> typeToGen) + { + Generator gen = (Generator) typeToGen.get(column.type); + if (gen == null) + { + throw new IllegalArgumentException(String.format("Could not find generator for column %s of type %s in %s", + column, column.type, typeToGen)); + } + return gen; + } + + public static Generator forValues(AbstractType type) + { + return forValues(type, defaults); + } + + private static Generator forValues(AbstractType type, Map, Generator> typeToGen) + { + Generator gen = (Generator) typeToGen.get(type); + if (gen == null) + { + throw new IllegalArgumentException(String.format("Could not find generator for column type %s in %s", + type, typeToGen)); + } + return gen; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/ValueGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/ValueGenerators.java new file mode 100644 index 0000000000..28a671a09c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/ValueGenerators.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Comparator; +import java.util.List; + +public class ValueGenerators +{ + protected final Bijections.Bijection pkGen; + protected final Bijections.Bijection ckGen; + + protected final List> regularColumnGens; + protected final List> staticColumnGens; + + protected final List> pkComparators; + protected final List> ckComparators; + protected final List> regularComparators; + protected final List> staticComparators; + + public ValueGenerators(Bijections.Bijection pkGen, + Bijections.Bijection ckGen, + List> regularColumnGens, + List> staticColumnGens, + + List> pkComparators, + List> ckComparators, + List> regularComparators, + List> staticComparators) + { + this.pkGen = pkGen; + this.ckGen = ckGen; + this.regularColumnGens = regularColumnGens; + this.staticColumnGens = staticColumnGens; + this.pkComparators = pkComparators; + this.ckComparators = ckComparators; + this.regularComparators = regularComparators; + this.staticComparators = staticComparators; + } + + public Bijections.Bijection pkGen() + { + return pkGen; + } + + public Bijections.Bijection ckGen() + { + return ckGen; + } + + public Bijections.Bijection regularColumnGen(int idx) + { + return regularColumnGens.get(idx); + } + + public Bijections.Bijection staticColumnGen(int idx) + { + return staticColumnGens.get(idx); + } + + public int ckColumnCount() + { + return ckComparators.size(); + } + + public int regularColumnCount() + { + return regularColumnGens.size(); + } + + public int staticColumnCount() + { + return staticColumnGens.size(); + } + + public Comparator pkComparator(int idx) + { + return pkComparators.get(idx); + } + + public Comparator ckComparator(int idx) + { + return ckComparators.get(idx); + } + + public Comparator regularComparator(int idx) + { + return regularComparators.get(idx); + } + + public Comparator staticComparator(int idx) + { + return staticComparators.get(idx); + } + + public int pkPopulation() + { + return pkGen.population(); + } + + public int ckPopulation() + { + return ckGen.population(); + } + + public int regularPopulation(int i) + { + return regularColumnGens.get(i).population(); + } + + public int staticPopulation(int i) + { + return staticColumnGens.get(i).population(); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/distribution/Distribution.java b/test/harry/main/org/apache/cassandra/harry/gen/distribution/Distribution.java deleted file mode 100644 index ac961f9ac7..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/gen/distribution/Distribution.java +++ /dev/null @@ -1,136 +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.harry.gen.distribution; - -import org.apache.cassandra.harry.core.Configuration; - -public interface Distribution -{ - Configuration.DistributionConfig toConfig(); - - interface DistributionFactory - { - Distribution make(); - } - - long skew(long i); - - class IdentityDistribution implements Distribution - { - public Configuration.DistributionConfig toConfig() - { - return new Configuration.IdentityDistributionConfig(); - } - - public long skew(long i) - { - return i; - } - } - - class ConstantDistribution implements Distribution - { - public final long constant; - - public ConstantDistribution(long constant) - { - this.constant = constant; - } - - public Configuration.DistributionConfig toConfig() - { - return new Configuration.ConstantDistributionConfig(constant); - } - - public long skew(long i) - { - return constant; - } - - public String toString() - { - return "ConstantDistribution{" + - "constant=" + constant + - '}'; - } - } - - class ScaledDistribution implements Distribution - { - private final long min; - private final long max; - - public ScaledDistribution(long min, long max) - { - this.min = min; - this.max = max; - } - - public Configuration.DistributionConfig toConfig() - { - return new Configuration.ScaledDistributionConfig(min, max); - } - - public long skew(long i) - { - return scale(i, min, max); - } - - public static long scale(long value, long min, long max) - { - if (value == 0) - return (max - min) / 2; - - double nomalized = (1.0 * Math.abs(value)) / Long.MAX_VALUE; - double diff = 0.5 * (max - min); - if (value > 0) - return (long) (min + nomalized * diff); - else - return (long) (max - nomalized * diff); - } - - public String toString() - { - return "ScaledDistribution{" + - "min=" + min + - ", max=" + max + - '}'; - } - } - - class NormalDistribution implements Distribution - { - private final org.apache.commons.math3.distribution.NormalDistribution delegate; - - public NormalDistribution() - { - delegate = new org.apache.commons.math3.distribution.NormalDistribution(); - } - - public Configuration.DistributionConfig toConfig() - { - return new Configuration.NormalDistributionConfig(); - } - - public long skew(long i) - { - return (long) delegate.cumulativeProbability((double) i); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java index 5ddc885283..068b673785 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java @@ -36,34 +36,48 @@ public class JdkRandomEntropySource implements EntropySource this.rng = rng; } + @Override public long next() { return rng.nextLong(); } + @Override public void seed(long seed) { rng.setSeed(seed); } + @Override public EntropySource derive() { return new JdkRandomEntropySource(new Random(rng.nextLong())); } + @Override public int nextInt() { return rng.nextInt(); } + /** + * Generates int between [0, max). + */ + @Override public int nextInt(int max) { return rng.nextInt(max); } + /** + * Generates a value between [min, max). + */ + @Override public int nextInt(int min, int max) { - return rng.nextInt(max) + min; + if (min == max) + return min; + return rng.nextInt(max - min) + min; } public long nextLong() @@ -71,11 +85,19 @@ public class JdkRandomEntropySource implements EntropySource return rng.nextLong(); } + @Override public float nextFloat() { return rng.nextFloat(); } + @Override + public double nextDouble() + { + return rng.nextDouble(); + } + + @Override public boolean nextBoolean() { return rng.nextBoolean(); diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.java index 070fe24f67..913e61615c 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.java @@ -108,6 +108,12 @@ public class PcgRSUFast implements EntropySource return RngUtils.asFloat(next()); } + @Override + public double nextDouble() + { + return RngUtils.asDouble(next()); + } + @Override public boolean nextBoolean() { diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/PureRng.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/PureRng.java new file mode 100644 index 0000000000..6fc8aa276b --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/PureRng.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +public interface PureRng +{ + long randomNumber(long i, long stream); + + long sequenceNumber(long r, long stream); + + default long next(long r) + { + return next(r, 0); + } + + long next(long r, long stream); + + long prev(long r, long stream); + + default long prev(long r) + { + return next(r, 0); + } + + class PCGFast implements PureRng + { + private final long seed; + + public PCGFast(long seed) + { + this.seed = seed; + } + + public long randomNumber(long i, long stream) + { + return PCGFastPure.shuffle(PCGFastPure.advanceState(seed, i, stream)); + } + + public long sequenceNumber(long r, long stream) + { + return PCGFastPure.distance(seed, PCGFastPure.unshuffle(r), stream); + } + + public long next(long r, long stream) + { + return PCGFastPure.next(r, stream); + } + + public long prev(long r, long stream) + { + return PCGFastPure.previous(r, stream); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/SeedableEntropySource.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/SeedableEntropySource.java new file mode 100644 index 0000000000..48afebe4c2 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/SeedableEntropySource.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.gen.rng; + +import java.util.function.Consumer; +import java.util.function.Function; + +import io.netty.util.concurrent.FastThreadLocal; +import org.apache.cassandra.harry.gen.EntropySource; + +public class SeedableEntropySource +{ + private static final FastThreadLocal THREAD_LOCAL = new FastThreadLocal<>(); + + public static T computeWithSeed(long seed, Function fn) + { + return fn.apply(entropySource(seed)); + } + + public static void doWithSeed(long seed, Consumer fn) + { + fn.accept(entropySource(seed)); + } + + private static EntropySource entropySource(long seed) + { + EntropySource entropySource = THREAD_LOCAL.get(); + if (entropySource == null) + { + entropySource = new JdkRandomEntropySource(0); + THREAD_LOCAL.set(entropySource); + } + entropySource.seed(seed); + return entropySource; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/test/EntropySourceTest.java b/test/harry/main/org/apache/cassandra/harry/gen/test/EntropySourceTest.java new file mode 100644 index 0000000000..d04ad5a783 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/test/EntropySourceTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.gen.test; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.rng.PCGFastPure; +import org.apache.cassandra.harry.gen.rng.PcgRSUFast; +import org.apache.cassandra.harry.gen.rng.PureRng; + +import static org.apache.cassandra.harry.checker.TestHelper.repeat; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; + +public class EntropySourceTest +{ + private static int RUNS = 100000; + + @Test + public void testShuffleUnshuffle() + { + withRandom(rng -> { + repeat(RUNS, () -> { + long l = rng.next(); + Assert.assertEquals(l, PCGFastPure.unshuffle(PCGFastPure.shuffle(l))); + }); + }); + } + + @Test + public void testImmutableRng() + { + withRandom(entropy -> { + int size = 5; + PureRng rng = new PureRng.PCGFast(entropy.next()); + repeat(RUNS, () -> { + long stream = entropy.next(); + 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() + { + withRandom(entropy -> { + int size = 5; + PureRng rng = new PureRng.PCGFast(entropy.next()); + 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() + { + withRandom(entropy -> { + repeat(RUNS, () -> { + long a = entropy.next(); + Assert.assertEquals(a, PCGFastPure.unshuffle(PCGFastPure.shuffle(a))); + }); + }); + } + + @Test + public void testIntBetween() + { + withRandom(entropy -> { + EntropySource rng = new PcgRSUFast(entropy.next(), entropy.next()); + 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); + }); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/AgainstSutChecker.java b/test/harry/main/org/apache/cassandra/harry/model/AgainstSutChecker.java deleted file mode 100644 index e4bd009c8c..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/AgainstSutChecker.java +++ /dev/null @@ -1,109 +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.harry.model; - -import java.util.List; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.sut.QueryModifyingSut; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.tracker.DataTracker; - -/** - * A simple way to verify if something might be a Harry issue: check against a different SUT. - * - * For example, if you are using `flush` in your primary SUT, avoid using it for the secondary SUT, - * and compare results. - * - * Usually used in combination with {@link QueryModifyingSut}, which writes to - * the second SUT. - * - * SchemaSpec doubleWriteSchema = schema.cloneWithName(schema.keyspace, schema.keyspace + "_debug"); - * - * sut.schemaChange(doubleWriteSchema.compile().cql()); - * - * QueryModifyingSut sut = new QueryModifyingSut(this.sut, - * schema.table, - * doubleWriteSchema.table); - * - * - * Model model = new AgainstSutChecker(tracker, history.clock(), sut, schema, doubleWriteSchema); - */ -public class AgainstSutChecker implements Model -{ - protected final OpSelectors.Clock clock; - protected final SystemUnderTest sut; - protected final SchemaSpec schema; - protected final SchemaSpec doubleWriteTable; - protected final DataTracker tracker; - - public AgainstSutChecker(DataTracker tracker, - OpSelectors.Clock clock, - SystemUnderTest sut, - SchemaSpec schema, - SchemaSpec doubleWriteTable) - { - this.clock = clock; - this.sut = sut; - this.schema = schema; - this.doubleWriteTable = doubleWriteTable; - this.tracker = tracker; - } - - public void validate(Query query) - { - tracker.beginValidation(query.pd); - - List rows1 = executeOnMainSchema(query); - List rows2 = executeOnDebugSchema(query); - - if (rows1.size() != rows2.size()) - throw new IllegalStateException(String.format("Sizes do not match %d %d\n%s\n%s\nQuery:%s\n", rows1.size(), rows2.size(), rows1, rows2, query.toSelectStatement())); - - for (int i = 0; i < rows1.size(); i++) - { - if (!rows1.get(i).equals(rows2.get(i))) - { - throw new IllegalStateException(String.format("Rows mismatch:\n" + - "%s\n" + - "%s\n", - rows1.get(i), - rows2.get(i))); - } - } - tracker.endValidation(query.pd); - } - - protected final List executeOnMainSchema(Query query) - { - CompiledStatement s1 = query.toSelectStatement(schema.allColumnsSet, true); - return SelectHelper.execute(sut, clock, s1, schema); - } - - protected List executeOnDebugSchema(Query query) - { - CompiledStatement s2 = query.toSelectStatement(doubleWriteTable.allColumnsSet, true) - .withSchema(schema.keyspace, schema.table, doubleWriteTable.keyspace, doubleWriteTable.table); - return SelectHelper.execute(sut, clock, s2, schema); - } - -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/AlwaysSamePartitionSelector.java b/test/harry/main/org/apache/cassandra/harry/model/AlwaysSamePartitionSelector.java deleted file mode 100644 index daecd029fe..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/AlwaysSamePartitionSelector.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.harry.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; - -/** - * A simple test-only descriptor selector that can used for testing things where you only need one partition - */ -public class AlwaysSamePartitionSelector extends OpSelectors.PdSelector -{ - private final long pd; - - public AlwaysSamePartitionSelector(long pd) - { - this.pd = pd; - } - - protected long pd(long lts) - { - return 0; - } - - public long nextLts(long lts) - { - return lts + 1; - } - - public long prevLts(long lts) - { - return lts - 1; - } - - public long maxLtsFor(long pd) - { - return 1000; - } - - public long minLtsAt(long position) - { - return 0; - } - - public long minLtsFor(long pd) - { - return 0; - } - - public long positionFor(long lts) - { - return 0; - } - - public long maxPosition(long maxLts) - { - return 0; - } - - @JsonTypeName("always_same") - public static class AlwaysSamePartitionSelectorConfiguration implements Configuration.PDSelectorConfiguration - { - private final long pd; - - public AlwaysSamePartitionSelectorConfiguration(@JsonProperty("pd") long pd) - { - this.pd = pd; - } - - public OpSelectors.PdSelector make(OpSelectors.PureRng rng) - { - return new AlwaysSamePartitionSelector(pd); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/Model.java b/test/harry/main/org/apache/cassandra/harry/model/Model.java index 0a85eaffeb..642488014d 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/Model.java +++ b/test/harry/main/org/apache/cassandra/harry/model/Model.java @@ -1,47 +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. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.apache.cassandra.harry.model; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.operations.Query; +import java.util.List; + +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.execution.ResultSetRow; +import org.apache.cassandra.harry.op.Visit; public interface Model { - long NO_TIMESTAMP = Long.MIN_VALUE; + void validate(Operations.SelectStatement select, List actual); - void validate(Query query); - - interface ModelFactory + class LtsOperationPair { - Model make(Run run); - } + final long lts; + final int opId; - class ValidationException extends RuntimeException - { - public ValidationException(String trackerState, String partitionState, String observedState, String format, Object... objects) + public LtsOperationPair(long lts, int opId) { - super(String.format(format, objects) + - "\nTracker state:\n" + trackerState + - "\nPartition state:\n" + partitionState + - "\nObserved state:\n" + observedState); + this.lts = lts; + this.opId = opId; } } + interface Replay extends Iterable + { + Visit replay(long lts); + Operations.Operation replay(long lts, int opId); + } -} \ No newline at end of file +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/NoOpChecker.java b/test/harry/main/org/apache/cassandra/harry/model/NoOpChecker.java deleted file mode 100644 index 7683f0ef95..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/NoOpChecker.java +++ /dev/null @@ -1,44 +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.harry.model; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.Query; - -public class NoOpChecker implements Model -{ - private final SystemUnderTest sut; - private final SystemUnderTest.ConsistencyLevel cl; - public NoOpChecker(Run run) - { - this(run.sut, SystemUnderTest.ConsistencyLevel.QUORUM); - } - - public NoOpChecker(SystemUnderTest sut, SystemUnderTest.ConsistencyLevel cl) - { - this.sut = sut; - this.cl = cl; - } - - public void validate(Query query) - { - sut.execute(query.toSelectStatement(), cl); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/OpSelectors.java b/test/harry/main/org/apache/cassandra/harry/model/OpSelectors.java deleted file mode 100644 index fe83a418a8..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/OpSelectors.java +++ /dev/null @@ -1,903 +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.harry.model; - -import java.util.EnumMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import org.apache.cassandra.harry.core.Configuration; -import com.google.common.annotations.VisibleForTesting; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Bytes; -import org.apache.cassandra.harry.gen.rng.PCGFastPure; -import org.apache.cassandra.harry.gen.rng.RngUtils; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.gen.distribution.Distribution; -import org.apache.cassandra.harry.runner.EarlyExitException; -import org.apache.cassandra.harry.util.BitSet; - -import static org.apache.cassandra.harry.gen.DataGenerators.NIL_DESCR; -import static org.apache.cassandra.harry.gen.DataGenerators.UNSET_DESCR; - -/** - * Row (deflated) data selectors. Not calling them generators, since their output is entirely - * deterministic, and for each input they are able to produce a single output. - *

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

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

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

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

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

- * For any LTS, we can calculate previous and next LTS on which it will visit the same - * partition - */ - class DefaultPdSelector extends OpSelectors.PdSelector - { - public final static long PARTITION_DESCRIPTOR_STREAM_ID = 0x706b; - - private final PureRng rng; - - private final long slideAfterRepeats; - private final long switchAfter; - private final long windowSize; - - private final long positionOffset; - private final long positionWindowSize; - - public DefaultPdSelector(PureRng rng, long windowSize, long slideAfterRepeats) - { - this(rng, windowSize, slideAfterRepeats, 0L, Long.MAX_VALUE); - } - - public DefaultPdSelector(PureRng rng, long windowSize, long slideAfterRepeats, long positionOffset, long positionWindowSize) - { - this.rng = rng; - this.slideAfterRepeats = slideAfterRepeats; - this.windowSize = windowSize; - this.switchAfter = windowSize * slideAfterRepeats; - this.positionOffset = positionOffset; - this.positionWindowSize = positionWindowSize; - } - - public Iterator allPds(long maxLts) - { - final long maxPosition = maxPosition(maxLts); - return new Iterator() - { - private long position = 0; - - public boolean hasNext() - { - return position <= maxPosition; - } - - public Long next() - { - long pd = rng.randomNumber(adjustPosition(position), PARTITION_DESCRIPTOR_STREAM_ID); - position++; - return pd; - } - }; - } - - protected long pd(long lts) - { - return rng.randomNumber(adjustPosition(positionFor(lts)), PARTITION_DESCRIPTOR_STREAM_ID); - } - - public long minLtsAt(long position) - { - if (position < windowSize) - return position; - - long windowStart = (position - (windowSize - 1)) * slideAfterRepeats * windowSize; - return windowStart + windowSize - 1; - } - - public long minLtsFor(long pd) - { - long position = positionForPd(pd); - return minLtsAt(position); - } - - public long randomVisitedPd(long maxLts, long visitLts, SchemaSpec schema) - { - long maxPosition = maxPosition(maxLts); - if (maxPosition == 0) - return pdAtPosition(0, schema); - - int idx = RngUtils.asInt(rng.randomNumber(visitLts, maxPosition), 0, (int) (maxPosition - 1)); - return pdAtPosition(idx, schema); - } - - public long maxPosition(long maxLts) - { - long timesCycled = maxLts / switchAfter; - long windowStart = timesCycled * windowSize; - - // We have cycled through _current_ window at least once - if (maxLts > (windowStart * slideAfterRepeats) + windowSize) - return windowStart + windowSize; - return windowStart + maxLts % windowSize; - } - - public long positionFor(long lts) - { - long windowStart = lts / switchAfter; - return windowStart + lts % windowSize; - } - - /** - * We only adjust position right before we go to the PCG RNG to grab a partition id, since we want - * the fact that PCG is actually offset to be hidden from the user and even the rest of this class. - */ - private long adjustPosition(long position) - { - if (position > positionWindowSize) - throw new EarlyExitException(String.format("Partition position has wrapped around, so can not be safely used. " + - "This runner has been given %d partitions, and if we wrap back to " + - "position 0, partition state is not going to be inflatable, since" + - "nextLts will not jump to the lts that is about to be visited. " + - "Increase rows per visit, batch size, or slideAfter repeats.", - positionWindowSize)); - return positionOffset + position; - } - - /** - * When computing position from pd or lts, we need to translate the offset back to the original, since - * it is not aware (and should not be) of the fact that positions are offset. - */ - private long unadjustPosition(long position) - { - return position - positionOffset; - } - - public long positionForPd(long pd) - { - return unadjustPosition(rng.sequenceNumber(pd, PARTITION_DESCRIPTOR_STREAM_ID)); - } - - public long pdAtPosition(long position, SchemaSpec schemaSpec) - { - return schemaSpec.adjustCdEntropy(rng.randomNumber(adjustPosition(position), PARTITION_DESCRIPTOR_STREAM_ID)); - } - - public long nextLts(long lts) - { - long slideCount = lts / switchAfter; - long positionInCycle = lts - slideCount * switchAfter; - long nextRepeat = positionInCycle / windowSize + 1; - - if (nextRepeat > slideAfterRepeats || - (nextRepeat == slideAfterRepeats && (positionInCycle % windowSize) == 0)) - return -1; - - // last cycle before window slides; next window will have shifted by one - if (nextRepeat == slideAfterRepeats) - positionInCycle -= 1; - - return slideCount * switchAfter + windowSize + positionInCycle; - } - - public long prevLts(long lts) - { - long slideCount = lts / switchAfter; - long positionInCycle = lts - slideCount * switchAfter; - long prevRepeat = positionInCycle / windowSize - 1; - - if (lts < windowSize || - prevRepeat < -1 || - (prevRepeat == -1 && (positionInCycle % windowSize) == (windowSize - 1))) - return -1; - - // last cycle before window slides; next window will have shifted by one - if (prevRepeat == -1) - positionInCycle += 1; - - return slideCount * switchAfter - windowSize + positionInCycle; - } - - public long maxLtsFor(long pd) - { - long position = positionForPd(pd); - return position * switchAfter + (slideAfterRepeats - 1) * windowSize; - } - - public String toString() - { - return "DefaultPdSelector{" + - "slideAfterRepeats=" + slideAfterRepeats + - ", windowSize=" + windowSize + - '}'; - } - } - - static ColumnSelectorBuilder columnSelectorBuilder() - { - return new ColumnSelectorBuilder(); - } - - class ColumnSelectorBuilder - { - private Map> m; - - public ColumnSelectorBuilder() - { - this.m = new EnumMap<>(OperationKind.class); - } - - public ColumnSelectorBuilder forAll(SchemaSpec schema) - { - return forAll(schema, BitSet.surjection(schema.allColumns.size())); - } - - // TODO: change bitsets to take into account _all_ columns not only regulars - public ColumnSelectorBuilder forAll(SchemaSpec schema, Surjections.Surjection orig) - { - for (OperationKind type : OperationKind.values()) - { - Surjections.Surjection gen = orig; - - switch (type) - { - case UPDATE_WITH_STATICS: - case DELETE_COLUMN_WITH_STATICS: - gen = (descriptor) -> { - long counter = 0; - while (counter <= 100) - { - BitSet bitSet = orig.inflate(descriptor); - if ((schema.regularColumns.isEmpty() || !bitSet.allUnset(schema.regularColumnsMask)) - && (schema.staticColumns.isEmpty() || !bitSet.allUnset(schema.staticColumnsMask))) - return bitSet; - - descriptor = RngUtils.next(descriptor); - counter++; - } - throw new RuntimeException(String.format("Could not generate a value after %d attempts.", counter)); - }; - break; - // Can not have an UPDATE statement without anything to update - case UPDATE: - gen = descriptor -> { - long counter = 0; - while (counter <= 100) - { - BitSet bitSet = orig.inflate(descriptor); - - if (!bitSet.allUnset(schema.regularColumnsMask)) - return bitSet; - - descriptor = RngUtils.next(descriptor); - counter++; - } - throw new RuntimeException(String.format("Could not generate a value after %d attempts.", counter)); - }; - break; - case DELETE_COLUMN: - gen = (descriptor) -> { - long counter = 0; - while (counter <= 100) - { - BitSet bitSet = orig.inflate(descriptor); - BitSet mask = schema.regularColumnsMask; - - if (!bitSet.allUnset(mask)) - return bitSet; - - descriptor = RngUtils.next(descriptor); - counter++; - } - throw new RuntimeException(String.format("Could not generate a value after %d attempts.", counter)); - }; - break; - } - this.m.put(type, gen); - } - return this; - } - - public ColumnSelectorBuilder forWrite(Surjections.Surjection gen) - { - m.put(OperationKind.INSERT, gen); - return this; - } - - public ColumnSelectorBuilder forWrite(BitSet pickFrom) - { - return forWrite(Surjections.pick(pickFrom)); - } - - public ColumnSelectorBuilder forDelete(Surjections.Surjection gen) - { - m.put(OperationKind.DELETE_ROW, gen); - return this; - } - - public ColumnSelectorBuilder forDelete(BitSet pickFrom) - { - return forDelete(Surjections.pick(pickFrom)); - } - - public ColumnSelectorBuilder forColumnDelete(Surjections.Surjection gen) - { - m.put(OperationKind.DELETE_COLUMN, gen); - return this; - } - - public ColumnSelectorBuilder forColumnDelete(BitSet pickFrom) - { - return forColumnDelete(Surjections.pick(pickFrom)); - } - - public ColumnSelector build() - { - return (kind, descriptor) -> m.get(kind).inflate(descriptor); - } - } - - - /** - * ColumnSelector has to return BitSet specifying _all_ columns - */ - interface ColumnSelector - { - public BitSet columnMask(OperationKind operationKind, long descriptor); - } - - class HierarchicalDescriptorSelector extends DefaultDescriptorSelector - { - private final int[] fractions; - - public HierarchicalDescriptorSelector(PureRng rng, - // how many parts (at most) each subsequent "level" should contain - int[] fractions, - ColumnSelector columnSelector, - OperationSelector operationSelector, - Distribution operationsPerLtsDistribution, - int maxPartitionSize) - { - super(rng, - columnSelector, - operationSelector, - operationsPerLtsDistribution, - maxPartitionSize); - this.fractions = fractions; - } - - @Override - public long cd(long pd, long lts, long opId, SchemaSpec schema) - { - if (schema.clusteringKeys.size() <= 1) - return schema.adjustCdEntropy(super.cd(pd, lts, opId)); - - int partitionSize = maxPartitionSize(); - int clusteringOffset = clusteringOffset(lts); - long res; - if (clusteringOffset == 0) - { - res = rng.prev(opId, pd); - } - else - { - int positionInPartition = (int) ((clusteringOffset + opId) % partitionSize); - res = cd(positionInPartition, fractions, schema, rng, pd); - } - return schema.adjustCdEntropy(res); - } - - @VisibleForTesting - public static long cd(int positionInPartition, int[] fractions, SchemaSpec schema, PureRng rng, long pd) - { - long[] slices = new long[schema.clusteringKeys.size()]; - for (int i = 0; i < slices.length; i++) - { - int idx = i < fractions.length ? (positionInPartition % (fractions[i] - 1)) : positionInPartition; - slices[i] = rng.prev(idx, rng.next(pd, i + 1)); - } - - return schema.ckGenerator.stitch(slices); - } - - protected long cd(long pd, long lts, long opId) - { - throw new RuntimeException("Shouldn't be called"); - } - } - - class DefaultDescriptorSelector extends DescriptorSelector - { - protected final static long OPERATIONS_PER_LTS_STREAM = 0x5e03812e293L; - protected final static long BITSET_IDX_STREAM = 0x92eb607bef1L; - - public static OperationSelector DEFAULT_OP_SELECTOR = OperationSelector.weighted(Surjections.weights(45, 45, 3, 2, 2, 1, 1, 1), - OperationKind.INSERT, - OperationKind.INSERT_WITH_STATICS, - OperationKind.DELETE_ROW, - OperationKind.DELETE_COLUMN, - OperationKind.DELETE_COLUMN_WITH_STATICS, - OperationKind.DELETE_PARTITION, - OperationKind.DELETE_RANGE, - OperationKind.DELETE_SLICE); - - protected final PureRng rng; - protected final OperationSelector operationSelector; - protected final ColumnSelector columnSelector; - protected final Distribution operationsPerLtsDistribution; - protected final int maxPartitionSize; - - public DefaultDescriptorSelector(PureRng rng, - ColumnSelector columnMaskSelector, - OperationSelector operationSelector, - Distribution operationsPerLtsDistribution, - int maxPartitionSize) - { - this.rng = rng; - - this.operationSelector = operationSelector; - this.columnSelector = columnMaskSelector; - this.operationsPerLtsDistribution = operationsPerLtsDistribution; - this.maxPartitionSize = maxPartitionSize; - } - - public int operationsPerLts(long lts) - { - return (int) operationsPerLtsDistribution.skew(rng.randomNumber(lts, OPERATIONS_PER_LTS_STREAM)); - } - - public int maxPartitionSize() - { - return maxPartitionSize; - } - - protected int clusteringOffset(long lts) - { - return RngUtils.asInt(lts, 0, maxPartitionSize() - 1); - } - - // TODO: this won't work for entropy-adjusted CDs, at least the way they're implemented now - public boolean isCdVisitedBy(long pd, long lts, long cd) - { - return opId(pd, lts, cd) < operationsPerLts(lts); - } - - public long randomCd(long pd, long entropy) - { - long positionInPartition = Math.abs(rng.prev(entropy)) % maxPartitionSize(); - return rng.prev(positionInPartition, pd); - } - - protected long cd(long pd, long lts, long opId) - { - int partitionSize = maxPartitionSize(); - int clusteringOffset = clusteringOffset(lts); - if (clusteringOffset == 0) - return rng.prev(opId, pd); - - int positionInPartition = (int) ((clusteringOffset + opId) % partitionSize); - return rng.prev(positionInPartition, pd); - } - - public long opId(long pd, long lts, long cd) - { - int partitionSize = maxPartitionSize(); - int clusteringOffset = clusteringOffset(lts); - int positionInPartition = (int) rng.next(cd, pd); - - if (clusteringOffset == 0) - return positionInPartition; - - if (positionInPartition == 0) - return partitionSize - clusteringOffset; - if (positionInPartition == clusteringOffset) - return 0; - else if (positionInPartition < clusteringOffset) - return partitionSize - clusteringOffset + positionInPartition; - else - return positionInPartition - clusteringOffset; - } - - public OperationKind operationType(long pd, long lts, long opId) - { - return operationType(pd, lts, opId, partitionLevelOperationsMask(pd, lts)); - } - - public BitSet partitionLevelOperationsMask(long pd, long lts) - { - int totalOps = operationsPerLts(lts); - if (totalOps > 64) - { - throw new IllegalArgumentException("RngUtils#randomBits currently supports only up to 64 bits of entropy, so we can not " + - "split partition and row level operations for more than 64 operations at the moment."); - } - - long seed = rng.randomNumber(pd, lts); - - int partitionLevelOps = (int) Math.ceil(operationSelector.partitionLevelThreshold * totalOps); - long partitionLevelOpsMask = RngUtils.randomBits(partitionLevelOps, totalOps, seed); - - return BitSet.create(partitionLevelOpsMask, totalOps); - } - - private OperationKind operationType(long pd, long lts, long opId, BitSet partitionLevelOperationsMask) - { - try - { - long descriptor = rng.randomNumber(pd ^ lts ^ opId, BITSET_IDX_STREAM); - return operationSelector.inflate(descriptor, partitionLevelOperationsMask.isSet((int) opId)); - } - catch (Throwable t) - { - throw new RuntimeException(String.format("Can not generate a random number with the following inputs: " + - "pd=%d lts=%d opId=%d partitionLevelOperationsMask=%s", - pd, lts, opId, partitionLevelOperationsMask)); - } - } - - public BitSet columnMask(long pd, long lts, long opId, OperationKind opType) - { - long descriptor = rng.randomNumber(pd ^ lts ^ opId, BITSET_IDX_STREAM); - return columnSelector.columnMask(opType, descriptor); - } - - public long vd(long pd, long cd, long lts, long opId, int col) - { - return rng.randomNumber(opId + 1, pd ^ cd ^ lts ^ col); - } - } - - enum OperationKind - { - UPDATE(false), - INSERT(false), - UPDATE_WITH_STATICS(true), - INSERT_WITH_STATICS(true), - DELETE_PARTITION(true), - DELETE_ROW(false), - DELETE_COLUMN(false), - DELETE_COLUMN_WITH_STATICS(true), - DELETE_RANGE(false), - DELETE_SLICE(false); - - public final boolean partititonLevel; - - OperationKind(boolean partitionLevel) - { - this.partititonLevel = partitionLevel; - } - - /** - * All operations apart from partiton delition, including partition hist are visible since if there is a - * partition liveness marker, partition's static column is going to survive. We use this method to match - * computed LTS with tracked ones. - */ - public boolean hasVisibleVisit() - { - return this != OpSelectors.OperationKind.DELETE_PARTITION; - } - } - - public static class OperationSelector - { - public final Surjections.Surjection partitionLevelOperationSelector; - public final Surjections.Surjection rowLevelOperationSelector; - // TODO: start using partitionLevelThreshold - public final double partitionLevelThreshold; - - public OperationSelector(Surjections.Surjection partitionLevelOperationSelector, - Surjections.Surjection rowLevelOperationSelector, - double partitionLevelThreshold) - { - this.partitionLevelOperationSelector = partitionLevelOperationSelector; - this.rowLevelOperationSelector = rowLevelOperationSelector; - this.partitionLevelThreshold = partitionLevelThreshold; - } - - public OperationKind inflate(long descriptor, boolean partitionLevel) - { - OperationKind operationKind = partitionLevel ? partitionLevelOperationSelector.inflate(descriptor) : rowLevelOperationSelector.inflate(descriptor); - assert operationKind.partititonLevel == partitionLevel : "Generated operation with an incorrect partition level. Check your generators."; - return operationKind; - } - - public static OperationSelector weighted(Map weightsMap) - { - int[] weights = new int[weightsMap.size()]; - OperationKind[] operationKinds = new OperationKind[weightsMap.size()]; - int i = 0; - for (Map.Entry entry : weightsMap.entrySet()) - { - weights[i] = entry.getValue(); - operationKinds[i] = entry.getKey(); - i++; - } - return weighted(Surjections.weights(weights), operationKinds); - } - - public static OperationSelector weighted(long[] weights, OperationKind... operationKinds) - { - assert weights.length == operationKinds.length; - - Map partitionLevel = new EnumMap(OperationKind.class); - Map rowLevel = new EnumMap(OperationKind.class); - - int partitionLevelSum = 0; - int rowLevelSum = 0; - for (int i = 0; i < weights.length; i++) - { - int v = (int) (weights[i] >> 32); - if (operationKinds[i].partititonLevel) - { - partitionLevel.put(operationKinds[i], v); - partitionLevelSum += v; - } - else - { - rowLevel.put(operationKinds[i], v); - rowLevelSum += v; - } - } - int sum = (partitionLevelSum + rowLevelSum); - - return new OperationSelector(Surjections.weighted(normalize(partitionLevel)), - Surjections.weighted(normalize(rowLevel)), - (partitionLevelSum * 1.0) / sum); - } - - public static Map normalize(Map weights) - { - Map normalized = new EnumMap(OperationKind.class); - int sum = 0; - for (Integer value : weights.values()) - sum += value; - - for (OperationKind kind : weights.keySet()) - { - double dbl = (sum * ((double) weights.get(kind)) / sum); - normalized.put(kind, (int) Math.round(dbl)); - } - - return normalized; - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/PartitionState.java b/test/harry/main/org/apache/cassandra/harry/model/PartitionState.java new file mode 100644 index 0000000000..c47f733db6 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/PartitionState.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.model; + +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.gen.Bijections; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.function.IntFunction; + +public class PartitionState implements Iterable +{ + public static long STATIC_CLUSTERING = MagicConstants.NIL_DESCR; + + private static final Logger logger = LoggerFactory.getLogger(PartitionState.class); + + public final long pd; + + final List visitedLts = new ArrayList<>(); + final List skippedLts = new ArrayList<>(); + + RowState staticRow; + NavigableMap rows; + + final ValueGenerators valueGenerators; + + public PartitionState(long pd, ValueGenerators valueGenerators) + { + this.pd = pd; + this.rows = new TreeMap<>(valueGenerators.ckGen().descriptorsComparator()); + this.valueGenerators = valueGenerators; + this.staticRow = new RowState(this, + STATIC_CLUSTERING, + arr(valueGenerators.staticColumnCount(), MagicConstants.NIL_DESCR), + arr(valueGenerators.staticColumnCount(), MagicConstants.NO_TIMESTAMP)); + } + + /** + * Returns a navigable map of rows + */ + public NavigableMap rows() + { + return rows; + } + + public void writeStatic(long[] sds, long lts) + { + staticRow = updateRowState(staticRow, valueGenerators::staticColumnGen, STATIC_CLUSTERING, sds, lts, false); + } + + public void writeRegular(long cd, long[] vds, long lts, boolean writePrimaryKeyLiveness) + { + rows.compute(cd, (cd_, current) -> updateRowState(current, valueGenerators::regularColumnGen, cd, vds, lts, writePrimaryKeyLiveness)); + } + + public void delete(Operations.DeleteRange delete, long lts) + { + // TODO: inefficient; need to search for lower/higher bounds + rows.entrySet().removeIf(e -> Relations.matchRange(valueGenerators.ckGen(), + valueGenerators::ckComparator, + valueGenerators.ckColumnCount(), + delete.lowerBound(), + delete.upperBound(), + delete.lowerBoundRelation(), + delete.upperBoundRelation(), + e.getValue().cd)); + } + + public void reverse() + { + rows = rows.descendingMap(); + } + + public void filter(Operations.SelectStatement select) + { + switch (select.kind) + { + case SELECT_PARTITION: + // selecting everything + break; + case SELECT_ROW: + filterInternal((Operations.SelectRow) select); + break; + case SELECT_RANGE: + filterInternal((Operations.SelectRange) select); + break; + case SELECT_CUSTOM: + filterInternal((Operations.SelectCustom) select); + break; + default: + throw new IllegalStateException("Filtering not implemented for " + select); + } + } + + private void filterInternal(Operations.SelectRow select) + { + // TODO: inefficient; need to search for lower/higher bounds + rows.entrySet().removeIf(e -> e.getValue().cd != select.cd()); + } + + private void filterInternal(Operations.SelectRange select) + { + // TODO: inefficient; need to search for lower/higher bounds + rows.entrySet().removeIf(e -> !Relations.matchRange(valueGenerators.ckGen(), + valueGenerators::ckComparator, + valueGenerators.ckColumnCount(), + select.lowerBound(), + select.upperBound(), + select.lowerBoundRelation(), + select.upperBoundRelation(), + e.getValue().cd)); + } + + private void filterInternal(Operations.SelectCustom select) + { + // TODO: inefficient; need to search for lower/higher bounds + rows.entrySet().removeIf(e -> { + Map cache = new HashMap<>(); + for (Relations.Relation relation : select.ckRelations()) + { + Object[] query = cache.computeIfAbsent(relation.descriptor, valueGenerators.ckGen()::inflate); + Object[] match = cache.computeIfAbsent(e.getValue().cd, valueGenerators.ckGen()::inflate); + if (!relation.kind.match(valueGenerators.ckComparator(relation.column), match[relation.column], query[relation.column])) + return true; // true means "no match", so remove from resultset + } + + for (Relations.Relation relation : select.regularRelations()) + { + Object query = valueGenerators.regularColumnGen(relation.column).inflate(relation.descriptor); + long descriptor = e.getValue().vds[relation.column]; + if (MagicConstants.MAGIC_DESCRIPTOR_VALS.contains(descriptor)) // TODO: do we allow UNSET queries? + return true; + Object match = valueGenerators.regularColumnGen(relation.column).inflate(e.getValue().vds[relation.column]); + if (!relation.kind.match(valueGenerators.regularComparator(relation.column), match, query)) + return true; + } + + for (Relations.Relation relation : select.staticRelations()) + { + Object query = valueGenerators.staticColumnGen(relation.column).inflate(relation.descriptor); + long descriptor = e.getValue().partitionState.staticRow.vds[relation.column]; + if (MagicConstants.MAGIC_DESCRIPTOR_VALS.contains(descriptor)) // TODO: do we allow UNSET queries? + return true; + Object match = valueGenerators.staticColumnGen(relation.column).inflate(e.getValue().partitionState.staticRow.vds[relation.column]); + if (!relation.kind.match(valueGenerators.staticComparator(relation.column), match, query)) + return true; + } + + return false; + }); + } + + public void delete(long cd, long lts) + { + RowState state = rows.remove(cd); + if (state != null) + { + for (long v : state.lts) + assert lts >= v : String.format("Attempted to remove a row with a tombstone that has older timestamp (%d): %s", lts, state); + } + } + + public boolean isEmpty() + { + return rows.isEmpty(); + } + + /** + * Method used to update row state of both static and regular rows. + */ + private RowState updateRowState(RowState currentState, IntFunction> columns, long cd, long[] vds, long lts, boolean writePrimaryKeyLiveness) + { + if (currentState == null) + { + long[] ltss = new long[vds.length]; + long[] vdsCopy = new long[vds.length]; + for (int i = 0; i < vds.length; i++) + { + if (vds[i] != MagicConstants.UNSET_DESCR) + { + ltss[i] = lts; + vdsCopy[i] = vds[i]; + } + else + { + ltss[i] = MagicConstants.NO_TIMESTAMP; + vdsCopy[i] = MagicConstants.NIL_DESCR; + } + } + + currentState = new RowState(this, cd, vdsCopy, ltss); + } + else + { + assert currentState.vds.length == vds.length : String.format("Vds: %d, sds: %d", currentState.vds.length, vds.length); + for (int i = 0; i < vds.length; i++) + { + if (vds[i] == MagicConstants.UNSET_DESCR) + continue; + + assert lts >= currentState.lts[i] : String.format("Out-of-order LTS: %d. Max seen: %s", lts, currentState.lts[i]); // sanity check; we're iterating in lts order + + if (currentState.lts[i] == lts) + { + // Timestamp collision case + Bijections.Bijection column = columns.apply(i); + if (column.compare(vds[i], currentState.vds[i]) > 0) + currentState.vds[i] = vds[i]; + } + else + { + currentState.vds[i] = vds[i]; + assert lts > currentState.lts[i]; + currentState.lts[i] = lts; + } + } + } + + if (writePrimaryKeyLiveness) + currentState.hasPrimaryKeyLivenessInfo = true; + + return currentState; + } + + public void deleteRegularColumns(long lts, long cd, BitSet columns) + { + deleteColumns(lts, rows.get(cd), columns); + } + + public void deleteStaticColumns(long lts, BitSet columns) + { + deleteColumns(lts, staticRow, columns); + } + + public void deleteColumns(long lts, RowState state, BitSet columns) + { + if (state == null) + return; + + //TODO: optimise by iterating over the columns that were removed by this deletion + //TODO: optimise final decision to fully remove the column by counting a number of set/unset columns + boolean allNil = true; + for (int i = 0; i < state.vds.length; i++) + { + if (columns.isSet(i)) + { + state.vds[i] = MagicConstants.NIL_DESCR; + state.lts[i] = MagicConstants.NO_TIMESTAMP; + } + else if (state.vds[i] != MagicConstants.NIL_DESCR) + { + allNil = false; + } + } + + if (state.cd != STATIC_CLUSTERING && allNil & !state.hasPrimaryKeyLivenessInfo) + delete(state.cd, lts); + } + + public void deletePartition(long lts) + { + rows.clear(); + Arrays.fill(staticRow.vds, MagicConstants.NIL_DESCR); + Arrays.fill(staticRow.lts, MagicConstants.NO_TIMESTAMP); + } + + public Iterator iterator() + { + return iterator(false); + } + + public Iterator iterator(boolean reverse) + { + if (reverse) + return rows.descendingMap().values().iterator(); + + return rows.values().iterator(); + } + + public Collection rows(boolean reverse) + { + if (reverse) + return rows.descendingMap().values(); + + return rows.values(); + } + + public RowState staticRow() + { + return staticRow; + } + + public String toString() + { + // TODO: display inices not LTS when doing tostring + StringBuilder sb = new StringBuilder(); + + sb.append("Visited LTS: " + visitedLts).append("\n"); + sb.append("Skipped LTS: " + skippedLts).append("\n"); + + if (staticRow != null) + { + sb.append("Static row:\n" + staticRow.toString(valueGenerators)).append("\n"); + sb.append("\n"); + } + + for (RowState row : rows.values()) + sb.append(row.toString(valueGenerators)).append("\n"); + + return sb.toString(); + } + + public static class RowState + { + public boolean hasPrimaryKeyLivenessInfo = false; + + public final PartitionState partitionState; + public final long cd; + public final long[] vds; + public final long[] lts; + + public RowState(PartitionState partitionState, + long cd, + long[] vds, + long[] lts) + { + this.partitionState = partitionState; + this.cd = cd; + this.vds = vds; + this.lts = lts; + } + + public RowState clone() + { + RowState rowState = new RowState(partitionState, cd, Arrays.copyOf(vds, vds.length), Arrays.copyOf(lts, lts.length)); + rowState.hasPrimaryKeyLivenessInfo = hasPrimaryKeyLivenessInfo; + return rowState; + } + + private static String toString(long[] descriptors, IntFunction> gens) + { + String[] idxs = new String[descriptors.length]; + for (int i = 0; i < descriptors.length; i++) + idxs[i] = descrToIdxForToString(gens.apply(i), descriptors[i]); + return String.join(",", idxs); + } + + public static String descrToIdxForToString(Bijections.Bijection gen, long descr) + { + return gen.toString(descr); + } + + public String toString(ValueGenerators valueGenerators) + { + if (cd == STATIC_CLUSTERING) + { + return " rowStateRow(" + + valueGenerators.pkGen().toString(partitionState.pd) + + ", STATIC" + + ", statics(" + toString(partitionState.staticRow.vds, valueGenerators::staticColumnGen) + ")" + + ", lts(" + StringUtils.toString(partitionState.staticRow.lts) + ")"; + } + else + { + return " rowStateRow(" + + valueGenerators.pkGen().toString(partitionState.pd) + + ", " + descrToIdxForToString(valueGenerators.ckGen(), cd) + + ", vds(" + toString(vds, valueGenerators::regularColumnGen) + ")" + + ", lts(" + StringUtils.toString(lts) + ")"; + } + } + } + + public static long[] arr(int length, long fill) + { + long[] arr = new long[length]; + Arrays.fill(arr, fill); + return arr; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/PartitionStateBuilder.java b/test/harry/main/org/apache/cassandra/harry/model/PartitionStateBuilder.java new file mode 100644 index 0000000000..d67a9ed215 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/PartitionStateBuilder.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.execution.VisitExecutor; + +import static org.apache.cassandra.harry.op.Operations.*; +import static org.apache.cassandra.harry.op.Operations.Kind.INSERT; + +class PartitionStateBuilder extends VisitExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(PartitionStateBuilder.class); + + private final PartitionState partitionState; + private boolean hadPartitionDeletion; + private boolean hadTrackingRowWrite; + private final List rangeDeletes; + private final List writes; + private final List columnDeletes; + private final ValueGenerators valueGenerators; + + PartitionStateBuilder(ValueGenerators valueGenerators, + PartitionState partitionState) + { + this.valueGenerators = valueGenerators; + this.partitionState = partitionState; + hadPartitionDeletion = false; + hadTrackingRowWrite = false; + rangeDeletes = new ArrayList<>(); + writes = new ArrayList<>(); + columnDeletes = new ArrayList<>(); + } + + public PartitionState build() + { + return partitionState; + } + + protected void operation(Operation operation) + { + if (hadPartitionDeletion) + return; + + long lts = operation.lts(); + + switch (operation.kind()) + { + case DELETE_RANGE: + rangeDeletes.add(operation); + break; + case DELETE_ROW: + long cd = ((DeleteRow) operation).cd(); + partitionState.delete(cd, lts); + break; + case DELETE_PARTITION: + partitionState.deletePartition(lts); + rangeDeletes.clear(); + writes.clear(); + columnDeletes.clear(); + hadPartitionDeletion = true; + break; + case INSERT: + case UPDATE: + writes.add(operation); + break; + case DELETE_COLUMNS: + columnDeletes.add(operation); + break; + default: + throw new IllegalStateException("Unknown operation " + operation.kind()); + } + } + + @Override + protected void beginLts(long lts) + { + rangeDeletes.clear(); + writes.clear(); + columnDeletes.clear(); + hadPartitionDeletion = false; + } + + @Override + protected void endLts(long lts) + { + if (hadPartitionDeletion) + return; + + for (Operation op : writes) + { + WriteOp writeOp = (WriteOp) op; + long cd = writeOp.cd(); + + if (hadTrackingRowWrite) + { + long[] statics = new long[valueGenerators.staticColumnCount()]; + Arrays.fill(statics, MagicConstants.UNSET_DESCR); + partitionState.writeStatic(statics, lts); + } + + switch (op.kind()) + { + case INSERT: + case UPDATE: + WriteOp writeStaticOp = (WriteOp) op; + // We could apply static columns during the first iteration, but it's more convenient + // to reconcile static-level deletions. + partitionState.writeStatic(writeStaticOp.sds(), lts); + partitionState.writeRegular(cd, writeOp.vds(), lts, op.kind() == INSERT); + break; + default: + throw new IllegalStateException(op.kind().toString()); + } + } + + for (Operation op : columnDeletes) + { + DeleteColumnsOp deleteColumnsOp = (DeleteColumnsOp) op; + partitionState.deleteStaticColumns(lts, deleteColumnsOp.staticColumns()); + partitionState.deleteRegularColumns(lts, deleteColumnsOp.cd(), deleteColumnsOp.staticColumns()); + } + + for (Operation rangeDelete : rangeDeletes) + { + partitionState.delete((DeleteRange) rangeDelete, lts); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/QueryingNoOpValidator.java b/test/harry/main/org/apache/cassandra/harry/model/QueryingNoOpValidator.java deleted file mode 100644 index 723b8e7e44..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/QueryingNoOpValidator.java +++ /dev/null @@ -1,65 +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.harry.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.Query; - -/** - * A model that can be used to "simply" run random queries. Does not perform validation - * of the results that it sees. Useful for increasing concurrency and triggering - * exceptions rather than data loss issues. - */ -public class QueryingNoOpValidator implements Model -{ - private final Run run; - - public QueryingNoOpValidator(Run run) - { - this.run = run; - } - - @Override - public void validate(Query query) - { - CompiledStatement compiled = query.toSelectStatement(); - run.sut.execute(compiled.cql(), - SystemUnderTest.ConsistencyLevel.QUORUM, - compiled.bindings()); - } - - @JsonTypeName("no_op") - public static class QueryingNoOpCheckerConfig implements Configuration.ModelConfiguration - { - @JsonCreator - public QueryingNoOpCheckerConfig() - { - } - - public Model make(Run run) - { - return new QueryingNoOpValidator(run); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java b/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java index 64af6520d5..a77904202c 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java +++ b/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java @@ -1,232 +1,215 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.apache.cassandra.harry.model; -import java.util.*; -import java.util.function.Supplier; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NavigableMap; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.model.reconciler.PartitionState; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.model.reconciler.Reconciler; -import org.apache.cassandra.harry.tracker.DataTracker; +import accord.utils.Invariants; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.execution.ResultSetRow; +import org.apache.cassandra.harry.gen.ValueGenerators; -import static org.apache.cassandra.harry.gen.DataGenerators.NIL_DESCR; -import static org.apache.cassandra.harry.gen.DataGenerators.UNSET_DESCR; +import static org.apache.cassandra.harry.MagicConstants.LTS_UNKNOWN; +import static org.apache.cassandra.harry.MagicConstants.NIL_DESCR; +import static org.apache.cassandra.harry.MagicConstants.NO_TIMESTAMP; +import static org.apache.cassandra.harry.MagicConstants.UNKNOWN_DESCR; +import static org.apache.cassandra.harry.MagicConstants.UNSET_DESCR; +/** + * Unfortunately model is not reducible to a simple interface of apply/validate, at least not if + * we intend to support concurrent validation and in-flight/timed out queries. Validation needs to + * know which queries might have been applied. + * + * + */ public class QuiescentChecker implements Model { - protected final OpSelectors.Clock clock; + private final DataTracker tracker; + private final Replay replay; + private final ValueGenerators valueGenerators; - protected final DataTracker tracker; - protected final SystemUnderTest sut; - protected final Reconciler reconciler; - protected final SchemaSpec schema; - - public QuiescentChecker(Run run) + public QuiescentChecker(ValueGenerators valueGenerators, DataTracker tracker, Replay replay) { - this(run, new Reconciler(run)); - } - - public QuiescentChecker(Run run, Reconciler reconciler) - { - this(run.clock, run.sut, run.tracker, run.schemaSpec, reconciler); - } - - public QuiescentChecker(OpSelectors.Clock clock, - SystemUnderTest sut, - DataTracker tracker, - SchemaSpec schema, - Reconciler reconciler) - { - this.clock = clock; - this.sut = sut; - this.reconciler = reconciler; + this.valueGenerators = valueGenerators; this.tracker = tracker; - this.schema = schema; + this.replay = replay; } - public void validate(Query query) + @Override + public void validate(Operations.SelectStatement select, List actualRows) { - tracker.beginValidation(query.pd); - validate(() -> SelectHelper.execute(sut, clock, query), query); - tracker.endValidation(query.pd); - } + PartitionState partitionState = new PartitionState(select.pd, valueGenerators); + PartitionStateBuilder stateBuilder = new PartitionStateBuilder(valueGenerators, partitionState); - protected void validate(Supplier> rowsSupplier, Query query) - { - List actualRows = rowsSupplier.get(); - PartitionState partitionState = reconciler.inflatePartitionState(query.pd, tracker, query); - validate(schema, tracker, partitionState, actualRows, query); - } - - public static void validate(SchemaSpec schema, DataTracker tracker, PartitionState partitionState, List actualRows, Query query) - { - Set> columns = new HashSet<>(); - columns.addAll(schema.allColumns); - validate(schema, tracker, columns, partitionState, actualRows, query); - } - - public static Reconciler.RowState adjustForSelection(Reconciler.RowState row, SchemaSpec schema, Set> selection, boolean isStatic) - { - if (selection.size() == schema.allColumns.size()) - return row; - - List> columns = isStatic ? schema.staticColumns : schema.regularColumns; - Reconciler.RowState newRowState = row.clone(); - assert newRowState.vds.length == columns.size(); - for (int i = 0; i < columns.size(); i++) + long prevLts = -1; + for (LtsOperationPair potentialVisit : tracker.potentialVisits(select.pd())) { - if (!selection.contains(columns.get(i))) + if (tracker.isFinished(potentialVisit.lts)) { - newRowState.vds[i] = UNSET_DESCR; - newRowState.lts[i] = NO_TIMESTAMP; + if (potentialVisit.lts != prevLts) + { + if (prevLts != -1) + stateBuilder.endLts(prevLts); + stateBuilder.beginLts(potentialVisit.lts); + prevLts = potentialVisit.lts; + } + Operations.Operation op = replay.replay(potentialVisit.lts, potentialVisit.opId); + stateBuilder.operation(op); } } - return newRowState; + + // Close last open LTS + if (prevLts != -1) + stateBuilder.endLts(prevLts); + + partitionState.filter(select); + if (select.orderBy() == Operations.ClusteringOrderBy.DESC) + { + partitionState.reverse(); + } + + validate(valueGenerators, partitionState, actualRows); } - public static void validate(SchemaSpec schema, DataTracker tracker, Set> selection, PartitionState partitionState, List actualRows, Query query) + // TODO: reverse + public static void validate(ValueGenerators valueGenerators, PartitionState partitionState, List actualRows) { - boolean isWildcardQuery = selection == null; - String trackerBefore = tracker.toString(); - if (isWildcardQuery) - selection = new HashSet<>(schema.allColumns); - Iterator actual = actualRows.iterator(); - Collection expectedRows = partitionState.rows(query.reverse); + NavigableMap expectedRows = partitionState.rows(); - Iterator expected = expectedRows.iterator(); - - String trackerState = String.format("Tracker before: %s, Tracker after: %s", trackerBefore, tracker); + Iterator expected = expectedRows.values().iterator(); // It is possible that we only get a single row in response, and it is equal to static row if (partitionState.isEmpty() && partitionState.staticRow() != null && actual.hasNext()) { ResultSetRow actualRowState = actual.next(); + // TODO: it is possible to start distinguising between unknown and null values if (actualRowState.cd != UNSET_DESCR && actualRowState.cd != partitionState.staticRow().cd) { - throw new ValidationException(trackerState, - partitionState.toString(schema), - toString(actualRows), + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Found a row while model predicts statics only:" + "\nExpected: %s" + - "\nActual: %s" + - "\nQuery: %s", + "\nActual: %s", partitionState.staticRow(), - actualRowState, - query.toSelectStatement()); + actualRowState); } for (int i = 0; i < actualRowState.vds.length; i++) { - if (actualRowState.vds[i] != NIL_DESCR || actualRowState.lts[i] != NO_TIMESTAMP) - throw new ValidationException(trackerState, - partitionState.toString(schema), - toString(actualRows), + // If clustering is unset, all values should be equal to NIL: we have received a row with statics only. + if (actualRowState.vds[i] != NIL_DESCR || (actualRowState.lts != LTS_UNKNOWN && actualRowState.lts[i] != NO_TIMESTAMP)) + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Found a row while model predicts statics only:" + - "\nActual: %s" + - "\nQuery: %s", - actualRowState, query.toSelectStatement()); + "\nActual: %s", + actualRowState); } - assertStaticRow(partitionState, actualRows, - adjustForSelection(partitionState.staticRow(), schema, selection, true), - actualRowState, query, trackerState, schema); + assertStaticRow(partitionState, + actualRows, + partitionState.staticRow(), + actualRowState, + valueGenerators); } while (actual.hasNext() && expected.hasNext()) { ResultSetRow actualRowState = actual.next(); - Reconciler.RowState originalExpectedRowState = expected.next(); - Reconciler.RowState expectedRowState = adjustForSelection(originalExpectedRowState, schema, selection, false); - - if (schema.trackLts) - partitionState.compareVisitedLts(actualRowState.visited_lts); + PartitionState.RowState expectedRowState = expected.next(); // TODO: this is not necessarily true. It can also be that ordering is incorrect. - if (actualRowState.cd != UNSET_DESCR && actualRowState.cd != expectedRowState.cd) + if (actualRowState.cd != UNKNOWN_DESCR && actualRowState.cd != expectedRowState.cd) { - throw new ValidationException(trackerState, - partitionState.toString(schema), - toString(actualRows), + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Found a row in the model that is not present in the resultset:" + "\nExpected: %s" + - "\nActual: %s" + - "\nQuery: %s", - expectedRowState.toString(schema), - actualRowState, query.toSelectStatement()); + "\nActual: %s", + expectedRowState.toString(valueGenerators), + actualRowState); } - if (!Arrays.equals(expectedRowState.vds, actualRowState.vds)) - throw new ValidationException(trackerState, - partitionState.toString(schema), - toString(actualRows), + if (!vdsEqual(expectedRowState.vds, actualRowState.vds)) + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Returned row state doesn't match the one predicted by the model:" + - "\nExpected: %s (%s)" + - "\nActual: %s (%s)." + - "\nQuery: %s", - descriptorsToString(expectedRowState.vds), expectedRowState.toString(schema), - descriptorsToString(actualRowState.vds), actualRowState, - query.toSelectStatement()); + "\nExpected: %s" + + "\nActual: %s.", + expectedRowState.toString(valueGenerators), + actualRowState.toString(valueGenerators)); if (!ltsEqual(expectedRowState.lts, actualRowState.lts)) - throw new ValidationException(trackerState, - partitionState.toString(schema), - toString(actualRows), + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Timestamps in the row state don't match ones predicted by the model:" + - "\nExpected: %s (%s)" + - "\nActual: %s (%s)." + - "\nQuery: %s", - Arrays.toString(expectedRowState.lts), expectedRowState.toString(schema), - Arrays.toString(actualRowState.lts), actualRowState, - query.toSelectStatement()); + "\nExpected: %s" + + "\nActual: %s.", + expectedRowState.toString(valueGenerators), + actualRowState.toString(valueGenerators)); if (partitionState.staticRow() != null || actualRowState.hasStaticColumns()) { - Reconciler.RowState expectedStaticRowState = adjustForSelection(partitionState.staticRow(), schema, selection, true); - assertStaticRow(partitionState, actualRows, expectedStaticRowState, actualRowState, query, trackerState, schema); + PartitionState.RowState expectedStaticRowState = partitionState.staticRow(); + assertStaticRow(partitionState, actualRows, expectedStaticRowState, actualRowState, valueGenerators); } } if (actual.hasNext() || expected.hasNext()) { - throw new ValidationException(trackerState, - partitionState.toString(schema), - toString(actualRows), + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Expected results to have the same number of results, but %s result iterator has more results." + "\nExpected: %s" + - "\nActual: %s" + - "\nQuery: %s", + "\nActual: %s", actual.hasNext() ? "actual" : "expected", expectedRows, - actualRows, - query.toSelectStatement()); + actualRows); } } + public static boolean vdsEqual(long[] expected, long[] actual) + { + Invariants.checkState(expected.length == actual.length); + for (int i = 0; i < actual.length; i++) + { + long expectedD = expected[i]; + long actualD = actual[i]; + // An unset column will show as NIL + if (expectedD == UNSET_DESCR && actualD == NIL_DESCR) + continue; + if (actualD != expectedD) + return false; + } + return true; + } + public static boolean ltsEqual(long[] expected, long[] actual) { + if (actual == LTS_UNKNOWN) + return true; + if (actual == expected) return true; if (actual == null || expected == null) @@ -248,35 +231,27 @@ public class QuiescentChecker implements Model public static void assertStaticRow(PartitionState partitionState, List actualRows, - Reconciler.RowState staticRow, + PartitionState.RowState staticRow, ResultSetRow actualRowState, - Query query, - String trackerState, - SchemaSpec schemaSpec) + ValueGenerators valueGenerators) { - if (!Arrays.equals(staticRow.vds, actualRowState.sds)) - throw new ValidationException(trackerState, - partitionState.toString(schemaSpec), - toString(actualRows), + if (!vdsEqual(staticRow.vds, actualRowState.sds)) + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Returned static row state doesn't match the one predicted by the model:" + "\nExpected: %s (%s)" + - "\nActual: %s (%s)." + - "\nQuery: %s", - descriptorsToString(staticRow.vds), staticRow.toString(schemaSpec), - descriptorsToString(actualRowState.sds), actualRowState, - query.toSelectStatement()); + "\nActual: %s (%s).", + descriptorsToString(staticRow.vds), staticRow.toString(valueGenerators), + descriptorsToString(actualRowState.sds), actualRowState); if (!ltsEqual(staticRow.lts, actualRowState.slts)) - throw new ValidationException(trackerState, - partitionState.toString(schemaSpec), - toString(actualRows), + throw new ValidationException(partitionState.toString(), + toString(valueGenerators, actualRows), "Timestamps in the static row state don't match ones predicted by the model:" + "\nExpected: %s (%s)" + - "\nActual: %s (%s)." + - "\nQuery: %s", - Arrays.toString(staticRow.lts), staticRow.toString(schemaSpec), - Arrays.toString(actualRowState.slts), actualRowState, - query.toSelectStatement()); + "\nActual: %s (%s).", + Arrays.toString(staticRow.lts), staticRow.toString(valueGenerators), + Arrays.toString(actualRowState.slts), actualRowState); } public static String descriptorsToString(long[] descriptors) @@ -284,33 +259,44 @@ public class QuiescentChecker implements Model StringBuilder sb = new StringBuilder(); for (int i = 0; i < descriptors.length; i++) { + if (i > 0) + sb.append(", "); if (descriptors[i] == NIL_DESCR) sb.append("NIL"); - if (descriptors[i] == UNSET_DESCR) + else if (descriptors[i] == UNSET_DESCR) sb.append("UNSET"); else sb.append(descriptors[i]); - if (i > 0) - sb.append(", "); } return sb.toString(); } - public static String toString(Collection collection, SchemaSpec schema) + public static String toString(Collection collection, ValueGenerators valueGenerators) { StringBuilder builder = new StringBuilder(); - for (Reconciler.RowState rowState : collection) - builder.append(rowState.toString(schema)).append("\n"); + for (PartitionState.RowState rowState : collection) + builder.append(rowState.toString(valueGenerators)).append("\n"); return builder.toString(); } - public static String toString(List collection) + public static String toString(ValueGenerators valueGenerators, List collection) { StringBuilder builder = new StringBuilder(); for (ResultSetRow rowState : collection) - builder.append(rowState.toString()).append("\n"); + builder.append(rowState.toString(valueGenerators)).append("\n"); return builder.toString(); } -} + + + public static class ValidationException extends RuntimeException + { + public ValidationException(String partitionState, String observedState, String format, Object... objects) + { + super(String.format(format, objects) + + "\nPartition state:\n" + partitionState + + "\nObserved state:\n" + observedState); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/QuiescentLocalStateCheckerBase.java b/test/harry/main/org/apache/cassandra/harry/model/QuiescentLocalStateCheckerBase.java deleted file mode 100644 index 54ea8638b9..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/QuiescentLocalStateCheckerBase.java +++ /dev/null @@ -1,124 +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.harry.model; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.cassandra.harry.sut.TokenPlacementModel.Replica; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.reconciler.Reconciler; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.tracker.DataTracker; - -import static org.apache.cassandra.harry.model.SelectHelper.resultSetToRow; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.Node; - -public abstract class QuiescentLocalStateCheckerBase extends QuiescentChecker -{ - private static final Logger logger = LoggerFactory.getLogger(QuiescentLocalStateCheckerBase.class); - - public final SystemUnderTest sut; - public final TokenPlacementModel.ReplicationFactor rf; - private final OpSelectors.PdSelector pdSelector; - - public QuiescentLocalStateCheckerBase(Run run, TokenPlacementModel.ReplicationFactor rf) - { - this(run.clock, - run.pdSelector, - run.sut, - run.tracker, - run.schemaSpec, - new Reconciler(run), - rf); - } - - public QuiescentLocalStateCheckerBase(OpSelectors.Clock clock, - OpSelectors.PdSelector pdSelector, - SystemUnderTest sut, - DataTracker tracker, - SchemaSpec schema, - Reconciler reconciler, - TokenPlacementModel.ReplicationFactor rf) - { - super(clock, sut, tracker, schema, reconciler); - this.sut = sut; - this.rf = rf; - this.pdSelector = pdSelector; - } - - @SuppressWarnings("unused") - public void validateAll() - { - TokenPlacementModel.ReplicatedRanges ring = getRing(); - - for (int lts = 0; lts < clock.peek(); lts++) - validate(Query.selectAllColumns(schema, pdSelector.pd(lts, schema), false), ring); - } - - @Override - public void validate(Query query) - { - TokenPlacementModel.ReplicatedRanges ring = getRing(); - tracker.beginValidation(query.pd); - validate(query, ring); - tracker.endValidation(query.pd); - } - - protected void validate(Query query, TokenPlacementModel.ReplicatedRanges ring) - { - CompiledStatement compiled = query.toSelectStatement(); - List replicas = ring.replicasFor(token(query.pd)); - - logger.trace("Predicted {} as replicas for {}. Ring: {}", replicas, query.pd, ring); - for (Replica replica : replicas) - { - try - { - validate(() -> { - Object[][] objects = executeNodeLocal(compiled.cql(), replica.node(), compiled.bindings()); - - List result = new ArrayList<>(); - for (Object[] obj : objects) - result.add(resultSetToRow(query.schemaSpec, clock, obj)); - - return result; - }, query); - } - catch (ValidationException e) - { - throw new AssertionError(String.format("Caught error while validating replica %s of replica set %s", - replica, replicas), - e); - } - } - } - - protected abstract TokenPlacementModel.ReplicatedRanges getRing(); - protected abstract long token(long pd); - protected abstract Object[][] executeNodeLocal(String statement, Node node, Object... bindings); -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/Reconciler.java b/test/harry/main/org/apache/cassandra/harry/model/Reconciler.java new file mode 100644 index 0000000000..c0956d51c2 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/Reconciler.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.model; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.op.Operations; + +import static org.apache.cassandra.harry.op.Operations.Operation; + +// TODO: unused at the moment +public class Reconciler +{ + private final ValueGenerators valueGenerators; + + public Reconciler(ValueGenerators valueGenerators) + { + this.valueGenerators = valueGenerators; + } + + public Map inflate(Iterator iter) + { + Map state = new HashMap<>(); + apply(state, iter); + + Map result = new HashMap<>(); + for (Map.Entry e : state.entrySet()) + result.put(e.getKey(), e.getValue().build()); + return result; + } + + public void apply(Map state, Iterator iter) + { + assert iter.hasNext(); + + while (iter.hasNext()) + { + Visit visit = iter.next(); + for (Long pd : visit.visitedPartitions) + state.computeIfAbsent(pd, pd_ -> new PartitionStateBuilder(valueGenerators, new PartitionState(pd, valueGenerators))) + .beginLts(visit.lts); + + + for (Operation operation : visit.operations) + { + if (operation instanceof Operations.PartitionOperation) + state.get(((Operations.PartitionOperation) operation).pd()).operation(operation); + } + + for (Long pd : visit.visitedPartitions) + state.computeIfAbsent(pd, pd_ -> new PartitionStateBuilder(valueGenerators, new PartitionState(pd, valueGenerators))) + .endLts(visit.lts); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/RepairingLocalStateValidator.java b/test/harry/main/org/apache/cassandra/harry/model/RepairingLocalStateValidator.java deleted file mode 100644 index 2f310db93f..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/RepairingLocalStateValidator.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.harry.model; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.visitors.AllPartitionsValidator; -import org.apache.cassandra.harry.visitors.QueryLogger; -import org.apache.cassandra.harry.visitors.Visitor; - -/** - * Validator similar to {@link AllPartitionsValidator}, but performs - * repair before checking node states. - * - * Can be useful for testing repair, bootstrap, and streaming code. - */ -public class RepairingLocalStateValidator extends AllPartitionsValidator -{ - private static final Logger logger = LoggerFactory.getLogger(RepairingLocalStateValidator.class); - - private final InJvmSut inJvmSut; - - public static Configuration.VisitorConfiguration factoryForTests(int concurrency, Model.ModelFactory modelFactory) - { - return (r) -> new RepairingLocalStateValidator(r, concurrency, modelFactory); - } - - public RepairingLocalStateValidator(Run run, int concurrency, Model.ModelFactory modelFactory) - { - this(run, concurrency, modelFactory, QueryLogger.NO_OP); - } - - public RepairingLocalStateValidator(Run run, int concurrency, Model.ModelFactory modelFactory, QueryLogger queryLogger) - { - super(run, concurrency, modelFactory, queryLogger); - this.inJvmSut = (InJvmSut) run.sut; - } - - public void visit() - { - logger.debug("Starting repair..."); - inJvmSut.cluster().stream().forEach((instance) -> instance.nodetool("repair", "--full")); - logger.debug("Validating partitions..."); - super.visit(); - } - - @JsonTypeName("repair_and_validate_local_states") - public static class RepairingLocalStateValidatorConfiguration implements Configuration.VisitorConfiguration - { - private final int concurrency; - private final Configuration.ModelConfiguration modelConfiguration; - private final Configuration.QueryLoggerConfiguration query_logger; - - @JsonCreator - public RepairingLocalStateValidatorConfiguration(@JsonProperty("concurrency") int concurrency, - @JsonProperty("model") Configuration.ModelConfiguration model, - @JsonProperty("query_logger") Configuration.QueryLoggerConfiguration query_logger) - { - this.concurrency = concurrency; - this.modelConfiguration = model; - this.query_logger = QueryLogger.thisOrDefault(query_logger); - } - - public Visitor make(Run run) - { - return new RepairingLocalStateValidator(run, concurrency, modelConfiguration, query_logger.make()); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/SelectHelper.java b/test/harry/main/org/apache/cassandra/harry/model/SelectHelper.java deleted file mode 100644 index b2888afa19..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/SelectHelper.java +++ /dev/null @@ -1,348 +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.harry.model; - -import java.util.*; - -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.DataGenerators; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.Relation; -import org.apache.cassandra.harry.operations.Query; - -import static org.apache.cassandra.harry.gen.DataGenerators.UNSET_DESCR; - -public class SelectHelper -{ - private static final long[] EMPTY_ARR = new long[]{}; - public static CompiledStatement selectWildcard(SchemaSpec schema, long pd) - { - return select(schema, pd, null, Collections.emptyList(), false, true); - } - - public static CompiledStatement select(SchemaSpec schema, long pd) - { - return select(schema, pd, schema.allColumnsSet, Collections.emptyList(), false, true); - } - - /** - * Here, {@code reverse} should be understood not in ASC/DESC sense, but rather in terms - * of how we're going to iterate through this partition (in other words, if first clustering component order - * is DESC, we'll iterate in ASC order) - */ - public static CompiledStatement select(SchemaSpec schema, long pd, List relations, boolean reverse, boolean includeWriteTime) - { - return select(schema, pd, schema.allColumnsSet, relations, reverse, includeWriteTime); - } - - public static CompiledStatement selectWildcard(SchemaSpec schema, long pd, List relations, boolean reverse, boolean includeWriteTime) - { - return select(schema, pd, null, relations, reverse, includeWriteTime); - } - - public static CompiledStatement select(SchemaSpec schema, Long pd, Set> columns, List relations, boolean reverse, boolean includeWriteTime) - { - boolean isWildcardQuery = columns == null; - if (isWildcardQuery) - { - columns = schema.allColumnsSet; - includeWriteTime = false; - } - - StringBuilder b = new StringBuilder(); - b.append("SELECT "); - - boolean isFirst = true; - if (isWildcardQuery) - { - b.append("*"); - } - else - { - for (int i = 0; i < schema.allColumns.size(); i++) - { - ColumnSpec spec = schema.allColumns.get(i); - if (columns != null && !columns.contains(spec)) - continue; - - if (isFirst) - isFirst = false; - else - b.append(", "); - b.append(spec.name); - } - } - - if (includeWriteTime) - { - for (ColumnSpec spec : schema.staticColumns) - { - if (columns != null && !columns.contains(spec)) - continue; - b.append(", ") - .append("writetime(") - .append(spec.name) - .append(")"); - } - - for (ColumnSpec spec : schema.regularColumns) - { - if (columns != null && !columns.contains(spec)) - continue; - b.append(", ") - .append("writetime(") - .append(spec.name) - .append(")"); - } - } - - if (schema.trackLts) - b.append(", visited_lts"); - - b.append(" FROM ") - .append(schema.keyspace) - .append(".") - .append(schema.table) - .append(" WHERE "); - - List bindings = new ArrayList<>(); - - SchemaSpec.AddRelationCallback consumer = new SchemaSpec.AddRelationCallback() - { - boolean isFirst = true; - public void accept(ColumnSpec spec, Relation.RelationKind kind, Object value) - { - if (isFirst) - isFirst = false; - else - b.append(" AND "); - b.append(kind.getClause(spec)); - bindings.add(value); - } - }; - if (pd != null) - { - Object[] pk = schema.inflatePartitionKey(pd); - for (int i = 0; i < pk.length; i++) - consumer.accept(schema.partitionKeys.get(i), Relation.RelationKind.EQ, pk[i]); - - } - schema.inflateRelations(relations, consumer); - - addOrderBy(schema, b, reverse); - b.append(";"); - Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); - return new CompiledStatement(b.toString(), bindingsArr); - } - - public static CompiledStatement count(SchemaSpec schema, long pd) - { - StringBuilder b = new StringBuilder(); - b.append("SELECT count(*) "); - - b.append(" FROM ") - .append(schema.keyspace) - .append(".") - .append(schema.table) - .append(" WHERE "); - - List bindings = new ArrayList<>(schema.partitionKeys.size()); - - schema.inflateRelations(pd, - Collections.emptyList(), - new SchemaSpec.AddRelationCallback() - { - boolean isFirst = true; - public void accept(ColumnSpec spec, Relation.RelationKind kind, Object value) - { - if (isFirst) - isFirst = false; - else - b.append(" AND "); - b.append(kind.getClause(spec)); - bindings.add(value); - } - }); - - Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); - return new CompiledStatement(b.toString(), bindingsArr); - } - - private static void addOrderBy(SchemaSpec schema, StringBuilder b, boolean reverse) - { - if (reverse && schema.clusteringKeys.size() > 0) - { - b.append(" ORDER BY "); - for (int i = 0; i < schema.clusteringKeys.size(); i++) - { - ColumnSpec c = schema.clusteringKeys.get(i); - if (i > 0) - b.append(", "); - b.append(c.isReversed() ? asc(c.name) : desc(c.name)); - } - } - } - - public static String asc(String name) - { - return name + " ASC"; - } - - public static String desc(String name) - { - return name + " DESC"; - } - - - public static Object[] broadenResult(SchemaSpec schemaSpec, Set> columns, Object[] result) - { - boolean isWildcardQuery = columns == null; - - if (isWildcardQuery) - columns = schemaSpec.allColumnsSet; - else if (schemaSpec.allColumns.size() == columns.size()) - return result; - - Object[] newRes = new Object[schemaSpec.allColumns.size() + schemaSpec.staticColumns.size() + schemaSpec.regularColumns.size()]; - - int origPointer = 0; - int newPointer = 0; - for (int i = 0; i < schemaSpec.allColumns.size(); i++) - { - ColumnSpec column = schemaSpec.allColumns.get(i); - if (columns.contains(column)) - newRes[newPointer] = result[origPointer++]; - else - newRes[newPointer] = DataGenerators.UNSET_VALUE; - newPointer++; - } - - // Make sure to include writetime, but only in case query actually includes writetime (for example, it's not a wildcard query) - for (int i = 0; i < schemaSpec.staticColumns.size() && origPointer < result.length; i++) - { - ColumnSpec column = schemaSpec.staticColumns.get(i); - if (columns.contains(column)) - newRes[newPointer] = result[origPointer++]; - else - newRes[newPointer] = null; - newPointer++; - } - - for (int i = 0; i < schemaSpec.regularColumns.size() && origPointer < result.length; i++) - { - ColumnSpec column = schemaSpec.regularColumns.get(i); - if (columns.contains(column)) - newRes[newPointer] = result[origPointer++]; - else - newRes[newPointer] = null; - newPointer++; - } - - return newRes; - } - - static boolean isDeflatable(Object[] columns) - { - for (Object column : columns) - { - if (column == DataGenerators.UNSET_VALUE) - return false; - } - return true; - } - - public static ResultSetRow resultSetToRow(SchemaSpec schema, OpSelectors.Clock clock, Object[] result) - { - Object[] partitionKey = new Object[schema.partitionKeys.size()]; - Object[] clusteringKey = new Object[schema.clusteringKeys.size()]; - Object[] staticColumns = new Object[schema.staticColumns.size()]; - Object[] regularColumns = new Object[schema.regularColumns.size()]; - - System.arraycopy(result, 0, partitionKey, 0, partitionKey.length); - System.arraycopy(result, partitionKey.length, clusteringKey, 0, clusteringKey.length); - System.arraycopy(result, partitionKey.length + clusteringKey.length, staticColumns, 0, staticColumns.length); - System.arraycopy(result, partitionKey.length + clusteringKey.length + staticColumns.length, regularColumns, 0, regularColumns.length); - - - List visited_lts_list; - if (schema.trackLts) - { - visited_lts_list = (List) result[result.length - 1]; - visited_lts_list.sort(Long::compare); - } - else - { - visited_lts_list = Collections.emptyList(); - } - - long[] slts = new long[schema.staticColumns.size()]; - Arrays.fill(slts, Model.NO_TIMESTAMP); - for (int i = 0, sltsBase = schema.allColumns.size(); i < slts.length && sltsBase + i < result.length; i++) - { - Object v = result[schema.allColumns.size() + i]; - if (v != null) - slts[i] = clock.lts((long) v); - } - - long[] lts = new long[schema.regularColumns.size()]; - Arrays.fill(lts, Model.NO_TIMESTAMP); - for (int i = 0, ltsBase = schema.allColumns.size() + slts.length; i < lts.length && ltsBase + i < result.length; i++) - { - Object v = result[ltsBase + i]; - if (v != null) - lts[i] = clock.lts((long) v); - } - - return new ResultSetRow(isDeflatable(partitionKey) ? schema.deflatePartitionKey(partitionKey) : UNSET_DESCR, - isDeflatable(clusteringKey) ? schema.deflateClusteringKey(clusteringKey) : UNSET_DESCR, - schema.staticColumns.isEmpty() ? EMPTY_ARR : schema.deflateStaticColumns(staticColumns), - schema.staticColumns.isEmpty() ? EMPTY_ARR : slts, - schema.deflateRegularColumns(regularColumns), - lts, - visited_lts_list); - } - - public static List execute(SystemUnderTest sut, OpSelectors.Clock clock, Query query) - { - return execute(sut, clock, query, query.schemaSpec.allColumnsSet); - } - - public static List execute(SystemUnderTest sut, OpSelectors.Clock clock, Query query, Set> columns) - { - CompiledStatement compiled = query.toSelectStatement(columns, true); - Object[][] objects = sut.executeIdempotent(compiled.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, compiled.bindings()); - List result = new ArrayList<>(); - for (Object[] obj : objects) - result.add(resultSetToRow(query.schemaSpec, clock, broadenResult(query.schemaSpec, columns, obj))); - return result; - } - - public static List execute(SystemUnderTest sut, OpSelectors.Clock clock, CompiledStatement compiled, SchemaSpec schemaSpec) - { - Set> columns = schemaSpec.allColumnsSet; - Object[][] objects = sut.executeIdempotent(compiled.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, compiled.bindings()); - List result = new ArrayList<>(); - for (Object[] obj : objects) - result.add(resultSetToRow(schemaSpec, clock, broadenResult(schemaSpec, columns, obj))); - return result; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/TokenPlacementModel.java b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java similarity index 90% rename from test/harry/main/org/apache/cassandra/harry/sut/TokenPlacementModel.java rename to test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java index a25d1ffc6e..2c24ec257c 100644 --- a/test/harry/main/org/apache/cassandra/harry/sut/TokenPlacementModel.java +++ b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java @@ -1,22 +1,22 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -package org.apache.cassandra.harry.sut; +package org.apache.cassandra.harry.model; import java.net.InetAddress; import java.net.UnknownHostException; @@ -95,11 +95,17 @@ public class TokenPlacementModel { public final Range[] ranges; public final NavigableMap> placementsForRange; + private final Map> replicaToRanges = new HashMap<>(); public ReplicatedRanges(Range[] ranges, NavigableMap> placementsForRange) { this.ranges = ranges; this.placementsForRange = placementsForRange; + for (Map.Entry> e : placementsForRange.entrySet()) + { + for (Replica replica : e.getValue()) + replicaToRanges.computeIfAbsent(replica, i -> new ArrayList<>()).add(e.getKey()); + } } public List replicasFor(long token) @@ -118,7 +124,7 @@ public class TokenPlacementModel return placementsForRange.get(ranges[idx]); } - public NavigableMap> asMap() + public NavigableMap> asMap() { return placementsForRange; } @@ -143,6 +149,11 @@ public class TokenPlacementModel } return -(low + 1); // key not found } + + public List ranges(Replica replica) + { + return replicaToRanges.get(replica); + } } public interface CompareTo @@ -150,12 +161,12 @@ public class TokenPlacementModel int compareTo(V v); } - public static void addIfUnique(List replicas, Set names, Replica replica) + private static void addIfUnique(List replicas, Set names, Replica replica) { - if (names.contains(replica.node().idx())) + if (names.contains(replica.node().id())) return; replicas.add(replica); - names.add(replica.node().idx()); + names.add(replica.node().id()); } /** @@ -210,22 +221,41 @@ public class TokenPlacementModel List nodes = new ArrayList<>(); for (Object[] row : resultset) { - InetAddress address = (InetAddress) row[0]; - Set tokens = (Set) row[1]; - String dc = (String) row[2]; - String rack = (String) row[3]; + InetAddress address = get(row, 0, "address"); + Set tokens = get(row, 1, "tokens"); + String dc = get(row, 2, "dc"); + String rack = get(row, 3, "rack"); for (String token : tokens) { nodes.add(new Node(0, 0, 0, 0, - constantLookup(address.toString(), - Long.parseLong(token), - dc, - rack))); + constantLookup(address.toString(), + Long.parseLong(token), + dc, + rack))); } } return nodes; } + private static T get(Object[] row, int idx, String name) + { + T t = (T) row[idx]; + if (t == null || ((t instanceof Collection) && ((Collection) t).isEmpty())) + throw new IncompletePeersStateException(name); + return t; + } + + /** + * When the node sees the new epoch, the update of the peers table is async, so checking the table may yield partial results, so need some way to detect this to enable retries. + */ + public static class IncompletePeersStateException extends RuntimeException + { + public IncompletePeersStateException(String column) + { + super("Found incomplete column " + column); + } + } + public static class NtsReplicationFactor extends ReplicationFactor { private KeyspaceParams keyspaceParams; @@ -399,8 +429,8 @@ public class TokenPlacementModel public String toString() { return "NtsReplicationFactor{" + - "map=" + asMap() + - '}'; + "map=" + asMap() + + '}'; } } @@ -483,11 +513,11 @@ public class TokenPlacementModel public String toString() { return "DatacenterNodes{" + - "nodes=" + nodes + - ", racks=" + racks + - ", rfLeft=" + rfLeft + - ", acceptableRackRepeats=" + acceptableRackRepeats + - '}'; + "nodes=" + nodes + + ", racks=" + racks + + ", rfLeft=" + rfLeft + + ", acceptableRackRepeats=" + acceptableRackRepeats + + '}'; } } @@ -589,7 +619,7 @@ public class TokenPlacementModel Range skipped = null; for (Range range : ranges) { - Set names = new HashSet<>(); + Set names = new HashSet<>(); List replicas = new ArrayList<>(); int idx = primaryReplica(nodes, range); if (idx >= 0) @@ -626,8 +656,8 @@ public class TokenPlacementModel public String toString() { return "SimpleReplicationFactor{" + - "rf=" + dcReplicas().toString() + - '}'; + "rf=" + dcReplicas().toString() + + '}'; } } @@ -681,9 +711,9 @@ public class TokenPlacementModel public String toString() { return "(" + - "" + (start == Long.MIN_VALUE ? "MIN" : start) + - ", " + (end == Long.MIN_VALUE ? "MIN" : end) + - ']'; + "" + (start == Long.MIN_VALUE ? "MIN" : start) + + ", " + (end == Long.MIN_VALUE ? "MIN" : end) + + ']'; } } diff --git a/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModelHelper.java b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModelHelper.java new file mode 100644 index 0000000000..c7976e8d2d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModelHelper.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.ICoordinator; + +import static org.apache.cassandra.harry.model.TokenPlacementModel.peerStateToNodes; + +public class TokenPlacementModelHelper +{ + /** + * Updates to the peers table is async, which can see partial state... to avoid this issue retry until the writes have become stable. + */ + private static List retryWhenNotComplete(ICoordinator coordinator, String query) + { + TokenPlacementModel.IncompletePeersStateException last = null; + // 20 retries with 500ms ~10s... + for (int i = 0; i < 20; i++) + { + try + { + return peerStateToNodes(coordinator.execute(query, ConsistencyLevel.ONE)); + } + catch (TokenPlacementModel.IncompletePeersStateException e) + { + last = e; + Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS); + } + } + throw last; + } + + public static TokenPlacementModel.ReplicatedRanges getRing(ICoordinator coordinator, TokenPlacementModel.ReplicationFactor rf) + { + List other = retryWhenNotComplete(coordinator, "select peer, tokens, data_center, rack from system.peers"); + List self = retryWhenNotComplete(coordinator, "select broadcast_address, tokens, data_center, rack from system.local"); + List all = new ArrayList<>(); + all.addAll(self); + all.addAll(other); + all.sort(TokenPlacementModel.Node::compareTo); + return rf.replicate(all); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/reconciler/PartitionState.java b/test/harry/main/org/apache/cassandra/harry/model/reconciler/PartitionState.java deleted file mode 100644 index 3484b1cdc4..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/reconciler/PartitionState.java +++ /dev/null @@ -1,310 +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.harry.model.reconciler; - -import java.util.*; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.operations.FilteringQuery; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.util.BitSet; -import org.apache.cassandra.harry.util.DescriptorRanges; -import org.apache.cassandra.harry.gen.DataGenerators; -import org.apache.cassandra.harry.model.Model; - -public class PartitionState implements Iterable -{ - private static final Logger logger = LoggerFactory.getLogger(Reconciler.class); - - public final long pd; - final long debugCd; - final SchemaSpec schema; - final List visitedLts = new ArrayList<>(); - final List skippedLts = new ArrayList<>(); - - // Collected state - Reconciler.RowState staticRow; - - final NavigableMap rows; - - public PartitionState(long pd, long debugCd, SchemaSpec schema) - { - this.pd = pd; - this.rows = new TreeMap<>(); - this.staticRow = new Reconciler.RowState(this, - Reconciler.STATIC_CLUSTERING, - Reconciler.arr(schema.staticColumns.size(), DataGenerators.NIL_DESCR), - Reconciler.arr(schema.staticColumns.size(), Model.NO_TIMESTAMP)); - this.debugCd = debugCd; - this.schema = schema; - } - - private PartitionState(long pd, long debugCd, Reconciler.RowState staticRow, NavigableMap rows, SchemaSpec schema) - { - this.pd = pd; - this.rows = rows; - this.staticRow = new Reconciler.RowState(this, Reconciler.STATIC_CLUSTERING, - Arrays.copyOf(staticRow.vds, staticRow.vds.length), - Arrays.copyOf(staticRow.lts, staticRow.lts.length)); - this.debugCd = debugCd; - this.schema = schema; - } - - public void compareVisitedLts(List actualVisitedLts) - { - long min = actualVisitedLts.get(0); - int predictedIdx = Collections.binarySearch(visitedLts, min); - List predictedSublist = visitedLts.subList(predictedIdx, visitedLts.size()); - Set set = new HashSet<>(actualVisitedLts); - for (long lts : predictedSublist) - { - if (!set.contains(lts)) - throw new IllegalStateException(String.format("Predicted visit to %d, but did not see it in the debug row\n" + - "Actual: %s\n" + - "Predicted: %s", - lts, actualVisitedLts, predictedSublist)); - } - } - - public NavigableMap rows() - { - return rows; - } - - public PartitionState filter(FilteringQuery query) - { - NavigableMap rows = new TreeMap<>(); - for (Long cd : this.rows.keySet()) - { - Reconciler.RowState rowState = this.rows.get(cd); - if (query.match(rowState)) - rows.put(cd, rowState); - } - PartitionState ps = new PartitionState(pd, debugCd, staticRow, rows, schema); - return ps; - } - - public void writeStaticRow(long[] sds, long lts) - { - staticRow = updateRowState(staticRow, schema.staticColumns, Reconciler.STATIC_CLUSTERING, sds, lts, false); - } - - public void write(long cd, long[] vds, long lts, boolean writePrimaryKeyLiveness) - { - rows.compute(cd, (cd_, current) -> updateRowState(current, schema.regularColumns, cd, vds, lts, writePrimaryKeyLiveness)); - } - - public void delete(DescriptorRanges.DescriptorRange range, long lts) - { - if (range.minBound > range.maxBound) - return; - - Iterator> iter = rows.subMap(range.minBound, range.minInclusive, - range.maxBound, range.maxInclusive) - .entrySet() - .iterator(); - while (iter.hasNext()) - { - Map.Entry e = iter.next(); - if (debugCd != -1 && e.getKey() == debugCd) - logger.info("Hiding {} at {} because of range tombstone {}", debugCd, lts, range); - - // assert row state doesn't have fresher lts - iter.remove(); - } - } - - public void delete(long cd, long lts) - { - Reconciler.RowState state = rows.remove(cd); - if (state != null) - { - for (long v : state.lts) - assert lts >= v : String.format("Attempted to remove a row with a tombstone that has older timestamp (%d): %s", lts, state); - } - } - - public boolean isEmpty() - { - return rows.isEmpty(); - } - - /** - * Method used to update row state of both static and regular rows. - */ - private Reconciler.RowState updateRowState(Reconciler.RowState currentState, List> columns, long cd, long[] vds, long lts, boolean writePrimaryKeyLiveness) - { - if (currentState == null) - { - long[] ltss = new long[vds.length]; - long[] vdsCopy = new long[vds.length]; - for (int i = 0; i < vds.length; i++) - { - if (vds[i] != DataGenerators.UNSET_DESCR) - { - ltss[i] = lts; - vdsCopy[i] = vds[i]; - } - else - { - ltss[i] = Model.NO_TIMESTAMP; - vdsCopy[i] = DataGenerators.NIL_DESCR; - } - } - - currentState = new Reconciler.RowState(this, cd, vdsCopy, ltss); - } - else - { - assert currentState.vds.length == vds.length : String.format("Vds: %d, sds: %d", currentState.vds.length, vds.length); - for (int i = 0; i < vds.length; i++) - { - if (vds[i] == DataGenerators.UNSET_DESCR) - continue; - - assert lts >= currentState.lts[i] : String.format("Out-of-order LTS: %d. Max seen: %s", lts, currentState.lts[i]); // sanity check; we're iterating in lts order - - if (currentState.lts[i] == lts) - { - // Timestamp collision case - ColumnSpec column = columns.get(i); - if (column.type.compareLexicographically(vds[i], currentState.vds[i]) > 0) - currentState.vds[i] = vds[i]; - } - else - { - currentState.vds[i] = vds[i]; - assert lts > currentState.lts[i]; - currentState.lts[i] = lts; - } - } - } - - if (writePrimaryKeyLiveness) - currentState.hasPrimaryKeyLivenessInfo = true; - - return currentState; - } - - public void deleteRegularColumns(long lts, long cd, int columnOffset, org.apache.cassandra.harry.util.BitSet columns, org.apache.cassandra.harry.util.BitSet mask) - { - deleteColumns(lts, rows.get(cd), columnOffset, columns, mask); - } - - public void deleteStaticColumns(long lts, int columnOffset, org.apache.cassandra.harry.util.BitSet columns, org.apache.cassandra.harry.util.BitSet mask) - { - deleteColumns(lts, staticRow, columnOffset, columns, mask); - } - - public void deleteColumns(long lts, Reconciler.RowState state, int columnOffset, org.apache.cassandra.harry.util.BitSet columns, BitSet mask) - { - if (state == null) - return; - - //TODO: optimise by iterating over the columns that were removed by this deletion - //TODO: optimise final decision to fully remove the column by counting a number of set/unset columns - boolean allNil = true; - for (int i = 0; i < state.vds.length; i++) - { - if (columns.isSet(columnOffset + i, mask)) - { - state.vds[i] = DataGenerators.NIL_DESCR; - state.lts[i] = Model.NO_TIMESTAMP; - } - else if (state.vds[i] != DataGenerators.NIL_DESCR) - { - allNil = false; - } - } - - if (state.cd != Reconciler.STATIC_CLUSTERING && allNil & !state.hasPrimaryKeyLivenessInfo) - delete(state.cd, lts); - } - - public void deletePartition(long lts) - { - if (debugCd != -1) - logger.info("Hiding {} at {} because partition deletion", debugCd, lts); - - rows.clear(); - if (!schema.staticColumns.isEmpty()) - { - Arrays.fill(staticRow.vds, DataGenerators.NIL_DESCR); - Arrays.fill(staticRow.lts, Model.NO_TIMESTAMP); - } - } - - public Iterator iterator() - { - return iterator(false); - } - - public Iterator iterator(boolean reverse) - { - if (reverse) - return rows.descendingMap().values().iterator(); - - return rows.values().iterator(); - } - - public Collection rows(boolean reverse) - { - if (reverse) - return rows.descendingMap().values(); - - return rows.values(); - } - - public Reconciler.RowState staticRow() - { - return staticRow; - } - - public PartitionState apply(Query query) - { - PartitionState partitionState = new PartitionState(pd, debugCd, schema); - partitionState.staticRow = staticRow; - // TODO: we could improve this if we could get original descriptors - for (Reconciler.RowState rowState : rows.values()) - if (query.matchCd(rowState.cd)) - partitionState.rows.put(rowState.cd, rowState); - - return partitionState; - } - - public String toString(SchemaSpec schema) - { - StringBuilder sb = new StringBuilder(); - - sb.append("Visited LTS: " + visitedLts).append("\n"); - sb.append("Skipped LTS: " + skippedLts).append("\n"); - - if (staticRow != null) - sb.append("Static row:\n" + staticRow.toString(schema)).append("\n"); - - for (Reconciler.RowState row : rows.values()) - sb.append(row.toString(schema)).append("\n"); - - return sb.toString(); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/model/reconciler/Reconciler.java b/test/harry/main/org/apache/cassandra/harry/model/reconciler/Reconciler.java deleted file mode 100644 index 23ba8a41b5..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/reconciler/Reconciler.java +++ /dev/null @@ -1,342 +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.harry.model.reconciler; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.util.DescriptorRanges; -import org.apache.cassandra.harry.util.StringUtils; -import org.apache.cassandra.harry.visitors.GeneratingVisitor; -import org.apache.cassandra.harry.visitors.LtsVisitor; -import org.apache.cassandra.harry.visitors.VisitExecutor; -import org.apache.cassandra.harry.gen.DataGenerators; - -/** - * A simple Cassandra-style reconciler for operations against model state. - *

- * It is useful both as a testing/debugging tool (to avoid starting Cassandra - * cluster to get a result set), and as a quiescent model checker. - * - * TODO: it might be useful to actually record deletions instead of just removing values as we do right now. - */ -public class Reconciler -{ - private static final Logger logger = LoggerFactory.getLogger(Reconciler.class); - - public static long STATIC_CLUSTERING = DataGenerators.NIL_DESCR; - - private final OpSelectors.PdSelector pdSelector; - private final SchemaSpec schema; - - private final Function visitorFactory; - - public Reconciler(Run run) - { - this(run, - (processor) -> new GeneratingVisitor(run, processor)); - } - - public Reconciler(Run run, Function ltsVisitorFactory) - { - this(run.pdSelector, run.schemaSpec, ltsVisitorFactory); - } - - public Reconciler(OpSelectors.PdSelector pdSelector, SchemaSpec schema, Function ltsVisitorFactory) - { - this.pdSelector = pdSelector; - this.schema = schema; - this.visitorFactory = ltsVisitorFactory; - } - - private final long debugCd = -1L; - - public PartitionState inflatePartitionState(final long pd, DataTracker tracker, Query query) - { - PartitionState partitionState = new PartitionState(pd, debugCd, schema); - - class Processor extends VisitExecutor - { - // Whether a partition deletion was encountered on this LTS. - private boolean hadPartitionDeletion = false; - private boolean hadTrackingRowWrite = false; - private final List rangeDeletes = new ArrayList<>(); - private final List writes = new ArrayList<>(); - private final List columnDeletes = new ArrayList<>(); - - @Override - protected void operation(Operation operation) - { - if (hadPartitionDeletion) - return; - - long lts = operation.lts(); - assert pdSelector.pd(operation.lts(), schema) == operation.pd() : String.format("Computed partition descriptor (%d) does for the lts %d. Does not match actual descriptor %d", - pdSelector.pd(operation.lts(), schema), - operation.lts(), - operation.pd()); - - if (operation.kind().hasVisibleVisit()) - partitionState.visitedLts.add(operation.lts()); - - if (schema.trackLts) - hadTrackingRowWrite = true; - - switch (operation.kind()) - { - case DELETE_RANGE: - case DELETE_SLICE: - DescriptorRanges.DescriptorRange range = ((DeleteOp) operation).relations().toRange(lts); - rangeDeletes.add(range); - partitionState.delete(range, lts); - break; - case DELETE_ROW: - long cd = ((DeleteRowOp) operation).cd(); - range = new DescriptorRanges.DescriptorRange(cd, cd, true, true, lts); - rangeDeletes.add(range); - partitionState.delete(cd, lts); - break; - case DELETE_PARTITION: - partitionState.deletePartition(lts); - rangeDeletes.clear(); - writes.clear(); - columnDeletes.clear(); - hadPartitionDeletion = true; - break; - case INSERT_WITH_STATICS: - case INSERT: - case UPDATE: - case UPDATE_WITH_STATICS: - writes.add(operation); - break; - case DELETE_COLUMN_WITH_STATICS: - case DELETE_COLUMN: - columnDeletes.add(operation); - break; - default: - throw new IllegalStateException(); - } - } - - @Override - protected void beforeLts(long lts, long pd) - { - rangeDeletes.clear(); - writes.clear(); - columnDeletes.clear(); - hadPartitionDeletion = false; - } - - @Override - protected void afterLts(long lts, long pd) - { - if (hadPartitionDeletion) - return; - - outer: for (Operation op : writes) - { - WriteOp writeOp = (WriteOp) op; - long opId = op.opId(); - long cd = writeOp.cd(); - - if (hadTrackingRowWrite) - { - long[] statics = new long[schema.staticColumns.size()]; - Arrays.fill(statics, DataGenerators.UNSET_DESCR); - partitionState.writeStaticRow(statics, lts); - } - - switch (op.kind()) - { - case INSERT_WITH_STATICS: - case UPDATE_WITH_STATICS: - WriteStaticOp writeStaticOp = (WriteStaticOp) op; - // We could apply static columns during the first iteration, but it's more convenient - // to reconcile static-level deletions. - partitionState.writeStaticRow(writeStaticOp.sds(), lts); - case INSERT: - case UPDATE: - if (!query.matchCd(cd)) - { - if (debugCd != -1 && cd == debugCd) - logger.info("Hiding {} at {}/{} because there was no query match", debugCd, lts, opId); - continue outer; - } - - for (DescriptorRanges.DescriptorRange range : rangeDeletes) - { - if (range.timestamp >= lts && range.contains(cd)) - { - if (debugCd != -1 && cd == debugCd) - logger.info("Hiding {} at {}/{} because of range tombstone {}", debugCd, lts, opId, range); - continue outer; - } - } - - partitionState.write(cd, - writeOp.vds(), - lts, - op.kind() == OpSelectors.OperationKind.INSERT || op.kind() == OpSelectors.OperationKind.INSERT_WITH_STATICS); - break; - default: - throw new IllegalStateException(op.kind().toString()); - } - } - - outer: for (Operation op : columnDeletes) - { - DeleteColumnsOp deleteColumnsOp = (DeleteColumnsOp) op; - long opId = op.opId(); - long cd = deleteColumnsOp.cd(); - - switch (op.kind()) - { - case DELETE_COLUMN_WITH_STATICS: - partitionState.deleteStaticColumns(lts, - schema.staticColumnsOffset, - deleteColumnsOp.columns(), // descriptorSelector.columnMask(pd, lts, opId, op.opKind()) - schema.staticColumnsMask()); - case DELETE_COLUMN: - if (!query.matchCd(cd)) - { - if (debugCd != -1 && cd == debugCd) - logger.info("Hiding {} at {}/{} because there was no query match", debugCd, lts, opId); - continue outer; - } - - for (DescriptorRanges.DescriptorRange range : rangeDeletes) - { - if (range.timestamp >= lts && range.contains(cd)) - { - if (debugCd != -1 && cd == debugCd) - logger.info("Hiding {} at {}/{} because of range tombstone {}", debugCd, lts, opId, range); - continue outer; - } - } - - partitionState.deleteRegularColumns(lts, - cd, - schema.regularColumnsOffset, - deleteColumnsOp.columns(), - schema.regularColumnsMask()); - break; - } - } - } - - @Override - public void shutdown() throws InterruptedException {} - } - - LtsVisitor visitor = visitorFactory.apply(new Processor()); - - long currentLts = pdSelector.minLtsFor(pd); - long maxStarted = tracker.maxStarted(); - while (currentLts <= maxStarted && currentLts >= 0) - { - if (tracker.isFinished(currentLts)) - { - visitor.visit(currentLts); - } - else - { - partitionState.skippedLts.add(currentLts); - } - - currentLts = pdSelector.nextLts(currentLts); - } - - return partitionState; - } - - public static long[] arr(int length, long fill) - { - long[] arr = new long[length]; - Arrays.fill(arr, fill); - return arr; - } - - public static class RowState - { - public boolean hasPrimaryKeyLivenessInfo = false; - - public final PartitionState partitionState; - public final long cd; - public final long[] vds; - public final long[] lts; - - public RowState(PartitionState partitionState, - long cd, - long[] vds, - long[] lts) - { - this.partitionState = partitionState; - this.cd = cd; - this.vds = vds; - this.lts = lts; - } - - public RowState clone() - { - RowState rowState = new RowState(partitionState, cd, Arrays.copyOf(vds, vds.length), Arrays.copyOf(lts, lts.length)); - rowState.hasPrimaryKeyLivenessInfo = hasPrimaryKeyLivenessInfo; - return rowState; - } - - public String toString() - { - return toString(null); - } - - public String toString(SchemaSpec schema) - { - if (cd == STATIC_CLUSTERING) - { - return " rowStateRow(" - + partitionState.pd + - "L, " + cd + "L" + - ", statics(" + StringUtils.toString(partitionState.staticRow.vds) + ")" + - ", lts(" + StringUtils.toString(partitionState.staticRow.lts) + ")"; - } - else - { - return " rowStateRow(" - + partitionState.pd + - "L, " + cd + - (partitionState.staticRow == null ? "" : ", statics(" + StringUtils.toString(partitionState.staticRow.vds) + ")") + - (partitionState.staticRow == null ? "" : ", lts(" + StringUtils.toString(partitionState.staticRow.lts) + ")") + - ", values(" + StringUtils.toString(vds) + ")" + - ", lts(" + StringUtils.toString(lts) + ")" + - (schema == null ? "" : ", clustering=" + Arrays.toString(schema.inflateClusteringKey(cd))) + - (schema == null ? "" : ", values=" + Arrays.toString(schema.inflateRegularColumns(vds))) + - ")"; - } - } - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/ring/PlacementSimulator.java b/test/harry/main/org/apache/cassandra/harry/model/ring/PlacementSimulator.java deleted file mode 100644 index f6b7513212..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/model/ring/PlacementSimulator.java +++ /dev/null @@ -1,1052 +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.harry.model.ring; -// -//import java.io.BufferedWriter; -//import java.io.File; -//import java.io.FileNotFoundException; -//import java.io.FileOutputStream; -//import java.io.IOException; -//import java.io.OutputStreamWriter; -//import java.net.UnknownHostException; -//import java.util.*; -//import java.util.function.Function; -//import java.util.function.Predicate; -//import java.util.stream.Collectors; -// -//import org.apache.cassandra.distributed.api.TokenSupplier; -//import org.apache.cassandra.harry.sut.TokenPlacementModel; -// -//import static org.apache.cassandra.harry.sut.TokenPlacementModel.*; -// -///** -// * A small class that helps to avoid doing mental arithmetics on ranges. -// */ -//public class PlacementSimulator -//{ -// @SuppressWarnings("unused") // for debugging convenience -// public static List readableTokens(int number) -// { -// final List longs; -// longs = new ArrayList<>(); -// for (int i = 0; i < number; i++) -// { -// longs.add((i + 1) * 100L); -// } -// Collections.shuffle(longs, new Random(1)); -// -// return longs; -// } -// -// public static DebugLog debug = new DebugLog(); -// -// public static class SimulatedPlacements -// { -// public final ReplicationFactor rf; -// public final List nodes; -// public final NavigableMap> readPlacements; -// public final NavigableMap> writePlacements; -// // Stashed states are steps required to finish the operation. For example, in case of -// // bootstrap, this could be adding replicas to write (and then read) sets after splitting ranges. -// public final List stashedStates; -// -// public SimulatedPlacements(ReplicationFactor rf, -// List nodes, -// NavigableMap> readPlacements, -// NavigableMap> writePlacements, -// List stashedStates) -// { -// this.rf = rf; -// this.nodes = nodes; -// this.readPlacements = readPlacements; -// this.writePlacements = writePlacements; -// this.stashedStates = stashedStates; -// } -// -// public SimulatedPlacements withNodes(List newNodes) -// { -// return new SimulatedPlacements(rf, newNodes, readPlacements, writePlacements, stashedStates); -// } -// -// public SimulatedPlacements withReadPlacements(NavigableMap> newReadPlacements) -// { -// return new SimulatedPlacements(rf, nodes, newReadPlacements, writePlacements, stashedStates); -// } -// -// public SimulatedPlacements withWritePlacements(NavigableMap> newWritePlacements) -// { -// return new SimulatedPlacements(rf, nodes, readPlacements, newWritePlacements, stashedStates); -// } -// -// public SimulatedPlacements withStashed(Transformations steps) -// { -// List newStashed = new ArrayList<>(); -// newStashed.addAll(stashedStates); -// newStashed.add(steps); -// return new SimulatedPlacements(rf, nodes, readPlacements, writePlacements, newStashed); -// } -// -// private SimulatedPlacements withoutStashed(Transformations finished) -// { -// List newStates = new ArrayList<>(); -// for (Transformations s : stashedStates) -// if (s != finished) -// newStates.add(s); -// return new SimulatedPlacements(rf, nodes, readPlacements, writePlacements, newStates); -// } -// -// public boolean isWriteTargetFor(long token, Predicate predicate) -// { -// return writePlacementsFor(token).stream().anyMatch(predicate); -// } -// -// public boolean isReadReplicaFor(long token, Predicate predicate) -// { -// return readReplicasFor(token).stream().anyMatch(predicate); -// } -// -// public boolean isReadReplicaFor(long minToken, long maxToken, Predicate predicate) -// { -// return readReplicasFor(minToken, maxToken).stream().anyMatch(predicate); -// } -// -// public List writePlacementsFor(long token) -// { -// for (Map.Entry> e : writePlacements.entrySet()) -// { -// if (e.getKey().contains(token)) -// return e.getValue(); -// } -// -// throw new AssertionError(); -// } -// -// public List readReplicasFor(long minToken, long maxToken) -// { -// for (Map.Entry> e : readPlacements.entrySet()) -// { -// if (e.getKey().contains(minToken, maxToken)) -// return e.getValue(); -// } -// -// throw new AssertionError(); -// } -// -// -// public List readReplicasFor(long token) -// { -// for (Map.Entry> e : readPlacements.entrySet()) -// { -// if (e.getKey().contains(token)) -// return e.getValue(); -// } -// -// throw new AssertionError(); -// } -// -// public String toString() -// { -// return "ModelState{" + -// "\nrf=" + rf + -// "\nnodes=" + nodes + -// "\nreadPlacements=\n" + placementsToString(readPlacements) + -// "\nwritePlacements=\n" + placementsToString(writePlacements) + -// '}'; -// } -// } -// -// public interface SimulatedPlacementHolder -// { -// SimulatedPlacements get(); -// -// /** -// * Applies _one_ of the transformations given to the current state, returning the resulting state. -// * _Does_ set the state within the holder as well. -// */ -// SimulatedPlacements applyNext(Transformations fn); -// SimulatedPlacementHolder set(SimulatedPlacements placements); -// SimulatedPlacementHolder fork(); -// } -// -// public static class RefSimulatedPlacementHolder implements SimulatedPlacementHolder -// { -// private SimulatedPlacements state; -// -// public RefSimulatedPlacementHolder(SimulatedPlacements state) -// { -// this.state = state; -// } -// -// public SimulatedPlacements get() -// { -// return state; -// } -// -// public SimulatedPlacements applyNext(Transformations fn) -// { -// return state = fn.advance(state); -// } -// -// public SimulatedPlacementHolder set(SimulatedPlacements newState) -// { -// state = newState; -// return this; -// } -// -// public SimulatedPlacementHolder fork() -// { -// return new RefSimulatedPlacementHolder(state); -// } -// } -// -// public static class Transformation -// { -// private Function apply; -// private Function revert; -// -// Transformation(Function apply, -// Function revert) -// { -// this.apply = apply; -// this.revert = revert; -// } -// -// public Transformation prepare(Function apply, -// Function revert) -// { -// this.apply = apply; -// this.revert = revert; -// return this; -// } -// } -// -// public static class Transformations -// { -// private final List steps = new ArrayList<>(); -// private int idx = 0; -// -// public void add(Transformation step) -// { -// steps.add(step); -// } -// -// public boolean hasNext() -// { -// return idx < steps.size(); -// } -// -// public SimulatedPlacements advance(SimulatedPlacements prev) -// { -// if (idx >= steps.size()) -// throw new IllegalStateException("Cannot advance transformations, no more steps remaining"); -// -// SimulatedPlacements next = steps.get(idx++).apply.apply(prev); -// if (!hasNext()) -// next = next.withoutStashed(this); -// -// return next; -// } -// -// public boolean hasPrevious() -// { -// return idx > 0; -// } -// -// public SimulatedPlacements revertPublishedEffects(SimulatedPlacements state) -// { -// while (hasPrevious()) -// state = steps.get(--idx).revert.apply(state); -// -// return state.withoutStashed(this); -// } -// } -// -// public static SimulatedPlacements joinFully(SimulatedPlacements baseState, Node node) -// { -// Transformations transformations = join(baseState, node); -// baseState = baseState.withStashed(transformations); -// -// while (transformations.hasNext()) -// baseState = transformations.advance(baseState); -// -// return baseState; -// } -// -// /** -// * Diff-based bootstrap (very close implementation-wise to what production code does) -// */ -// public static Transformations join(SimulatedPlacements baseState, Node bootstrappingNode) -// { -// long token = bootstrappingNode.token(); -// List splitNodes = split(baseState.nodes, token); -// Map> maximalStateWithPlacement = baseState.rf.replicate(move(splitNodes, token, bootstrappingNode)).placementsForRange; -// -// NavigableMap> splitReadPlacements = baseState.rf.replicate(splitNodes).placementsForRange; -// NavigableMap> splitWritePlacements = baseState.rf.replicate(splitNodes).placementsForRange; -// -// Map> allWriteCommands = diff(splitWritePlacements, maximalStateWithPlacement); -// Map> step1WriteCommands = map(allWriteCommands, Diff::onlyAdditions); -// Map> step3WriteCommands = map(allWriteCommands, Diff::onlyRemovals); -// Map> readCommands = diff(splitReadPlacements, maximalStateWithPlacement); -// -// Transformations steps = new Transformations(); -// -// steps.add(new Transformation( -// (model) -> { // apply -// // add the new node to the system and split ranges according to its token, while retaining current -// // placement. This step will always be executed immediately, whereas subsequent steps may be deferred -// debug.log("Splitting ranges to prepare for join of " + bootstrappingNode + "\n"); -// return model.withReadPlacements(splitReplicated(baseState.readPlacements, token)) -// .withWritePlacements(splitReplicated(baseState.writePlacements, token)); -// }, -// (model) -> { // revert -// // final stage of reverting a join is to undo the range splits performed by preparing the operation -// debug.log("Reverting range splits from prepare-join of " + bootstrappingNode + "\n"); -// return model.withWritePlacements(mergeReplicated(model.writePlacements, token)) -// .withReadPlacements(mergeReplicated(model.readPlacements, token)); -// }) -// ); -// -// // Step 1: add new node as a write replica to all ranges it is gaining -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log("Executing start-join of " + bootstrappingNode + "\n"); -// debug.log(String.format("Commands for step 1 of bootstrap of %s.\n" + -// "\twriteModifications=\n%s", -// bootstrappingNode, diffsToString(step1WriteCommands))); -// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); -// }, -// (model) -> { // revert -// debug.log("Reverting start-join of " + bootstrappingNode + "\n"); -// Map> inverted = map(step1WriteCommands, Diff::invert); -// debug.log("Commands for reverting step 1 of bootstrap of %s.\n" + -// "\twriteModifications=\n%s", -// bootstrappingNode, diffsToString(inverted)); -// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); -// }) -// ); -// -// // Step 2: add new node as a read replica to the ranges it is gaining; remove old node from reads at the same time -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log("Executing mid-join of " + bootstrappingNode + "\n"); -// debug.log(String.format("Commands for step 2 of bootstrap of %s.\n" + -// "\treadCommands=\n%s", -// bootstrappingNode, diffsToString(readCommands))); -// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); -// }, -// (model) -> { // revert -// debug.log("Reverting mid-join of " + bootstrappingNode + "\n"); -// Map> inverted = map(readCommands, Diff::invert); -// debug.log(String.format("Commands for reverting step 2 of bootstrap of %s.\n" + -// "\treadCommands=\n%s", -// bootstrappingNode, diffsToString(inverted))); -// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); -// }) -// ); -// -// -// // Step 3: finally remove the old node from writes -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log("Executing finish-join of " + bootstrappingNode + "\n"); -// debug.log(String.format("Commands for step 3 of bootstrap of %s.\n" + -// "\twriteModifications=\n%s", -// bootstrappingNode, -// diffsToString(step3WriteCommands))); -// List newNodes = new ArrayList<>(model.nodes); -// newNodes.add(bootstrappingNode); -// newNodes.sort(Node::compareTo); -// return model.withNodes(newNodes) -// .withWritePlacements(PlacementSimulator.apply(model.writePlacements, step3WriteCommands)); -// }, -// (model) -> { //revert -// throw new IllegalStateException("Can't revert finish-join of " + bootstrappingNode + ", operation is already complete\n"); -// }) -// ); -// -// debug.log("Planned bootstrap of " + bootstrappingNode + "\n"); -// return steps; -// } -// -// public static Transformations move(SimulatedPlacements baseState, Node movingNode, long newToken) -// { -// List origNodes = new ArrayList<>(baseState.nodes); -// List finalNodes = new ArrayList<>(); -// for (int i = 0; i < origNodes.size(); i++) -// { -// if (origNodes.get(i).idx() == movingNode.idx()) -// continue; -// finalNodes.add(origNodes.get(i)); -// } -// finalNodes.add(movingNode.overrideToken(newToken)); -// finalNodes.sort(Node::compareTo); -// -// Map> start = splitReplicated(baseState.rf.replicate(origNodes).placementsForRange, newToken); -// Map> end = splitReplicated(baseState.rf.replicate(finalNodes).placementsForRange, movingNode.token()); -// -// Map> fromStartToEnd = diff(start, end); -// -// Transformations steps = new Transformations(); -// -// // Step 1: Prepare Move, -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log(String.format("Splitting ranges to prepare for move of %s to %d\n", movingNode, newToken)); -// return model.withReadPlacements(splitReplicated(model.readPlacements, newToken)) -// .withWritePlacements(splitReplicated(model.writePlacements, newToken)); -// }, -// (model) -> { // revert -// debug.log(String.format("Reverting range splits from prepare move of %s to %d\n", movingNode, newToken)); -// return model.withWritePlacements(mergeReplicated(model.writePlacements, newToken)) -// .withReadPlacements(mergeReplicated(model.readPlacements, newToken)); -// }) -// ); -// -// // Step 2: Start Move, add all potential owners to write quorums -// steps.add(new Transformation( -// (model) -> { // apply -// Map> diff = map(fromStartToEnd, Diff::onlyAdditions); -// debug.log("Executing start-move of " + movingNode + "\n"); -// debug.log(String.format("Commands for step 1 of move of %s to %d.\n" + -// "\twriteModifications=\n%s", -// movingNode, newToken, diffsToString(diff))); -// -// NavigableMap> placements = model.writePlacements; -// placements = PlacementSimulator.apply(placements, diff); -// return model.withWritePlacements(placements); -// }, -// (model) -> { // revert -// debug.log("Reverting start-move of " + movingNode + "\n"); -// Map> diff = map(fromStartToEnd, Diff::onlyAdditions); -// Map> inverted = map(diff, Diff::invert); -// debug.log(String.format("Commands for reverting step 1 of move of %s to %d.\n" + -// "\twriteModifications=\n%s", -// movingNode, newToken, diffsToString(inverted))); -// -// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); -// } -// )); -// // Step 3: Mid Move, remove all nodes that are losing ranges from read quorums, add all nodes gaining ranges to read quorums -// steps.add(new Transformation( -// (model) -> { -// debug.log("Executing mid-move of " + movingNode + "\n"); -// debug.log(String.format("Commands for step 2 of move of %s to %d.\n" + -// "\treadModifications=\n%s", -// movingNode, newToken, diffsToString(fromStartToEnd))); -// -// NavigableMap> placements = model.readPlacements; -// placements = PlacementSimulator.apply(placements, fromStartToEnd); -// return model.withReadPlacements(placements); -// }, -// (model) -> { -// NavigableMap> placements = PlacementSimulator.apply(model.readPlacements, map(fromStartToEnd, Diff::invert)); -// return model.withReadPlacements(placements); -// }) -// ); -// -// // Step 4: Finish Move, remove all nodes that are losing ranges from write quorums -// steps.add(new Transformation( -// (model) -> { -// Map> diff = map(fromStartToEnd, Diff::onlyRemovals); -// -// debug.log("Executing finish-move of " + movingNode + "\n"); -// debug.log(String.format("Commands for step 2 of move of %s to %d.\n" + -// "\twriteModifications=\n%s", -// movingNode, newToken, diffsToString(diff))); -// -// List currentNodes = new ArrayList<>(model.nodes); -// List newNodes = new ArrayList<>(); -// for (int i = 0; i < currentNodes.size(); i++) -// { -// if (currentNodes.get(i).idx() == movingNode.idx()) -// continue; -// newNodes.add(currentNodes.get(i)); -// } -// newNodes.add(movingNode.overrideToken(newToken)); -// newNodes.sort(Node::compareTo); -// -// Map> writePlacements = model.writePlacements; -// writePlacements = PlacementSimulator.apply(writePlacements, diff); -// -// return model.withWritePlacements(mergeReplicated(writePlacements, movingNode.token())) -// .withReadPlacements(mergeReplicated(model.readPlacements, movingNode.token())) -// .withNodes(newNodes); -// }, -// (model) -> { -// throw new IllegalStateException(String.format("Can't revert finish-move of %d, operation is already complete", newToken)); -// }) -// ); -// -// return steps; -// } -// -// public static Transformations leave(SimulatedPlacements baseState, Node toRemove) -// { -// // calculate current placements - this is start state -// Map> start = baseState.rf.replicate(baseState.nodes).placementsForRange; -// -// List afterLeaveNodes = new ArrayList<>(baseState.nodes); -// afterLeaveNodes.remove(toRemove); -// // calculate placements based on existing ranges but final set of nodes - this is end state -// Map> end = baseState.rf.replicate(toRanges(baseState.nodes), afterLeaveNodes).placementsForRange; -// // maximal state is union of start & end -// -// Map> allWriteCommands = diff(start, end); -// Map> step1WriteCommands = map(allWriteCommands, Diff::onlyAdditions); -// Map> step3WriteCommands = map(allWriteCommands, Diff::onlyRemovals); -// Map> readCommands = diff(start, end); -// Transformations steps = new Transformations(); -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log("Executing start-leave of " + toRemove + "\n"); -// debug.log(String.format("Commands for step 1 of decommission of %s.\n" + -// "\twriteModifications=\n%s", -// toRemove, diffsToString(step1WriteCommands))); -// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); -// }, -// (model) -> { // revert -// debug.log("Reverting start-leave of " + toRemove + "\n"); -// Map> inverted = map(step1WriteCommands, Diff::invert); -// debug.log(String.format("Commands for reverting step 1 of decommission of %s.\n" + -// "\twriteModifications=\n%s", -// toRemove, diffsToString(inverted))); -// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); -// }) -// ); -// -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log("Executing mid-leave of " + toRemove + "\n"); -// debug.log(String.format("Commands for step 2 of decommission of %s.\n" + -// "\treadModifications=\n%s", -// toRemove, -// diffsToString(readCommands))); -// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); -// }, -// (model) -> { // revert -// debug.log("Reverting mid-leave of " + toRemove + "\n"); -// Map> inverted = map(readCommands, Diff::invert); -// debug.log(String.format("Commands for reverting step 2 of decommission of %s.\n" + -// "\treadModifications=\n%s", -// toRemove, -// diffsToString(inverted))); -// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); -// }) -// ); -// -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log("Executing finish-leave decommission of " + toRemove + "\n"); -// debug.log(String.format("Commands for step 3 of decommission of %s.\n" + -// "\twriteModifications=\n%s", -// toRemove, -// diffsToString(step3WriteCommands))); -// List newNodes = new ArrayList<>(model.nodes); -// newNodes.remove(toRemove); -// newNodes.sort(Node::compareTo); -// Map> writes = PlacementSimulator.apply(model.writePlacements, step3WriteCommands); -// return model.withReadPlacements(mergeReplicated(model.readPlacements, toRemove.token())) -// .withWritePlacements(mergeReplicated(writes, toRemove.token())) -// .withNodes(newNodes); -// }, -// (model) -> { // revert -// throw new IllegalStateException("Can't revert finish-leave of " + toRemove + ", operation is already complete\n"); -// })); -// -// debug.log("Planned decommission of " + toRemove + "\n"); -// return steps; -// } -// -// public static Transformations replace(SimulatedPlacements baseState, Node toReplace, Node replacement) -// { -// Map> start = baseState.rf.replicate(baseState.nodes).placementsForRange; -// Map> allCommands = new TreeMap<>(); -// start.forEach((range, nodes) -> { -// if (nodes.contains(toReplace)) -// { -// allCommands.put(range, new Diff<>(Collections.singletonList(replacement), -// Collections.singletonList(toReplace))); -// } -// }); -// Map> step1WriteCommands = map(allCommands, Diff::onlyAdditions); -// Map> step3WriteCommands = map(allCommands, Diff::onlyRemovals); -// Map> readCommands = allCommands; -// Transformations steps = new Transformations(); -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log(String.format("Executing start-replace of %s for %s\n", replacement, toReplace)); -// debug.log(String.format("Commands for step 1 of bootstrap of %s for replacement of %s.\n" + -// "\twriteModifications=\n%s", -// replacement, toReplace, diffsToString(step1WriteCommands))); -// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); -// }, -// (model) -> { // revert -// debug.log(String.format("Reverting start-replace of %s for %s\n", replacement, toReplace)); -// Map> inverted = map(step1WriteCommands, Diff::invert); -// debug.log(String.format("Commands for reverting step 1 of bootstrap of %s for replacement of %s.\n" + -// "\twriteModifications=\n%s", -// replacement, toReplace, diffsToString(inverted))); -// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); -// }) -// ); -// -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log(String.format("Executing mid-replace of %s for %s\n", replacement, toReplace)); -// debug.log(String.format("Commands for step 2 of bootstrap of %s for replacement of %s.\n" + -// "\treadModifications=\n%s", -// replacement, toReplace, -// diffsToString(readCommands))); -// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); -// }, -// (model) -> { // revert -// debug.log(String.format("Reverting mid-replace of %s for %s\n", replacement, toReplace)); -// Map> inverted = map(readCommands, Diff::invert); -// debug.log(String.format("Commands for reverting step 2 of bootstrap of %s for replacement of %s.\n" + -// "\treadModifications=\n%s", -// replacement, toReplace, -// diffsToString(inverted))); -// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); -// }) -// ); -// -// steps.add(new Transformation( -// (model) -> { // apply -// debug.log(String.format("Executing finish-replace of %s for %s\n", replacement, toReplace)); -// debug.log(String.format("Commands for step 3 of bootstrap of %sfor replacement of %s.\n" + -// "\twriteModifications=\n%s", -// replacement, toReplace, -// diffsToString(step3WriteCommands))); -// List newNodes = new ArrayList<>(model.nodes); -// newNodes.remove(toReplace); -// newNodes.add(replacement); -// newNodes.sort(Node::compareTo); -// return model.withNodes(newNodes) -// .withWritePlacements(PlacementSimulator.apply(model.writePlacements, step3WriteCommands)); -// }, -// (model) -> { // revert -// throw new IllegalStateException(String.format("Can't revert finish-replace of %s for %s, operation is already complete\n", replacement, toReplace)); -// }) -// ); -// -// debug.log(String.format("Planned bootstrap of %s for replacement of %s\n", replacement, toReplace)); -// return steps; -// } -// -// public static void assertPlacements(SimulatedPlacements placements, Map> r, Map> w) -// { -// assertRanges(r, placements.readPlacements); -// assertRanges(w, placements.writePlacements); -// } -// -// public static void assertRanges(Map> expected, Map> actual) -// { -// Assert.assertEquals(expected.keySet(), actual.keySet()); -// expected.forEach((k, v) -> { -// // When comparing replica sets, we only care about the endpoint (i.e. the node.id). For the purpose -// // of simulation, during split operations we duplicate the node holding the range being split as if giving -// // it two tokens, the original one and the split point. e.g. With N1@100, N2@200 then splitting at 150, -// // we will end up with (100, 150] -> N2@150 and (150, 200] -> N2@200. As this is purely an artefact of the -// // bootstrap_diffBased implementation and the real code doesn't do this, only the endpoint matters for -// // correctness, so we limit this comparison to endpoints only. -// Assert.assertEquals(String.format("For key: %s\n", k), -// expected.get(k).stream().map(n -> n.idx()).sorted().collect(Collectors.toList()), -// actual.get(k).stream().map(n -> n.idx()).sorted().collect(Collectors.toList())); -// }); -// } -// -// public static boolean containsAll(Set a, Set b) -// { -// if (a.isEmpty() && !b.isEmpty()) -// return false; // empty set does not contain all entries of a non-empty one -// for (T v : b) -// if (!a.contains(v)) -// return false; -// -// return true; -// } -// -// /** -// * Applies a given diff to the placement map -// */ -// public static NavigableMap> apply(Map> orig, Map> diff) -// { -// assert containsAll(orig.keySet(), diff.keySet()) : String.format("Can't apply diff to a map with different sets of keys:" + -// "\nOrig ks: %s" + -// "\nDiff ks: %s" + -// "\nDiff: %s", -// orig.keySet(), diff.keySet(), diff); -// NavigableMap> res = new TreeMap<>(); -// for (Map.Entry> entry : orig.entrySet()) -// { -// Range range = entry.getKey(); -// if (diff.containsKey(range)) -// res.put(range, apply(entry.getValue(), diff.get(range))); -// else -// res.put(range, entry.getValue()); -// } -// return Collections.unmodifiableNavigableMap(res); -// } -// -// /** -// * Apply diff to a list of nodes -// */ -// public static List apply(List nodes, Diff diff) -// { -// Set tmp = new HashSet<>(nodes); -// tmp.addAll(diff.additions); -// for (Node removal : diff.removals) -// tmp.remove(removal); -// List newNodes = new ArrayList<>(tmp); -// newNodes.sort(Node::compareTo); -// return Collections.unmodifiableList(newNodes); -// } -// -// /** -// * Diff two placement maps -// */ -// public static Map> diff(Map> l, Map> r) -// { -// assert l.keySet().equals(r.keySet()) : String.format("Can't diff events from different bases %s %s", l.keySet(), r.keySet()); -// Map> diff = new TreeMap<>(); -// for (Map.Entry> entry : l.entrySet()) -// { -// Range range = entry.getKey(); -// Diff d = diff(entry.getValue(), r.get(range)); -// if (!d.removals.isEmpty() || !d.additions.isEmpty()) -// diff.put(range, d); -// } -// return Collections.unmodifiableMap(diff); -// } -// -// public static Map map(Map diff, Function fn) -// { -// Map newDiff = new TreeMap<>(); -// for (Map.Entry entry : diff.entrySet()) -// { -// T newV = fn.apply(entry.getValue()); -// if (newV != null) -// newDiff.put(entry.getKey(), newV); -// } -// return Collections.unmodifiableMap(newDiff); -// } -// -// public static List map(List coll, Function map) -// { -// List newColl = new ArrayList<>(coll); -// for (T v : coll) -// newColl.add(map.apply(v)); -// return newColl; -// } -// -// /** -// * Produce a diff (i.e. set of additions/subtractions that should be applied to the list of nodes in order to produce -// * r from l) -// */ -// public static Diff diff(List l, List r) -// { -// // additions things present in r but not in l -// List additions = new ArrayList<>(); -// // removals are things present in l but not r -// List removals = new ArrayList<>(); -// -// for (Node i : r) -// { -// boolean isPresentInL = false; -// for (Node j : l) -// { -// if (i.equals(j)) -// { -// isPresentInL = true; -// break; -// } -// } -// -// if (!isPresentInL) -// additions.add(i); -// } -// -// for (Node i : l) -// { -// boolean isPresentInR = false; -// for (Node j : r) -// { -// if (i.equals(j)) -// { -// isPresentInR = true; -// break; -// } -// } -// -// if (!isPresentInR) -// removals.add(i); -// } -// return new Diff<>(additions, removals); -// } -// -// public static Map> superset(Map> l, Map> r) -// { -// assert l.keySet().equals(r.keySet()) : String.format("%s != %s", l.keySet(), r.keySet()); -// -// Map> newState = new TreeMap<>(); -// for (Map.Entry> entry : l.entrySet()) -// { -// Range range = entry.getKey(); -// Set nodes = new HashSet<>(); -// nodes.addAll(entry.getValue()); -// nodes.addAll(r.get(range)); -// newState.put(range, new ArrayList<>(nodes)); -// } -// -// return newState; -// } -// -// public static NavigableMap> mergeReplicated(Map> orig, long removingToken) -// { -// NavigableMap> newState = new TreeMap<>(); -// Iterator>> iter = orig.entrySet().iterator(); -// while (iter.hasNext()) -// { -// Map.Entry> current = iter.next(); -// if (current.getKey().end == removingToken) -// { -// assert iter.hasNext() : "Cannot merge range, no more ranges in list"; -// Map.Entry> next = iter.next(); -// assert current.getValue().containsAll(next.getValue()) && current.getValue().size() == next.getValue().size() -// : "Cannot merge ranges with different replica groups"; -// Range merged = new Range(current.getKey().start, next.getKey().end); -// newState.put(merged, current.getValue()); -// } -// else -// { -// newState.put(current.getKey(), current.getValue()); -// } -// } -// -// return newState; -// } -// -// public static NavigableMap> splitReplicated(Map> orig, long splitAt) -// { -// NavigableMap> newState = new TreeMap<>(); -// for (Map.Entry> entry : orig.entrySet()) -// { -// Range range = entry.getKey(); -// if (range.contains(splitAt)) -// { -// newState.put(new Range(range.start, splitAt), entry.getValue()); -// newState.put(new Range(splitAt, range.end), entry.getValue()); -// } -// else -// { -// newState.put(range, entry.getValue()); -// } -// } -// return newState; -// } -// -// /** -// * "Split" the list of nodes at splitAt, without changing ownership -// */ -// public static List split(List nodes, long splitAt) -// { -// List newNodes = new ArrayList<>(); -// boolean inserted = false; -// Node previous = null; -// for (int i = nodes.size() - 1; i >= 0; i--) -// { -// Node node = nodes.get(i); -// if (!inserted && splitAt > node.token()) -// { -// // We're trying to split rightmost range -// if (previous == null) -// { -// newNodes.add(nodes.get(0).overrideToken(splitAt)); -// } -// else -// { -// newNodes.add(previous.overrideToken(splitAt)); -// } -// inserted = true; -// } -// -// newNodes.add(node); -// previous = node; -// } -// -// // Leftmost is split -// if (!inserted) -// newNodes.add(previous.overrideToken(splitAt)); -// -// newNodes.sort(Node::compareTo); -// return Collections.unmodifiableList(newNodes); -// } -// -// /** -// * Change the ownership of the freshly split token -// */ -// public static List move(List nodes, long tokenToMove, Node newOwner) -// { -// List newNodes = new ArrayList<>(); -// for (Node node : nodes) -// { -// if (node.token() == tokenToMove) -// newNodes.add(newOwner.overrideToken(tokenToMove)); -// else -// newNodes.add(node); -// } -// newNodes.sort(Node::compareTo); -// return Collections.unmodifiableList(newNodes); -// } -// -// public static List filter(List nodes, Predicate pred) -// { -// List newNodes = new ArrayList<>(); -// for (Node node : nodes) -// { -// if (pred.test(node)) -// newNodes.add(node); -// } -// newNodes.sort(Node::compareTo); -// return Collections.unmodifiableList(newNodes); -// } -// -// private static , T1, T2> Map mapValues(Map allDCs, Function map) -// { -// NavigableMap res = new TreeMap<>(); -// for (Map.Entry e : allDCs.entrySet()) -// { -// res.put(e.getKey(), map.apply(e.getValue())); -// } -// return res; -// } -// -// public static Map> nodesByDC(List nodes) -// { -// Map> nodesByDC = new HashMap<>(); -// for (Node node : nodes) -// nodesByDC.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node); -// -// return nodesByDC; -// } -// -// public static Map> racksByDC(List nodes) -// { -// Map> racksByDC = new HashMap<>(); -// for (Node node : nodes) -// racksByDC.computeIfAbsent(node.dc(), (k) -> new HashSet<>()).add(node.rack()); -// -// return racksByDC; -// } -// -// public static class Diff { -// public final List additions; -// public final List removals; -// -// public Diff(List additions, List removals) -// { -// this.additions = additions; -// this.removals = removals; -// } -// -// public String toString() -// { -// return "Diff{" + -// "additions=" + additions + -// ", removals=" + removals + -// '}'; -// } -// -// public Diff onlyAdditions() -// { -// if (additions.isEmpty()) return null; -// return new Diff<>(additions, Collections.emptyList()); -// } -// -// public Diff onlyRemovals() -// { -// if (removals.isEmpty()) return null; -// return new Diff<>(Collections.emptyList(), removals); -// } -// -// public Diff invert() -// { -// // invert removals & additions -// return new Diff<>(removals, additions); -// } -// } -// -// -// public static String diffsToString(Map> placements) -// { -// StringBuilder builder = new StringBuilder(); -// for (Map.Entry> e : placements.entrySet()) -// { -// builder.append("\t\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n"); -// } -// return builder.toString(); -// } -// -// public static String placementsToString(Map> placements) -// { -// StringBuilder builder = new StringBuilder(); -// for (Map.Entry> e : placements.entrySet()) -// { -// builder.append("\t\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n"); -// } -// return builder.toString(); -// } -// -// public static class DebugLog -// { -// private final BufferedWriter operationLog; -// public DebugLog() -// { -// File f = new File("simulated.log"); -// try -// { -// operationLog = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); -// } -// catch (FileNotFoundException e) -// { -// throw new RuntimeException(e); -// } -// } -// -// public void log(long seq, Object t) -// { -// log("%d: %s\n", seq, t); -// } -// -// private void log(String format, Object... objects) -// { -// try -// { -// operationLog.write(String.format(format, objects)); -// operationLog.flush(); -// } -// catch (IOException e) -// { -// // ignore -// } -// } -// } -// -//} diff --git a/test/harry/main/org/apache/cassandra/harry/op/Operations.java b/test/harry/main/org/apache/cassandra/harry/op/Operations.java new file mode 100644 index 0000000000..47ffa93c1c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/op/Operations.java @@ -0,0 +1,562 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.op; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.util.BitSet; + +public class Operations +{ + public static class WriteOp extends PartitionOperation + { + private final long cd; + private final long vds[]; + private final long sds[]; + + public WriteOp(long lts, long pd, long cd, long[] vds, long[] sds, Kind kind) + { + super(lts, pd, kind); + this.cd = cd; + this.vds = vds; + this.sds = sds; + } + + public long cd() + { + return this.cd; + } + + public long[] vds() + { + return vds; + } + + public long[] sds() + { + return sds; + } + } + + public static class DeleteRow extends PartitionOperation + { + protected final long cd; + + public DeleteRow(long lts, long pd, long cd) + { + super(lts, pd, Kind.DELETE_ROW); + this.cd = cd; + } + + public long cd() + { + return cd; + } + } + + public static class DeleteColumns extends PartitionOperation + { + private final long cd; + private final BitSet regularColumns; + private final BitSet staticColumns; + + public DeleteColumns(long lts, long pd, long cd, BitSet regularColumns, BitSet staticColumns) + { + super(lts, pd, Kind.DELETE_COLUMNS); + this.cd = cd; + this.regularColumns = regularColumns; + this.staticColumns = staticColumns; + } + + public long cd() + { + return cd; + } + + public BitSet regularColumns() + { + return regularColumns; + } + + public BitSet staticColumns() + { + return staticColumns; + } + } + + + public static class DeleteRange extends PartitionOperation + { + final long lowerBound; + final long upperBound; + + final Relations.RelationKind[] lowBoundRelation; + final Relations.RelationKind[] highBoundRelation; + + public DeleteRange(long lts, long pd, + long lowerCdBound, + long upperCdBound, + Relations.RelationKind[] lowBoundRelation, + Relations.RelationKind[] highBoundRelation) + { + super(lts, pd, Kind.DELETE_RANGE); + this.lowerBound = lowerCdBound; + this.upperBound = upperCdBound; + this.lowBoundRelation = lowBoundRelation; + this.highBoundRelation = highBoundRelation; + } + + public Relations.RelationKind[] lowerBoundRelation() + { + return lowBoundRelation; + } + + public Relations.RelationKind[] upperBoundRelation() + { + return highBoundRelation; + } + + public long lowerBound() + { + return lowerBound; + } + + public long upperBound() + { + return upperBound; + } + } + + public static abstract class SelectStatement extends PartitionOperation + { + public SelectStatement(long lts, long pd, Kind kind) + { + super(lts, pd, kind); + } + + public abstract ClusteringOrderBy orderBy(); + public abstract BitSet selection(); + } + + public static class SelectRange extends SelectStatement + { + final long lowerBound; + final long upperBound; + + final Relations.RelationKind[] lowerBoundRelation; + final Relations.RelationKind[] upperBoundRelation; + + public SelectRange(long lts, long pd, + long lowerBound, + long highBound, + Relations.RelationKind[] lowerBoundRelation, + Relations.RelationKind[] upperBoundRelation) + { + super(lts, pd, Kind.SELECT_RANGE); + this.lowerBound = lowerBound; + this.upperBound = highBound; + this.lowerBoundRelation = lowerBoundRelation; + this.upperBoundRelation = upperBoundRelation; + } + + @Override + public ClusteringOrderBy orderBy() + { + return ClusteringOrderBy.ASC; + } + + @Override + public BitSet selection() + { + return MagicConstants.ALL_COLUMNS; + } + + public Relations.RelationKind[] lowerBoundRelation() + { + return lowerBoundRelation; + } + + public Relations.RelationKind[] upperBoundRelation() + { + return upperBoundRelation; + } + + public long lowerBound() + { + return lowerBound; + } + + public long upperBound() + { + return upperBound; + } + } + + public static class SelectCustom extends SelectStatement + { + private final Relations.Relation[] ckRelations; + private final Relations.Relation[] regularRelations; + private final Relations.Relation[] staticRelations; + + public SelectCustom(long lts, long pd, + Relations.Relation[] ckRelations, + Relations.Relation[] regularRelations, + Relations.Relation[] staticRelations) + { + super(lts, pd, Kind.SELECT_CUSTOM); + this.ckRelations = ckRelations; + this.regularRelations = regularRelations; + this.staticRelations = staticRelations; + } + + @Override + public ClusteringOrderBy orderBy() + { + return ClusteringOrderBy.ASC; + } + + @Override + public BitSet selection() + { + return MagicConstants.ALL_COLUMNS; + } + + public Relations.Relation[] ckRelations() + { + return ckRelations; + } + + public Relations.Relation[] staticRelations() + { + return staticRelations; + } + + public Relations.Relation[] regularRelations() + { + return regularRelations; + } + } + + public static class SelectRow extends SelectStatement + { + private final long cd; + public SelectRow(long lts, long pd, long cd) + { + super(lts, pd, Kind.SELECT_ROW); + this.cd = cd; + } + + public long cd() + { + return cd; + } + + public ClusteringOrderBy orderBy() + { + return ClusteringOrderBy.ASC; + } + + @Override + public BitSet selection() + { + return MagicConstants.ALL_COLUMNS; + } + } + + public static class SelectPartition extends SelectStatement + { + private final ClusteringOrderBy orderBy; + + public SelectPartition(long lts, long pd) + { + this(lts, pd, ClusteringOrderBy.ASC); + } + + public SelectPartition(long lts, long pd, ClusteringOrderBy orderBy) + { + super(lts, pd, Kind.SELECT_PARTITION); + this.orderBy = orderBy; + } + + public SelectPartition orderBy(ClusteringOrderBy orderBy) + { + return new SelectPartition(lts, pd, orderBy); + } + + public ClusteringOrderBy orderBy() + { + return orderBy; + } + + @Override + public BitSet selection() + { + return MagicConstants.ALL_COLUMNS; + } + } + + public static class DeletePartition extends PartitionOperation + { + public DeletePartition(long lts, long pd) + { + super(lts, pd, Kind.DELETE_PARTITION); + } + } + + public interface DeleteColumnsOp extends Operation + { + long cd(); + BitSet regularColumns(); + BitSet staticColumns(); + } + + public interface Operation + { + long lts(); + Kind kind(); + } + + public static abstract class PartitionOperation implements Operation + { + public final long lts; + public final long pd; + public final Kind kind; + + public PartitionOperation(long lts, long pd, Kind kind) + { + this.pd = pd; + this.lts = lts; + this.kind = kind; + } + + public final long pd() + { + return pd; + } + + @Override + public final long lts() + { + return lts; + } + + @Override + public final Kind kind() + { + return kind; + } + + public String toString() + { + return "Operation{" + + " lts=" + lts + + " pd=" + pd + + ", kind=" + kind + + '}'; + } + } + + public enum Kind + { + /** + * Custom operation such as flush + */ + CUSTOM(false), + + UPDATE(true), + INSERT(true), + + DELETE_PARTITION(true), + DELETE_ROW(false), + DELETE_COLUMNS(true), + DELETE_RANGE(false), + + SELECT_PARTITION(true), + SELECT_ROW(false), + SELECT_RANGE(true), + SELECT_CUSTOM(true); + public final boolean partititonLevel; + + Kind(boolean partitionLevel) + { + this.partititonLevel = partitionLevel; + } + + } + + public static class CustomRunnableOperation implements Operation + { + public final long lts; + public final long opId; + private final Runnable run; + + public CustomRunnableOperation(long lts, long opId, Runnable runnable) + { + assert opId >= 0; + this.lts = lts; + this.opId = opId; + this.run = runnable; + } + + public void execute() + { + run.run(); + } + + @Override + public long lts() + { + return lts; + } + + @Override + public Kind kind() + { + return Kind.CUSTOM; + } + } + + /** + * ClusteringOrder by should be understood in terms of how we're going to iterate through this partition + * (in other words, if first clustering component order is DESC, we'll iterate in ASC order) + */ + public enum ClusteringOrderBy + { + ASC, DESC + } + + public interface Selection + { + // TODO: allow expressions here + Collection> columns(); + boolean includeTimestamps(); + boolean isWildcard(); + + boolean selects(ColumnSpec column); + boolean selectsAllOf(List> subSelection); + int indexOf(ColumnSpec column); + + static Selection fromBitSet(BitSet bitSet, SchemaSpec schema) + { + if (bitSet == MagicConstants.ALL_COLUMNS) + { + Map, Integer> columns = new HashMap<>(); + for (int i = 0; i < schema.allColumnInSelectOrder.size(); i++) + columns.put(schema.allColumnInSelectOrder.get(i), i); + return new Wildcard(columns); + } + else + { + Invariants.checkState(schema.allColumnInSelectOrder.size() == bitSet.size()); + Map, Integer> columns = new HashMap<>(); + for (int i = 0; i < schema.allColumnInSelectOrder.size(); i++) + { + if (bitSet.isSet(i)) + columns.put(schema.allColumnInSelectOrder.get(i), i); + } + // TODO: timestamp + return new Columns(columns, false); + } + } + } + + public static class Wildcard extends Columns + { + private Wildcard(Map, Integer> columns) + { + super(columns, false); + } + + @Override + public Collection> columns() + { + return columns.keySet(); + } + + @Override + public boolean includeTimestamps() + { + return false; + } + + @Override + public boolean isWildcard() + { + return true; + } + } + + public static class Columns implements Selection + { + final Map, Integer> columns; + final boolean includeTimestamp; + + public Columns(Map, Integer> columns, boolean includeTimestamp) + { + this.columns = columns; + this.includeTimestamp = includeTimestamp; + } + + @Override + public Collection> columns() + { + return columns.keySet(); + } + + @Override + public boolean includeTimestamps() + { + return includeTimestamp; + } + + @Override + public boolean isWildcard() + { + return false; + } + + public boolean selects(ColumnSpec column) + { + return columns.containsKey(column); + } + + public boolean selectsAllOf(List> subSelection) + { + for (ColumnSpec column : subSelection) + { + if (!selects(column)) + return false; + } + return true; + } + + public int indexOf(ColumnSpec column) + { + return columns.get(column); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/op/Visit.java b/test/harry/main/org/apache/cassandra/harry/op/Visit.java new file mode 100644 index 0000000000..6c8c31390c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/op/Visit.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.op; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Assert; + +import org.apache.cassandra.harry.op.Operations.Operation; + +public class Visit +{ + public final long lts; + public final Operation[] operations; + public final Set visitedPartitions; + + public final boolean selectOnly; + public Visit(long lts, Operation[] operations) + { + Assert.assertTrue(operations.length > 0); + this.lts = lts; + this.operations = operations; + this.visitedPartitions = new HashSet<>(); + boolean selectOnly = true; + for (Operation operation : operations) + { + if (selectOnly && !(operation instanceof Operations.SelectStatement)) + selectOnly = false; + + if (operation instanceof Operations.PartitionOperation) + visitedPartitions.add(((Operations.PartitionOperation) operation).pd()); + + } + this.selectOnly = selectOnly; + } + + public String toString() + { + if (operations.length == 1) + return String.format("Visit %d: %s", lts, operations[0]); + + StringBuilder sb = new StringBuilder(); + sb.append("Visit ").append(lts).append(":\n"); + boolean first = true; + for (Operation operation : operations) + { + if (!first) + sb.append("\n"); + first = false; + sb.append(operation); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/operations/CompiledStatement.java b/test/harry/main/org/apache/cassandra/harry/operations/CompiledStatement.java deleted file mode 100644 index 3d1428f782..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/operations/CompiledStatement.java +++ /dev/null @@ -1,95 +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.harry.operations; - -public class CompiledStatement -{ - private final String cql; - private final Object[] bindings; - - public CompiledStatement(String cql, Object... bindings) - { - this.cql = cql; - this.bindings = bindings; - } - - public String cql() - { - return cql; - } - - public CompiledStatement withSchema(String oldKs, String oldTable, String newKs, String newTable) - { - return new CompiledStatement(cql.replace(oldKs + "." + oldTable, - newKs + "." + newTable), - bindings); - } - - public CompiledStatement withFiltering() - { - return new CompiledStatement(cql.replace(";", - " ALLOW FILTERING;"), - bindings); - } - - public Object[] bindings() - { - return bindings; - } - - public static CompiledStatement create(String cql, Object... bindings) - { - return new CompiledStatement(cql, bindings); - } - - public String toString() - { - return "CompiledStatement{" + - "cql='" + cql + '\'' + - ", bindings=" + bindingsToString(bindings) + - '}'; - } - - public static String bindingsToString(Object... bindings) - { - StringBuilder sb = new StringBuilder(); - boolean isFirst = true; - for (Object binding : bindings) - { - if (isFirst) - isFirst = false; - else - sb.append(","); - - if (binding instanceof String) - sb.append("\"").append(binding).append("\""); - else if (binding instanceof Short) - sb.append("(short)").append(binding); - else if (binding instanceof Byte) - sb.append("(byte)").append(binding); - else if (binding instanceof Float) - sb.append("(float)").append(binding); - else if (binding instanceof Long) - sb.append(binding).append("L"); - else - sb.append(binding); - } - return sb.toString(); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/DeleteHelper.java b/test/harry/main/org/apache/cassandra/harry/operations/DeleteHelper.java deleted file mode 100644 index 1ad5ede560..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/operations/DeleteHelper.java +++ /dev/null @@ -1,190 +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.harry.operations; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.IntConsumer; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.util.BitSet; - -public class DeleteHelper -{ - public static CompiledStatement deleteColumn(SchemaSpec schema, - long pd, - long cd, - BitSet columns, - BitSet mask, - long rts) - { - if (columns == null || columns.allUnset(mask)) - throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns); - - return delete(schema, pd, cd, columns, mask, rts); - } - - public static CompiledStatement deleteColumn(SchemaSpec schema, - long pd, - BitSet columns, - BitSet mask, - long rts) - { - if (columns == null || columns.allUnset(mask)) - throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns); - - return delete(schema, pd, columns, mask, rts); - } - - public static CompiledStatement deleteRow(SchemaSpec schema, - long pd, - long cd, - long rts) - { - return delete(schema, pd, cd, null, null, rts); - } - - public static CompiledStatement delete(SchemaSpec schema, - long pd, - List relations, - BitSet columnsToDelete, - BitSet mask, - long rts) - { - assert (columnsToDelete == null && mask == null) || (columnsToDelete != null && mask != null); - return compile(schema, - pd, - relations, - columnsToDelete, - mask, - rts); - } - - private static CompiledStatement delete(SchemaSpec schema, - long pd, - long cd, - BitSet columnsToDelete, - BitSet mask, - long rts) - { - return compile(schema, - pd, - Relation.eqRelations(schema.ckGenerator.slice(cd), - schema.clusteringKeys), - columnsToDelete, - mask, - rts); - } - - private static CompiledStatement delete(SchemaSpec schema, - long pd, - BitSet columnsToDelete, - BitSet mask, - long rts) - { - return compile(schema, - pd, - new ArrayList<>(), - columnsToDelete, - mask, - rts); - } - - public static CompiledStatement delete(SchemaSpec schema, - long pd, - long rts) - { - return compile(schema, - pd, - Collections.emptyList(), - null, - null, - rts); - } - - private static CompiledStatement compile(SchemaSpec schema, - long pd, - List relations, - BitSet columnsToDelete, - BitSet mask, - long ts) - { - StringBuilder b = new StringBuilder(); - b.append("DELETE "); - if (columnsToDelete != null) - { - assert mask != null; - assert relations == null || relations.stream().allMatch((r) -> r.kind == Relation.RelationKind.EQ); - String[] names = columnNames(schema.allColumns, columnsToDelete, mask); - for (int i = 0; i < names.length; i++) - { - if (i > 0) - b.append(", "); - b.append(names[i]); - } - b.append(" "); - } - b.append("FROM ") - .append(schema.keyspace).append(".").append(schema.table) - .append(" USING TIMESTAMP ") - .append(ts) - .append(" WHERE "); - - List bindings = new ArrayList<>(); - - schema.inflateRelations(pd, - relations, - new SchemaSpec.AddRelationCallback() - { - boolean isFirst = true; - public void accept(ColumnSpec spec, Relation.RelationKind kind, Object value) - { - if (isFirst) - isFirst = false; - else - b.append(" AND "); - b.append(kind.getClause(spec)); - bindings.add(value); - } - }); - - b.append(";"); - - Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); - - return new CompiledStatement(b.toString(), bindingsArr); - } - - private static String[] columnNames(List> columns, BitSet selectedColumns, BitSet mask) - { - String[] columnNames = new String[selectedColumns.setCount(mask)]; - selectedColumns.eachSetBit(new IntConsumer() - { - int i = 0; - - public void accept(int idx) - { - columnNames[i++] = columns.get(idx).name; - } - }, mask); - return columnNames; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/FilteringQuery.java b/test/harry/main/org/apache/cassandra/harry/operations/FilteringQuery.java deleted file mode 100644 index 1a39e50fa8..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/operations/FilteringQuery.java +++ /dev/null @@ -1,74 +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.harry.operations; - -import java.util.List; - -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.reconciler.Reconciler; -import org.apache.cassandra.harry.util.DescriptorRanges; - -public class FilteringQuery extends Query -{ - public FilteringQuery(long pd, - boolean reverse, - List relations, - SchemaSpec schemaSpec) - { - super(QueryKind.SINGLE_PARTITION, pd, reverse, relations, schemaSpec); - } - - public boolean match(Reconciler.RowState rowState) - { - for (Relation relation : relations) - { - switch (relation.columnSpec.kind) - { - case CLUSTERING: - if (!matchCd(rowState.cd)) - return false; - break; - case REGULAR: - if (!relation.match(rowState.vds[relation.columnSpec.getColumnIndex()])) - return false; - break; - case STATIC: - if (!relation.match(rowState.partitionState.staticRow().vds[relation.columnSpec.getColumnIndex()])) - return false; - break; - case PARTITION_KEY: - if (!relation.match(rowState.partitionState.pd)) - return false; - break; - } - } - return true; - } - - public DescriptorRanges.DescriptorRange toRange(long ts) - { - throw new IllegalStateException("not implemented for filtering query"); - } - - @Override - public String toString() - { - return "FilteringQuery{pd=" + pd + ", reverse=" + reverse + ", relations=" + relations + '}'; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/Query.java b/test/harry/main/org/apache/cassandra/harry/operations/Query.java deleted file mode 100644 index 2ca8a429ee..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/operations/Query.java +++ /dev/null @@ -1,557 +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.harry.operations; - -import java.util.*; -import java.util.function.LongSupplier; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.DataGenerators; -import org.apache.cassandra.harry.gen.rng.RngUtils; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.util.DescriptorRanges; - -import static org.apache.cassandra.harry.operations.QueryGenerator.relationKind; -import static org.apache.cassandra.harry.operations.Relation.FORWARD_COMPARATOR; - -/** - * A class representing relations in the query, essentially what WHERE clause means. - */ -public abstract class Query -{ - private static final Logger logger = LoggerFactory.getLogger(Query.class); - // TODO: There are queries without PD - public final long pd; - public final boolean reverse; - public final List relations; - public final Map> relationsMap; - public final SchemaSpec schemaSpec; - public final QueryKind queryKind; - public final Selection selection; - - public Query(QueryKind kind, long pd, boolean reverse, List relations, SchemaSpec schemaSpec) - { - this(kind, pd, reverse, relations, schemaSpec, new Columns(schemaSpec.allColumnsSet, true)); - } - - public Query(QueryKind kind, long pd, boolean reverse, List relations, SchemaSpec schemaSpec, Selection selection) - { - this.queryKind = kind; - this.pd = pd; - this.reverse = reverse; - this.relations = relations; - this.relationsMap = new HashMap<>(); - for (Relation relation : relations) - this.relationsMap.computeIfAbsent(relation.column(), column -> new ArrayList<>()).add(relation); - this.schemaSpec = schemaSpec; - this.selection = selection; - } - - // TODO: pd, values, filtering? - public boolean matchCd(long cd) - { - return simpleMatch(this, cd); - } - - public static boolean simpleMatch(Query query, - long cd) - { - long[] sliced = query.schemaSpec.ckGenerator.slice(cd); - for (int i = 0; i < query.schemaSpec.clusteringKeys.size(); i++) - { - List relations = query.relationsMap.get(query.schemaSpec.clusteringKeys.get(i).name); - if (relations == null) - continue; - - for (Relation r : relations) - { - if (!r.match(sliced[i])) - return false; - } - } - - return true; - } - - public static class SinglePartitionQuery extends Query - { - public SinglePartitionQuery(QueryKind kind, long pd, boolean reverse, List allRelations, SchemaSpec schemaSpec, Selection selection) - { - super(kind, pd, reverse, allRelations, schemaSpec, selection); - } - - public boolean matchCd(long cd) - { - return true; - } - - public DescriptorRanges.DescriptorRange toRange(long ts) - { - return new DescriptorRanges.DescriptorRange(Long.MIN_VALUE, Long.MAX_VALUE, true, true, ts); - } - - public String toString() - { - return "SinglePartitionQuery{" + - "pd=" + pd + - ", reverse=" + reverse + - ", relations=" + relations + - ", relationsMap=" + relationsMap + - ", schemaSpec=" + schemaSpec + - '}'; - } - } - - public static class SingleClusteringQuery extends Query - { - private final long cd; - - public SingleClusteringQuery(QueryKind kind, long pd, long cd, boolean reverse, List allRelations, SchemaSpec schemaSpec) - { - super(kind, pd, reverse, allRelations, schemaSpec); - this.cd = cd; - } - - public DescriptorRanges.DescriptorRange toRange(long ts) - { - return new DescriptorRanges.DescriptorRange(cd, cd, true, true, ts); - } - - @Override - public boolean matchCd(long cd) - { - return cd == this.cd; - } - } - - public static class ClusteringSliceQuery extends ClusteringRangeQuery - { - public ClusteringSliceQuery(QueryKind kind, - long pd, - long cdMin, - long cdMax, - Relation.RelationKind minRelation, - Relation.RelationKind maxRelation, - boolean reverse, - List allRelations, - SchemaSpec schemaSpec) - { - super(kind, pd, cdMin, cdMax, minRelation, maxRelation, reverse, allRelations, schemaSpec); - } - - public String toString() - { - return "ClusteringSliceQuery{" + - "\n" + toSelectStatement() + - "\npd=" + pd + - "\nreverse=" + reverse + - "\nrelations=" + relations + - "\nrelationsMap=" + relationsMap + - "\nschemaSpec=" + schemaSpec + - "\nqueryKind=" + queryKind + - "\ncdMin=" + cdMin + - "(" + Arrays.toString(schemaSpec.ckGenerator.slice(cdMin)) + ")" + - "\ncdMax=" + cdMax + - "(" + Arrays.toString(schemaSpec.ckGenerator.slice(cdMax)) + ")" + - "\nminRelation=" + minRelation + - "\nmaxRelation=" + maxRelation + - '}' + "\n" + toSelectStatement().cql(); - } - } - - public static class ClusteringRangeQuery extends Query - { - protected final long cdMin; - protected final long cdMax; - protected final Relation.RelationKind minRelation; - protected final Relation.RelationKind maxRelation; - - public ClusteringRangeQuery(QueryKind kind, - long pd, - long cdMin, - long cdMax, - Relation.RelationKind minRelation, - Relation.RelationKind maxRelation, - boolean reverse, - List allRelations, - SchemaSpec schemaSpec) - { - super(kind, pd, reverse, allRelations, schemaSpec); - this.cdMin = cdMin; - this.cdMax = cdMax; - this.minRelation = minRelation; - this.maxRelation = maxRelation; - } - - public DescriptorRanges.DescriptorRange toRange(long ts) - { - return new DescriptorRanges.DescriptorRange(cdMin, cdMax, minRelation.isInclusive(), maxRelation.isInclusive(), ts); - } - - public boolean matchCd(long cd) - { - // TODO: looks like we don't really need comparator here. - Relation.LongComparator cmp = FORWARD_COMPARATOR; - boolean res = minRelation.match(cmp, cd, cdMin) && maxRelation.match(cmp, cd, cdMax); - if (!logger.isDebugEnabled()) - return res; - boolean superRes = super.matchCd(cd); - if (res != superRes) - { - logger.debug("Query did not pass a sanity check.\n{}\n Super call returned: {}, while current call: {}\n" + - "cd = {} {} ({})\n" + - "this.cdMin = {} {} ({})\n" + - "this.cdMax = {} {} ({})\n" + - "minRelation.match(cmp, cd, this.cdMin) = {}\n" + - "maxRelation.match(cmp, cd, this.cdMax) = {}\n", - this, - superRes, res, - cd, Long.toHexString(cd), Arrays.toString(schemaSpec.ckGenerator.slice(cd)), - cdMin, Long.toHexString(cdMin), Arrays.toString(schemaSpec.ckGenerator.slice(cdMin)), - cdMax, Long.toHexString(cdMax), Arrays.toString(schemaSpec.ckGenerator.slice(cdMax)), - minRelation.match(cmp, cd, cdMin), - maxRelation.match(cmp, cd, cdMax)); - } - return res; - } - - public String toString() - { - return "ClusteringRangeQuery{" + - "pd=" + pd + - ", reverse=" + reverse + - ", relations=" + relations + - ", relationsMap=" + relationsMap + - ", schemaSpec=" + schemaSpec + - ", cdMin=" + cdMin + - ", cdMax=" + cdMax + - ", minRelation=" + minRelation + - ", maxRelation=" + maxRelation + - '}'; - } - } - - public CompiledStatement toSelectStatement() - { - return SelectHelper.select(schemaSpec, pd, selection.columns(), relations, reverse, selection.includeTimestamp()); - } - - public CompiledStatement toSelectStatement(boolean includeWriteTime) - { - return SelectHelper.select(schemaSpec, pd, selection.columns(), relations, reverse, includeWriteTime); - } - - public CompiledStatement toSelectStatement(Set> columns, boolean includeWriteTime) - { - return SelectHelper.select(schemaSpec, pd, columns, relations, reverse, includeWriteTime); - } - - public CompiledStatement toDeleteStatement(long rts) - { - return DeleteHelper.delete(schemaSpec, pd, relations, null, null, rts); - } - - public abstract DescriptorRanges.DescriptorRange toRange(long ts); - - public static Query selectAllColumns(SchemaSpec schemaSpec, long pd, boolean reverse) - { - return selectPartition(schemaSpec, pd, reverse, new Columns(schemaSpec.allColumnsSet, true)); - } - - public static Query selectAllColumnsWildcard(SchemaSpec schemaSpec, long pd, boolean reverse) - { - return selectPartition(schemaSpec, pd, reverse, Wildcard.instance); - } - - public static Query selectPartition(SchemaSpec schemaSpec, long pd, boolean reverse, Selection selection) - { - return new Query.SinglePartitionQuery(Query.QueryKind.SINGLE_PARTITION, - pd, - reverse, - Collections.emptyList(), - schemaSpec, - selection); - } - - public static Query singleClustering(SchemaSpec schema, long pd, long cd, boolean reverse) - { - return new Query.SingleClusteringQuery(Query.QueryKind.SINGLE_CLUSTERING, - pd, - cd, - reverse, - Relation.eqRelations(schema.ckGenerator.slice(cd), schema.clusteringKeys), - schema); - } - - public static Query clusteringSliceQuery(SchemaSpec schema, long pd, long cd, long queryDescriptor, boolean isGt, boolean isEquals, boolean reverse) - { - List relations = new ArrayList<>(); - - long[] sliced = schema.ckGenerator.slice(cd); - long min; - long max; - int nonEqFrom = RngUtils.asInt(queryDescriptor, 0, sliced.length - 1); - - long[] minBound = new long[sliced.length]; - long[] maxBound = new long[sliced.length]; - - // Algorithm that determines boundaries for a clustering slice. - // - // Basic principles are not hard but there are a few edge cases. I haven't figured out how to simplify - // those, so there might be some room for improvement. In short, what we want to achieve is: - // - // 1. Every part that is restricted with an EQ relation goes into the bound verbatim. - // 2. Every part that is restricted with a non-EQ relation (LT, GT, LTE, GTE) is taken into the bound - // if it is required to satisfy the relationship. For example, in `ck1 = 0 AND ck2 < 5`, ck2 will go - // to the _max_ boundary, and minimum value will go to the _min_ boundary, since we can select every - // descriptor that is prefixed with ck1. - // 3. Every other part (e.g., ones that are not explicitly mentioned in the query) has to be restricted - // according to equality. For example, in `ck1 = 0 AND ck2 < 5`, ck3 that is present in schema but not - // mentioned in query, makes sure that any value between [0, min_value, min_value] and [0, 5, min_value] - // is matched. - // - // One edge case is a query on the first clustering key: `ck1 < 5`. In this case, we have to fixup the lower - // value to the minimum possible value. We could really just do Long.MIN_VALUE, but in case we forget to - // adjust entropy elsewhere, it'll be caught correctly here. - for (int i = 0; i < sliced.length; i++) - { - long v = sliced[i]; - DataGenerators.KeyGenerator gen = schema.ckGenerator; - ColumnSpec column = schema.clusteringKeys.get(i); - int idx = i; - LongSupplier maxSupplier = () -> gen.maxValue(idx); - LongSupplier minSupplier = () -> gen.minValue(idx); - - if (i < nonEqFrom) - { - relations.add(Relation.eqRelation(schema.clusteringKeys.get(i), v)); - minBound[i] = v; - maxBound[i] = v; - } - else if (i == nonEqFrom) - { - relations.add(Relation.relation(relationKind(isGt, isEquals), schema.clusteringKeys.get(i), v)); - - if (column.isReversed()) - { - minBound[i] = isGt ? minSupplier.getAsLong() : v; - maxBound[i] = isGt ? v : maxSupplier.getAsLong(); - } - else - { - minBound[i] = isGt ? v : minSupplier.getAsLong(); - maxBound[i] = isGt ? maxSupplier.getAsLong() : v; - } - } - else - { - if (isEquals) - { - minBound[i] = minSupplier.getAsLong(); - maxBound[i] = maxSupplier.getAsLong(); - } - // If we have a non-eq case, all subsequent bounds have to correspond to the maximum in normal case, - // or minimum in case the last bound locked with a relation was reversed. - // - // For example, if we have (ck1, ck2, ck3) as (ASC, DESC, ASC), and query ck1 > X, we'll have: - // [xxxxx | max_value | max_value] - // ck1 ck2 ck3 - // which will exclude xxxx, but take every possible (ck1 > xxxxx) prefixed value. - // - // Similarly, if we have (ck1, ck2, ck3) as (ASC, DESC, ASC), and query ck1 <= X, we'll have: - // [xxxxx | max_value | max_value] - // which will include every (ck1 < xxxxx), and any clustering prefixed with xxxxx. - else if (schema.clusteringKeys.get(nonEqFrom).isReversed()) - maxBound[i] = minBound[i] = isGt ? minSupplier.getAsLong() : maxSupplier.getAsLong(); - else - maxBound[i] = minBound[i] = isGt ? maxSupplier.getAsLong() : minSupplier.getAsLong(); - } - } - - if (schema.clusteringKeys.get(nonEqFrom).isReversed()) - isGt = !isGt; - - min = schema.ckGenerator.stitch(minBound); - max = schema.ckGenerator.stitch(maxBound); - - if (nonEqFrom == 0) - { - min = isGt ? min : schema.ckGenerator.minValue(); - max = !isGt ? max : schema.ckGenerator.maxValue(); - } - - // if we're about to create an "impossible" query, just bump the modifier and re-generate - if (min == max && !isEquals) - throw new IllegalArgumentException("Impossible Query"); - - return new Query.ClusteringSliceQuery(Query.QueryKind.CLUSTERING_SLICE, - pd, - min, - max, - relationKind(true, isGt ? isEquals : true), - relationKind(false, !isGt ? isEquals : true), - reverse, - relations, - schema); - } - - public static Query clusteringRangeQuery(SchemaSpec schema, long pd, long cd1, long cd2, long queryDescriptor, boolean isMinEq, boolean isMaxEq, boolean reverse) - { - List relations = new ArrayList<>(); - - long[] minBound = schema.ckGenerator.slice(cd1); - long[] maxBound = schema.ckGenerator.slice(cd2); - - int nonEqFrom = RngUtils.asInt(queryDescriptor, 0, schema.clusteringKeys.size() - 1); - - // Logic here is similar to how clustering slices are implemented, except for both lower and upper bound - // get their values from sliced value in (1) and (2) cases: - // - // 1. Every part that is restricted with an EQ relation, takes its value from the min bound. - // TODO: this can actually be improved, since in case of hierarchical clustering generation we can - // pick out of the keys that are already locked. That said, we'll exercise more cases the way - // it is implemented right now. - // 2. Every part that is restricted with a non-EQ relation is taken into the bound, if it is used in - // the query. For example in, `ck1 = 0 AND ck2 > 2 AND ck2 < 5`, ck2 values 2 and 5 will be placed, - // correspondingly, to the min and max bound. - // 3. Every other part has to be restricted according to equality. Similar to clustering slice, we have - // to decide whether we use a min or the max value for the bound. Foe example `ck1 = 0 AND ck2 > 2 AND ck2 <= 5`, - // assuming we have ck3 that is present in schema but not mentioned in the query, we'll have bounds - // created as follows: [0, 2, max_value] and [0, 5, max_value]. Idea here is that since ck2 = 2 is excluded, - // we also disallow all ck3 values for [0, 2] prefix. Similarly, since ck2 = 5 is included, we allow every - // ck3 value with a prefix of [0, 5]. - for (int i = 0; i < schema.clusteringKeys.size(); i++) - { - ColumnSpec col = schema.clusteringKeys.get(i); - if (i < nonEqFrom) - { - relations.add(Relation.eqRelation(col, minBound[i])); - maxBound[i] = minBound[i]; - } - else if (i == nonEqFrom) - { - long minLocked = Math.min(minBound[nonEqFrom], maxBound[nonEqFrom]); - long maxLocked = Math.max(minBound[nonEqFrom], maxBound[nonEqFrom]); - relations.add(Relation.relation(relationKind(true, col.isReversed() ? isMaxEq : isMinEq), col, - col.isReversed() ? maxLocked : minLocked)); - relations.add(Relation.relation(relationKind(false, col.isReversed() ? isMinEq : isMaxEq), col, - col.isReversed() ? minLocked : maxLocked)); - minBound[i] = minLocked; - maxBound[i] = maxLocked; - } - else - { - minBound[i] = isMinEq ? schema.ckGenerator.minValue(i) : schema.ckGenerator.maxValue(i); - maxBound[i] = isMaxEq ? schema.ckGenerator.maxValue(i) : schema.ckGenerator.minValue(i); - } - } - - long stitchedMin = schema.ckGenerator.stitch(minBound); - long stitchedMax = schema.ckGenerator.stitch(maxBound); - - // TODO: one of the ways to get rid of garbage here, and potentially even simplify the code is to - // simply return bounds here. After bounds are created, we slice them and generate query right - // from the bounds. In this case, we can even say that things like -inf/+inf are special values, - // and use them as placeholders. Also, it'll be easier to manipulate relations. - return new Query.ClusteringRangeQuery(Query.QueryKind.CLUSTERING_RANGE, - pd, - stitchedMin, - stitchedMax, - relationKind(true, isMinEq), - relationKind(false, isMaxEq), - reverse, - relations, - schema); - } - - public enum QueryKind - { - SINGLE_PARTITION, - SINGLE_CLUSTERING, - // Not sure if that's the best way to name these, but the difference is as follows: - // - // For a single clustering, clustering slice is essentially [x; ∞) or (-∞; x]. - // - // However, in case of multiple clusterings, such as (ck1, ck2, ck3), can result into a range query with locked prefix: - // ck1 = x and ck2 = y and ck3 > z - // - // This would translate into bounds such as: - // ( (x, y, z); (x, y, max_ck3) ) - // - // Logic here is that when we're "locking" x and y, and allow third slice to be in the range [z; ∞). - // Implementation is a bit more involved than that, since we have to make sure it works for all clustering sizes - // and for reversed type. - CLUSTERING_SLICE, - // For a single clustering, clustering slice is essentially [x; y]. - // - // For multiple-clusterings case, for example (ck1, ck2, ck3), we select how many clusterings we "lock" with EQ - // relation, and do a range slice for the rest. For example: - // ck1 = x and ck2 = y and ck3 > z1 and ck3 < z2 - // - // Such queries only make sense if written partition actually has clusterings that have intersecting parts. - CLUSTERING_RANGE - } - - public interface Selection - { - Set> columns(); - boolean includeTimestamp(); - } - - public static class Wildcard implements Selection - { - public static final Wildcard instance = new Wildcard(); - - public Set> columns() - { - return null; - } - - public boolean includeTimestamp() - { - return false; - } - } - - public static class Columns implements Selection - { - private Set> columns; - private boolean includeTimestamp; - - public Columns(Set> columns, boolean includeTimestamp) - { - this.columns = columns; - this.includeTimestamp = includeTimestamp; - } - - public Set> columns() - { - return columns; - } - - public boolean includeTimestamp() - { - return includeTimestamp; - } - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/operations/QueryGenerator.java b/test/harry/main/org/apache/cassandra/harry/operations/QueryGenerator.java deleted file mode 100644 index aff09e3cfc..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/operations/QueryGenerator.java +++ /dev/null @@ -1,176 +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.harry.operations; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.rng.RngUtils; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.model.OpSelectors; - -// TODO: there's a lot of potential to reduce an amount of garbage here. -// TODO: refactor. Currently, this class is a base for both SELECT and DELETE statements. In retrospect, -// a better way to do the same thing would've been to just inflate bounds, be able to inflate -// any type of query from the bounds, and leave things like "reverse" up to the last mile / implementation. -public class QueryGenerator -{ - private static final Logger logger = LoggerFactory.getLogger(QueryGenerator.class); - - private static final long GT_STREAM = 0b1; - private static final long E_STREAM = 0b10; - - private final OpSelectors.PureRng rng; - private final OpSelectors.PdSelector pdSelector; - private final OpSelectors.DescriptorSelector descriptorSelector; - private final SchemaSpec schema; - - public QueryGenerator(Run run) - { - this(run.schemaSpec, run.pdSelector, run.descriptorSelector, run.rng); - } - - public QueryGenerator(SchemaSpec schema, - OpSelectors.PdSelector pdSelector, - OpSelectors.DescriptorSelector descriptorSelector, - OpSelectors.PureRng rng) - { - this.pdSelector = pdSelector; - this.descriptorSelector = descriptorSelector; - this.schema = schema; - this.rng = rng; - } - - public static class TypedQueryGenerator - { - private final OpSelectors.PureRng rng; - private final QueryGenerator queryGenerator; - private final Surjections.Surjection queryKindGen; - - public TypedQueryGenerator(Run run) - { - this(run.rng, new QueryGenerator(run)); - } - - public TypedQueryGenerator(OpSelectors.PureRng rng, - QueryGenerator queryGenerator) - { - this(rng, Surjections.enumValues(Query.QueryKind.class), queryGenerator); - } - - public TypedQueryGenerator(OpSelectors.PureRng rng, - Surjections.Surjection queryKindGen, - QueryGenerator queryGenerator) - { - this.rng = rng; - this.queryGenerator = queryGenerator; - this.queryKindGen = queryKindGen; - } - - // Queries are inflated from LTS, which identifies the partition, and i, a modifier for the query to - // be able to generate different queries for the same lts. - public Query inflate(long lts, long modifier) - { - long descriptor = rng.next(modifier, lts); - Query.QueryKind queryKind = queryKindGen.inflate(descriptor); - return queryGenerator.inflate(lts, modifier, queryKind); - } - } - - public Query inflate(long lts, long modifier, Query.QueryKind queryKind) - { - long pd = pdSelector.pd(lts, schema); - long queryDescriptor = rng.next(modifier, lts); - boolean reverse = queryDescriptor % 2 == 0; - switch (queryKind) - { - case SINGLE_PARTITION: - return singlePartition(pd, reverse); - case SINGLE_CLUSTERING: - { - long cd = descriptorSelector.randomCd(pd, queryDescriptor, schema); - return singleClustering(pd, cd, reverse); - } - case CLUSTERING_SLICE: - { - long cd = descriptorSelector.randomCd(pd, queryDescriptor, schema); - try - { - return clusteringSliceQuery(pd, cd, queryDescriptor, reverse); - } - catch (IllegalArgumentException retry) - { - return inflate(lts, modifier + 1, queryKind); - } - } - case CLUSTERING_RANGE: - { - try - { - long cd1 = descriptorSelector.randomCd(pd, queryDescriptor, schema); - long cd2 = descriptorSelector.randomCd(pd, rng.next(queryDescriptor, lts), schema); - return clusteringRangeQuery(pd, cd1, cd2, queryDescriptor, reverse); - } - catch (IllegalArgumentException retry) - { - return inflate(lts, modifier + 1, queryKind); - } - } - default: - throw new IllegalArgumentException("Shouldn't happen"); - } - } - - public Query singlePartition(long pd, boolean reverse) - { - return Query.selectAllColumns(schema, pd, reverse); - } - - public Query singleClustering(long pd, long cd, boolean reverse) - { - return Query.singleClustering(schema, pd, cd, reverse); - } - - public Query clusteringSliceQuery(long pd, long cd, long queryDescriptor, boolean reverse) - { - boolean isGt = RngUtils.asBoolean(rng.next(queryDescriptor, GT_STREAM)); - // TODO: make generation of EQ configurable; turn it off and on - boolean isEquals = RngUtils.asBoolean(rng.next(queryDescriptor, E_STREAM)); - - return Query.clusteringSliceQuery(schema, pd, cd, queryDescriptor, isGt, isEquals, reverse); - } - - public Query clusteringRangeQuery(long pd, long cd1, long cd2, long queryDescriptor, boolean reverse) - { - boolean isMinEq = RngUtils.asBoolean(queryDescriptor); - boolean isMaxEq = RngUtils.asBoolean(rng.next(queryDescriptor, pd)); - - return Query.clusteringRangeQuery(schema, pd, cd1, cd2, queryDescriptor, isMinEq, isMaxEq, reverse); - } - - public static Relation.RelationKind relationKind(boolean isGt, boolean isEquals) - { - if (isGt) - return isEquals ? Relation.RelationKind.GTE : Relation.RelationKind.GT; - else - return isEquals ? Relation.RelationKind.LTE : Relation.RelationKind.LT; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/Relation.java b/test/harry/main/org/apache/cassandra/harry/operations/Relation.java deleted file mode 100644 index e957125192..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/operations/Relation.java +++ /dev/null @@ -1,299 +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.harry.operations; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.gen.DataGenerators; - -public class Relation -{ - public final RelationKind kind; - public final ColumnSpec columnSpec; - // Theoretically, in model, we'll just be able to compare stuff according to relation, and pass it to DB - public long descriptor; - - Relation(RelationKind kind, - ColumnSpec columnSpec, - long descriptor) - { - this.kind = kind; - this.columnSpec = columnSpec; - this.descriptor = descriptor; - } - - public boolean match(long l) - { - // TODO: there are == NULL queries - if (l == DataGenerators.NIL_DESCR || l == DataGenerators.UNSET_DESCR) - return false; - - return kind.match(columnSpec.type.generator()::compare, l, descriptor); - } - - public Object value() - { - return columnSpec.inflate(descriptor); - } - - public String column() - { - return columnSpec.name; - } - - public String toClause() - { - return kind.getClause(column()); - } - - public String toString() - { - return "Relation{" + - "kind=" + kind + - ", columnSpec=" + columnSpec + - ", descriptor=" + descriptor + " (" + Long.toHexString(descriptor) + ")" + - '}'; - } - - public static Relation relation(RelationKind kind, ColumnSpec columnSpec, long descriptor) - { - return new Relation(kind, columnSpec, descriptor); - } - - public static Relation eqRelation(ColumnSpec columnSpec, long descriptor) - { - return new Relation(RelationKind.EQ, columnSpec, descriptor); - } - - public static List eqRelations(long[] key, List> columnSpecs) - { - List relations = new ArrayList<>(key.length); - addEqRelation(key, columnSpecs, relations); - return relations; - } - - public static void addEqRelation(long[] key, List> columnSpecs, List relations) - { - addRelation(key, columnSpecs, relations, RelationKind.EQ); - } - - public static void addRelation(long[] key, List> columnSpecs, List relations, RelationKind kind) - { - assert key.length == columnSpecs.size() || key.length > DataGenerators.KeyGenerator.MAX_UNIQUE_PREFIX_COLUMNS : - String.format("Key size (%d) should equal to column spec size (%d). Specs: %s", key.length, columnSpecs.size(), columnSpecs); - for (int i = 0; i < key.length; i++) - { - ColumnSpec spec = columnSpecs.get(i); - relations.add(relation(kind, spec, key[i])); - } - } - - public enum RelationKind - { - LT - { - public boolean isNegatable() - { - return true; - } - - public boolean isInclusive() - { - return false; - } - - public RelationKind negate() - { - return GT; - } - - public long nextMatch(long n) - { - return Math.subtractExact(n, 1); - } - - public String toString() - { - return "<"; - } - - public boolean match(LongComparator comparator, long l, long r) - { - return comparator.compare(l, r) < 0; - } - }, - GT - { - public boolean isNegatable() - { - return true; - } - - public boolean isInclusive() - { - return false; - } - - public RelationKind negate() - { - return LT; - } - - public String toString() - { - return ">"; - } - - public boolean match(LongComparator comparator, long l, long r) - { - return comparator.compare(l, r) > 0; - } - - public long nextMatch(long n) - { - return Math.addExact(n, 1); - } - }, - LTE - { - public boolean isNegatable() - { - return true; - } - - public boolean isInclusive() - { - return true; - } - - public RelationKind negate() - { - return GTE; - } - - public String toString() - { - return "<="; - } - - public boolean match(LongComparator comparator, long l, long r) - { - return comparator.compare(l, r) <= 0; - } - - public long nextMatch(long n) - { - return Math.subtractExact(n, 1); - } - }, - GTE - { - public boolean isNegatable() - { - return true; - } - - public boolean isInclusive() - { - return true; - } - - public RelationKind negate() - { - return LTE; - } - - public String toString() - { - return ">="; - } - - public boolean match(LongComparator comparator, long l, long r) - { - return comparator.compare(l, r) >= 0; - } - - public long nextMatch(long n) - { - return Math.addExact(n, 1); - } - }, - EQ - { - public boolean isNegatable() - { - return false; - } - - public boolean isInclusive() - { - return true; - } - - public RelationKind negate() - { - throw new IllegalArgumentException("Cannot negate EQ"); - } - - public long nextMatch(long n) - { - return n; - } - - public String toString() - { - return "="; - } - - public boolean match(LongComparator comparator, long l, long r) - { - return comparator.compare(l, r) == 0; - } - }; - - public abstract boolean match(LongComparator comparator, long l, long r); - - public String getClause(String name) - { - return String.format("%s %s ?", name, toString()); - } - - public String getClause(ColumnSpec spec) - { - return getClause(spec.name); - } - - public abstract boolean isNegatable(); - - public abstract boolean isInclusive(); - - public abstract RelationKind negate(); - - public abstract long nextMatch(long n); - } - - public static interface LongComparator - { - public int compare(long l, long r); - } - - public static LongComparator FORWARD_COMPARATOR = Long::compare; -} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/WriteHelper.java b/test/harry/main/org/apache/cassandra/harry/operations/WriteHelper.java deleted file mode 100644 index 75921dfa47..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/operations/WriteHelper.java +++ /dev/null @@ -1,166 +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.harry.operations; - -import java.util.List; - -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.DataGenerators; - -public class WriteHelper -{ - public static CompiledStatement inflateInsert(SchemaSpec schema, - long pd, - long cd, - long[] vds, - long[] sds, - long timestamp) - { - Object[] partitionKey = schema.inflatePartitionKey(pd); - Object[] clusteringKey = schema.inflateClusteringKey(cd); - Object[] staticColumns = sds == null ? null : schema.inflateStaticColumns(sds); - Object[] regularColumns = schema.inflateRegularColumns(vds); - - Object[] bindings = new Object[schema.allColumns.size()]; - - StringBuilder b = new StringBuilder(); - b.append("INSERT INTO ") - .append(schema.keyspace) - .append('.') - .append(schema.table) - .append(" ("); - - int bindingsCount = 0; - bindingsCount += appendStatements(b, bindings, schema.partitionKeys, partitionKey, bindingsCount, true, ",", "%s"); - bindingsCount += appendStatements(b, bindings, schema.clusteringKeys, clusteringKey, bindingsCount, false, ",", "%s"); - bindingsCount += appendStatements(b, bindings, schema.regularColumns, regularColumns, bindingsCount, false, ",", "%s"); - if (staticColumns != null) - bindingsCount += appendStatements(b, bindings, schema.staticColumns, staticColumns, bindingsCount, false, ",", "%s"); - - b.append(") VALUES ("); - - for (int i = 0; i < bindingsCount; i++) - { - if (i > 0) - b.append(", "); - b.append("?"); - } - - b.append(") USING TIMESTAMP ") - .append(timestamp) - .append(";"); - - return new CompiledStatement(b.toString(), adjustArraySize(bindings, bindingsCount)); - } - - public static Object[] adjustArraySize(Object[] bindings, int bindingsCount) - { - if (bindingsCount != bindings.length) - { - Object[] tmp = new Object[bindingsCount]; - System.arraycopy(bindings, 0, tmp, 0, bindingsCount); - bindings = tmp; - } - return bindings; - } - - public static CompiledStatement inflateUpdate(SchemaSpec schema, - long pd, - long cd, - long[] vds, - long[] sds, - long timestamp) - { - Object[] partitionKey = schema.inflatePartitionKey(pd); - Object[] clusteringKey = schema.inflateClusteringKey(cd); - Object[] staticColumns = sds == null ? null : schema.inflateStaticColumns(sds); - Object[] regularColumns = schema.inflateRegularColumns(vds); - - Object[] bindings = new Object[schema.allColumns.size()]; - - StringBuilder b = new StringBuilder(); - b.append("UPDATE ") - .append(schema.keyspace) - .append('.') - .append(schema.table) - .append(" USING TIMESTAMP ") - .append(timestamp) - .append(" SET "); - - int bindingsCount = 0; - bindingsCount += addSetStatements(b, bindings, schema.regularColumns, regularColumns, bindingsCount); - if (staticColumns != null) - bindingsCount += addSetStatements(b, bindings, schema.staticColumns, staticColumns, bindingsCount); - - assert bindingsCount > 0 : "Can not have an UPDATE statement without any updates"; - b.append(" WHERE "); - - bindingsCount += addWhereStatements(b, bindings, schema.partitionKeys, partitionKey, bindingsCount, true); - bindingsCount += addWhereStatements(b, bindings, schema.clusteringKeys, clusteringKey, bindingsCount, false); - b.append(";"); - return new CompiledStatement(b.toString(), adjustArraySize(bindings, bindingsCount)); - } - - private static int addSetStatements(StringBuilder b, - Object[] bindings, - List> columns, - Object[] values, - int bound) - { - return appendStatements(b, bindings, columns, values, bound, bound == 0, ", ", "%s = ?"); - } - - private static int addWhereStatements(StringBuilder b, - Object[] bindings, - List> columns, - Object[] values, - int bound, - boolean firstStatement) - { - return appendStatements(b, bindings, columns, values, bound, firstStatement, " AND ", "%s = ?"); - } - - private static int appendStatements(StringBuilder b, - Object[] allBindings, - List> columns, - Object[] values, - int bound, - boolean firstStatement, - String separator, - String nameFormatter) - { - int bindingsCount = 0; - for (int i = 0; i < values.length; i++) - { - Object value = values[i]; - if (value == DataGenerators.UNSET_VALUE) - continue; - - ColumnSpec column = columns.get(i); - if (bindingsCount > 0 || !firstStatement) - b.append(separator); - - b.append(String.format(nameFormatter, column.name)); - allBindings[bound + bindingsCount] = value; - bindingsCount++; - } - return bindingsCount; - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/runner/EarlyExitException.java b/test/harry/main/org/apache/cassandra/harry/runner/EarlyExitException.java deleted file mode 100644 index 76b4229fd8..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/runner/EarlyExitException.java +++ /dev/null @@ -1,27 +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.harry.runner; - -public class EarlyExitException extends RuntimeException -{ - public EarlyExitException(String e) - { - super(e); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/runner/FlaggedRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/FlaggedRunner.java deleted file mode 100644 index c2450cd8bd..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/runner/FlaggedRunner.java +++ /dev/null @@ -1,84 +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.harry.runner; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.cassandra.concurrent.ExecutorFactory; -import org.apache.cassandra.concurrent.Interruptible; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.visitors.Visitor; -import org.apache.cassandra.utils.concurrent.CountDownLatch; - -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; - -public class FlaggedRunner extends Runner -{ - private final List poolConfigurations; - private final CountDownLatch stopLatch; - - public FlaggedRunner(Run run, Configuration config, List poolConfigurations, CountDownLatch stopLatch) - { - super(run, config); - this.poolConfigurations = poolConfigurations; - this.stopLatch = stopLatch; - } - - @Override - protected void runInternal() throws Throwable - { - List threads = new ArrayList<>(); - Map counters = new HashMap<>(); - for (Configuration.VisitorPoolConfiguration poolConfiguration : poolConfigurations) - { - for (int i = 0; i < poolConfiguration.concurrency; i++) - { - Visitor visitor = poolConfiguration.visitor.make(run); - String name = String.format("%s-%d", poolConfiguration.prefix, i + 1); - counters.put(name, 0); - Interruptible thread = ExecutorFactory.Global.executorFactory().infiniteLoop(name, wrapInterrupt((state) -> { - if (state == Interruptible.State.NORMAL) - { - visitor.visit(); - counters.compute(name, (n, old) -> old + 1); - } - }, stopLatch::decrement, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); - threads.add(thread); - } - } - - stopLatch.await(); - shutdown(threads::stream); - System.out.println("counters = " + counters); - if (!errors.isEmpty()) - mergeAndThrow(errors); - } - - @Override - public String type() - { - return "concurrent_flagged"; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/runner/HarryRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/HarryRunner.java deleted file mode 100644 index 0bfbb31b66..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/runner/HarryRunner.java +++ /dev/null @@ -1,115 +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.harry.runner; - -import java.io.File; -import java.io.FileNotFoundException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.util.ThrowingRunnable; - -import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL; -import static org.apache.cassandra.config.CassandraRelevantProperties.IO_NETTY_TRANSPORT_NONATIVE; -import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; - -public abstract class HarryRunner -{ - public static final Logger logger = LoggerFactory.getLogger(HarryRunner.class); - - public void run(Configuration config) throws Throwable - { - DISABLE_TCACTIVE_OPENSSL.setBoolean(true); - IO_NETTY_TRANSPORT_NONATIVE.setBoolean(true); - ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); - - Runner runner = config.createRunner(); - Run run = runner.getRun(); - - Throwable thrown = null; - - try - { - beforeRun(runner); - runner.run(); - } - catch (Throwable e) - { - logger.error("Failed due to exception: " + e.getMessage(), e); - thrown = e; - } - finally - { - logger.info("Shutting down runner.."); - boolean failed = thrown != null; - if (!failed) - { - logger.info("Shutting down cluster.."); - tryRun(run.sut::shutdown); - } - afterRun(runner, thrown); - logger.info("Exiting..."); - if (failed) - System.exit(1); - else - System.exit(0); - } - } - - public void tryRun(ThrowingRunnable runnable) - { - try - { - runnable.run(); - } - catch (Throwable t) - { - logger.error("Encountered an error while shutting down, ignoring.", t); - } - } - - /** - * Parses the command-line args and returns a File for the configuration YAML. - * @param args Command-line args. - * @return Configuration YAML file. - * @throws Exception If file is not found or cannot be read. - */ - public static File loadConfig(String... args) throws Exception { - if (args == null || args.length == 0) { - throw new Exception("Harry config YAML not provided."); - } - - File configFile = new File(args[0]); - if (!configFile.exists()) { - throw new FileNotFoundException(configFile.getAbsolutePath()); - } - - if (!configFile.canRead()) { - throw new Exception("Cannot read config file, check your permissions on " + configFile.getAbsolutePath()); - } - - return configFile; - } - - public abstract void beforeRun(Runner runner); - public abstract void afterRun(Runner runner, Object result); -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/runner/Runner.java b/test/harry/main/org/apache/cassandra/harry/runner/Runner.java deleted file mode 100644 index c62e1b671a..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/runner/Runner.java +++ /dev/null @@ -1,479 +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.harry.runner; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.ExecutorFactory; -import org.apache.cassandra.concurrent.Interruptible; -import org.apache.cassandra.concurrent.Shutdownable; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.visitors.Visitor; -import org.apache.cassandra.utils.concurrent.WaitQueue; - -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; - -public abstract class Runner -{ - private static final Logger logger = LoggerFactory.getLogger(Runner.class); - - protected final Run run; - protected final Configuration config; - - // If there's an error, there's a good chance we're going to hit it more than once - // since we have multiple concurrent checkers running - protected final List errors; - - public Runner(Run run, Configuration config) - { - this.run = run; - this.config = config; - this.errors = new CopyOnWriteArrayList<>(); - } - - public void run() throws Throwable - { - init(config, run); - try - { - runInternal(); - } - catch (EarlyExitException t) - { - logger.warn("Exiting early...", t); - } - teardown(); - } - - protected abstract void runInternal() throws Throwable; - - public Run getRun() - { - return run; - } - - public static void init(Configuration config, Run run) - { - if (config.create_schema) - { - if (config.keyspace_ddl == null) - run.sut.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + run.schemaSpec.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};"); - else - run.sut.schemaChange(config.keyspace_ddl); - - String schema = run.schemaSpec.compile().cql(); - logger.info("Creating table: " + schema); - run.sut.schemaChange(schema); - } - - if (config.truncate_table) - { - run.sut.schemaChange(String.format("truncate %s.%s;", - run.schemaSpec.keyspace, - run.schemaSpec.table)); - } - - run.sut.afterSchemaInit(); - - int res = run.sut.execute(String.format("SELECT * FROM %s.%s LIMIT 1", run.schemaSpec.keyspace, run.schemaSpec.table), SystemUnderTest.ConsistencyLevel.QUORUM).length; - if (res > 0) - { - System.out.println("========================================================================================================================"); - System.out.println("| |"); - System.out.println("| WARNING: Starting a run with non-empty tables! |"); - System.out.println("| |"); - System.out.println("========================================================================================================================"); - } - } - - public void teardown() - { - logger.info("Tearing down setup..."); - if (config.drop_schema) - { - if (!errors.isEmpty()) - { - logger.info("Preserving table {} due to errors during execution.", - run.schemaSpec.table); - return; - } - - logger.info("Dropping table: " + run.schemaSpec.table); - run.sut.schemaChange(String.format("DROP TABLE IF EXISTS %s.%s;", - run.schemaSpec.keyspace, - run.schemaSpec.table)); - } - } - - protected void maybeReportErrors() - { - if (!errors.isEmpty()) { - dumpStateToFile(run, config, errors); - } - } - - public abstract String type(); - - public interface RunnerFactory - { - Runner make(Run run, Configuration config); - } - - public abstract static class TimedRunner extends Runner - { - public final long runtime; - public final TimeUnit runtimeUnit; - - public TimedRunner(Run run, Configuration config, long runtime, TimeUnit runtimeUnit) - { - super(run, config); - - this.runtime = runtime; - this.runtimeUnit = runtimeUnit; - } - } - - public static Configuration.RunnerConfiguration single(Visitor.VisitorFactory visitor) - { - return (run, config) -> new SingleVisitRunner(run, config, Collections.singletonList(visitor)); - } - - public static class SingleVisitRunner extends Runner - { - private final List visitors; - - public SingleVisitRunner(Run run, - Configuration config, - List visitorFactories) - { - super(run, config); - this.visitors = visitorFactories.stream().map(factory -> factory.make(run)).collect(Collectors.toList()); - } - - @Override - public String type() - { - return "single"; - } - - public void runInternal() - { - for (Visitor value : visitors) - value.visit(); - } - } - - public static Configuration.RunnerConfiguration sequential(Visitor.VisitorFactory visitor, - long runtime, TimeUnit runtimeUnit) - { - return (r, c) -> new SequentialRunner(r, c, Collections.singletonList(visitor), runtime, runtimeUnit); - } - - public static Runner sequential(Run run, - Configuration config, - List poolConfigurations, - long runtime, TimeUnit runtimeUnit) - { - return new SequentialRunner(run, config, poolConfigurations, runtime, runtimeUnit); - } - - /** - * Runs all visitors sequentially, in the loop, for a specified amount of time - */ - public static class SequentialRunner extends TimedRunner - { - protected final List visitors; - - public SequentialRunner(Run run, - Configuration config, - List visitorFactories, - long runtime, TimeUnit runtimeUnit) - { - super(run, config, runtime, runtimeUnit); - - this.visitors = visitorFactories.stream().map(factory -> factory.make(run)).collect(Collectors.toList()); - } - - @Override - public String type() - { - return "sequential"; - } - - @Override - public void runInternal() throws Throwable - { - long deadline = nanoTime() + runtimeUnit.toNanos(runtime);; - while (nanoTime() < deadline) - { - for (Visitor visitor : visitors) - visitor.visit(); - } - } - } - - public static Runner concurrent(Configuration config, - List poolConfigurations, - long runtime, TimeUnit runtimeUnit) - { - return new ConcurrentRunner(config.createRun(), config, poolConfigurations, runtime, runtimeUnit); - } - - /** - * Runs all visitors concurrently, each visitor in its own thread, looped, for a specified amount of time - */ - public static class ConcurrentRunner extends TimedRunner - { - private final List poolConfigurations; - - public ConcurrentRunner(Run run, - Configuration config, - List poolConfigurations, - long runtime, TimeUnit runtimeUnit) - { - super(run, config, runtime, runtimeUnit); - this.poolConfigurations = poolConfigurations; - } - - @Override - public String type() - { - return "concurrent"; - } - - public void runInternal() throws Throwable - { - List threads = new ArrayList<>(); - WaitQueue queue = WaitQueue.newWaitQueue(); - WaitQueue.Signal interrupt = queue.register(); - - for (Configuration.VisitorPoolConfiguration poolConfiguration : poolConfigurations) - { - for (int i = 0; i < poolConfiguration.concurrency; i++) - { - Visitor visitor = poolConfiguration.visitor.make(run); - String name = String.format("%s-%d", poolConfiguration.prefix, i + 1); - Interruptible thread = ExecutorFactory.Global.executorFactory().infiniteLoop(name, wrapInterrupt((state) -> { - if (state == Interruptible.State.NORMAL) - visitor.visit(); - }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); - threads.add(thread); - } - } - - interrupt.await(runtime, runtimeUnit); - shutdown(threads::stream); - if (!errors.isEmpty()) - mergeAndThrow(errors); - } - } - - public static Runner chain(Configuration config, - Configuration.RunnerConfiguration... runners) - { - return new ChainRunner(config.createRun(), config, Arrays.asList(runners)); - } - - /** - * Chain runner is somewhat similar to StagedRunner in that it would execute multiple runners one after - * another, but unlike StagedRunner, it is single-shot: it will just run them once. - */ - public static class ChainRunner extends Runner - { - private final List stages; - - public ChainRunner(Run run, - Configuration config, - List runnerFactories) - { - super(run, config); - this.stages = new ArrayList<>(); - for (Configuration.RunnerConfiguration runner : runnerFactories) - stages.add(runner.make(run, config)); - } - - protected void runInternal() throws Throwable - { - for (Runner stage : stages) - stage.runInternal(); - } - - public String type() - { - return "chain"; - } - } - - public static void shutdown(Supplier> threads) - { - threads.get().forEach(Shutdownable::shutdown); - long deadline = nanoTime(); - threads.get().forEach((interruptible) -> { - try - { - if (!interruptible.awaitTermination(nanoTime() - deadline, TimeUnit.NANOSECONDS)) - logger.info("Could not terminate before the timeout: " + threads.get().map(Shutdownable::isTerminated).collect(Collectors.toList())); - } - catch (InterruptedException e) - { - throw new RuntimeException(e); - } - }); - - } - - public static Interruptible.Task wrapInterrupt(Interruptible.Task runnable, Runnable signalStop, Consumer registerException) - { - return (state) -> { - try - { - runnable.run(state); - } - catch (InterruptedException t) - { - signalStop.run(); - } - catch (Throwable t) - { - // Since some of the exceptions are thrown from inside instances - if (!t.getClass().toString().contains("InterruptedException")) - registerException.accept(t); - signalStop.run(); - } - }; - } - - public static void mergeAndThrow(List existingFail) - { - List skipped = existingFail.stream().filter(e -> e instanceof EarlyExitException).collect(Collectors.toList()); - for (Throwable throwable : skipped) - { - logger.warn("Skipping exit early exceptions", throwable); - return; - } - - List errors = existingFail.stream().filter(e -> !(e instanceof EarlyExitException)).collect(Collectors.toList()); - if (errors.size() == 1) - throw new RuntimeException("Interrupting run because of an exception", errors.get(0)); - - Throwable e = errors.get(0); - Throwable ret = e; - for (int i = 1; i < errors.size(); i++) - { - Throwable current = errors.get(i); - e.addSuppressed(current); - e = current; - } - - throw new RuntimeException("Interrupting run because of an exception", ret); - } - - private static void dumpExceptionToFile(BufferedWriter bw, Throwable throwable) throws IOException - { - if (throwable.getMessage() != null) - bw.write(throwable.getMessage()); - else - bw.write(""); - - bw.newLine(); - for (StackTraceElement line : throwable.getStackTrace()) - { - bw.write(line.toString()); - bw.newLine(); - } - bw.newLine(); - if (throwable.getCause() != null) - { - bw.write("Inner Exception: "); - dumpExceptionToFile(bw, throwable.getCause()); - } - - bw.newLine(); - bw.newLine(); - } - - public static void dumpStateToFile(Run run, Configuration config, List t) - { - try - { - File f = new File("failure.dump"); - logger.error("Dumping results into the file:" + f); - try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f)))) - { - bw.write("Caught exception during the run: "); - for (Throwable throwable : t) - dumpExceptionToFile(bw, throwable); - - bw.flush(); - } - - File file = new File("run.yaml"); - Configuration.ConfigurationBuilder builder = config.unbuild(); - - // override stateful components - builder.setClock(run.clock.toConfig()); - builder.setDataTracker(run.tracker.toConfig()); - - Configuration.toFile(file, builder.build()); - } - - catch (Throwable e) - { - logger.error("Caught an error while trying to dump to file", e); - try - { - File f = new File("tmp.dump"); - - if (!f.createNewFile()) - logger.info("File {} already exists. Appending...", f); - - BufferedWriter tmp = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); - dumpExceptionToFile(tmp, e); - } - catch (IOException ex) - { - ex.printStackTrace(); - } - - throw new RuntimeException(e); - } - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/runner/StagedRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/StagedRunner.java deleted file mode 100644 index bac36bfa25..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/runner/StagedRunner.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.harry.runner; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; - -import static org.apache.cassandra.utils.Clock.Global.nanoTime; - -public class StagedRunner extends Runner.TimedRunner -{ - public static final String TYPE = "staged"; - - private final List stages; - - public StagedRunner(Run run, - Configuration config, - List runnerFactories, - long runtime, TimeUnit runtimeUnit) - { - super(run, config, runtime, runtimeUnit); - this.stages = new ArrayList<>(); - for (Configuration.RunnerConfiguration runner : runnerFactories) - stages.add(runner.make(run, config)); - } - - @Override - public String type() { - return TYPE; - } - - @Override - protected void runInternal() throws Throwable - { - long deadline = nanoTime() + runtimeUnit.toNanos(nanoTime()); - - while (nanoTime() < deadline) - { - for (Runner runner : stages) - runner.runInternal(); - } - } - - @JsonTypeName(TYPE) - public static class StagedRunnerConfig implements Configuration.RunnerConfiguration - { - @JsonProperty(value = "stages") - public final List runnerFactories; - - public final long run_time; - public final TimeUnit run_time_unit; - - @JsonCreator - public StagedRunnerConfig(@JsonProperty(value = "stages") List stages, - @JsonProperty(value = "run_time", defaultValue = "2") long runtime, - @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) - { - this.runnerFactories = stages; - this.run_time = runtime; - this.run_time_unit = runtimeUnit; - } - - @Override - public Runner make(Run run, Configuration config) - { - return new StagedRunner(run, config, runnerFactories, run_time, run_time_unit); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/runner/TrivialShrinker.java b/test/harry/main/org/apache/cassandra/harry/runner/TrivialShrinker.java deleted file mode 100644 index a1bdc01af2..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/runner/TrivialShrinker.java +++ /dev/null @@ -1,189 +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.harry.runner; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Predicate; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.visitors.LtsVisitor; -import org.apache.cassandra.harry.visitors.SkippingVisitor; -import org.apache.cassandra.harry.visitors.Visitor; - -/** - * A most trivial imaginable shrinker: attempts to skip partitions and/or logical timestamps to see if the - * issue is still reproducible. - */ -public class TrivialShrinker -{ - public static void main(String[] args) throws Throwable - { - try - { - File configFile = HarryRunner.loadConfig(args); - Configuration configuration = Configuration.fromFile(configFile); - System.out.println(Configuration.toYamlString(configuration)); - - Set pdsToSkip = new HashSet<>(Arrays.asList( - // put pds you want to skip here, or Harry will find them for you - )); - - Set ltsToSkip = new HashSet<>(Arrays.asList( - // put lts you want to skip here, or Harry will find them for you - )); - - // Which LTS failure has occurred on - final long maxLts = 7000L; - - // Check if we've found exactly the exception that is causing the failure - Predicate check = (e) -> true; - - Run run = configuration.createRun(); - Configuration.SequentialRunnerConfig config = (Configuration.SequentialRunnerConfig) configuration.runner; - List visitors = new ArrayList<>(); - for (Configuration.VisitorConfiguration factory : config.visitorFactories) - { - Visitor visitor = factory.make(run); - if (visitor instanceof LtsVisitor) - { - AtomicLong counter = new AtomicLong(); - visitors.add(new SkippingVisitor((LtsVisitor) visitor, - counter::getAndIncrement, - (lts) -> run.pdSelector.pd(lts, run.schemaSpec), - ltsToSkip, - pdsToSkip)) ; - } - else - { - visitors.add(visitor); - } - } - - Set partitions = new HashSet<>(); - for (long i = 0; i < maxLts; i++) - partitions.add(run.pdSelector.pd(i, run.schemaSpec)); - - // Step one: figure out which partitions we can skip while still keeping it reproducible - for (Long pdToCheck : partitions) - { - if (pdsToSkip.contains(pdToCheck)) - continue; - pdsToSkip.add(pdToCheck); - Runner.init(configuration, run); - - try - { - runOnce(visitors, maxLts); - System.out.println("Can not skip " + pdToCheck + "\nCan only skip these: " + toString(pdsToSkip)); - pdsToSkip.remove(pdToCheck); - } - catch (RuntimeException t) - { - if (check.test(t)) - { - System.out.printf("Safe to skip: %d because without it we're still hitting an exception %s.\n%s\n", - pdToCheck, - t.getMessage(), - toString(pdsToSkip)); - } - else - { - System.out.println("Can not skip " + pdToCheck + "\n, since we seem to repro a different issue. Can only skip these: " + toString(pdsToSkip)); - pdsToSkip.remove(pdToCheck); - } - } - run.sut.schemaChange("DROP KEYSPACE " + run.schemaSpec.keyspace); - } - - // Step two: figure out which lts can be skipped within the remaining partitions - for (long lts = 0; lts < maxLts; lts++) - { - long ltsToCheck = lts; - if (ltsToSkip.contains(ltsToCheck) || pdsToSkip.contains(run.pdSelector.pd(lts, run.schemaSpec))) - continue; - ltsToSkip.add(ltsToCheck); - Runner.init(configuration, run); - - try - { - runOnce(visitors, maxLts); - System.out.println("Can not skip " + ltsToCheck + "\nCan only skip these: " + toString(ltsToSkip)); - ltsToSkip.remove(ltsToCheck); - } - catch (RuntimeException t) - { - if (check.test(t)) - { - System.out.printf("Safe to skip: %d because without it we're still hitting an exception %s.\n%s\n", - ltsToCheck, - t.getMessage(), - toString(ltsToSkip)); - } - else - { - System.out.println("Can not skip " + lts + "\n, since we seem to repro a different issue. Can only skip these: " + toString(ltsToSkip)); - ltsToSkip.remove(ltsToCheck); - } - - } - run.sut.schemaChange("DROP KEYSPACE " + run.schemaSpec.keyspace); - } - } - catch (Throwable t) - { - System.out.println(t.getMessage()); - t.printStackTrace(); - } - finally - { - System.exit(1); - } - } - - public static void runOnce(List visitors, long maxLts) - { - for (long lts = 0; lts <= maxLts; lts++) - { - for (Visitor visitor : visitors) - { - visitor.visit(); - } - } - } - - public static String toString(Set longs) - { - if (longs.isEmpty()) - return ""; - - String s = ""; - for (Long aLong : longs) - { - s += aLong + "L,"; - } - return s.substring(0, s.length() - 1); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/runner/UpToLtsRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/UpToLtsRunner.java deleted file mode 100644 index 9fc2cec5aa..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/runner/UpToLtsRunner.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.harry.runner; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.visitors.Visitor; - -import static org.apache.cassandra.utils.Clock.Global.nanoTime; - -/** - * Runner that allows to run for a specific number of logical timestamps, rather - * than being configured by time. - */ -public class UpToLtsRunner extends Runner.SequentialRunner -{ - public static final String TYPE = "up_to_lts"; - - private final long maxLts; - - public static Configuration.RunnerConfiguration factory(List visitorFactories, - long maxLts, - long runtime, TimeUnit runtimeUnit) - { - return (r, c) -> new UpToLtsRunner(r, c, visitorFactories, maxLts, runtime, runtimeUnit); - } - - public static Configuration.RunnerConfiguration factory(Visitor.VisitorFactory visitorFactory, - long maxLts, - long runtime, TimeUnit runtimeUnit) - { - return (r, c) -> new UpToLtsRunner(r, c, Collections.singletonList(visitorFactory), maxLts, runtime, runtimeUnit); - } - - public UpToLtsRunner(Run run, - Configuration config, - List visitorFactories, - long maxLts, - long runtime, TimeUnit runtimeUnit) - { - super(run, config, visitorFactories, runtime, runtimeUnit); - this.maxLts = maxLts; - } - - @Override - public String type() - { - return TYPE; - } - - @Override - public void runInternal() - { - long deadline = nanoTime() + runtimeUnit.toNanos(runtime); - while (run.tracker.maxStarted() < maxLts && nanoTime() < deadline) - { - for (Visitor visitor : visitors) - visitor.visit(); - } - } - - @JsonTypeName(TYPE) - public static class UpToLtsRunnerConfig implements Configuration.RunnerConfiguration - { - public final List visitor_factories; - public final long max_lts; - public final long run_time; - public final TimeUnit run_time_unit; - - @JsonCreator - public UpToLtsRunnerConfig(@JsonProperty(value = "visitors") List visitors, - @JsonProperty(value = "max_lts") long maxLts, - @JsonProperty(value = "run_time", defaultValue = "2") long runtime, - @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) - { - this.visitor_factories = visitors; - this.max_lts = maxLts; - this.run_time = runtime; - this.run_time_unit = runtimeUnit; - } - - @Override - public Runner make(Run run, Configuration config) - { - return new UpToLtsRunner(run, config, visitor_factories, max_lts, run_time, run_time_unit); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/DoubleWritingSut.java b/test/harry/main/org/apache/cassandra/harry/sut/DoubleWritingSut.java deleted file mode 100644 index 5c516f93ec..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/DoubleWritingSut.java +++ /dev/null @@ -1,60 +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.harry.sut; - -import java.util.concurrent.CompletableFuture; -import java.util.regex.Pattern; - -public class DoubleWritingSut implements SystemUnderTest -{ - private final SystemUnderTest primary; - private final SystemUnderTest secondary; - - public DoubleWritingSut(SystemUnderTest primary, - SystemUnderTest secondary) - { - this.primary = primary; - this.secondary = secondary; - } - public boolean isShutdown() - { - return primary.isShutdown(); - } - - public void shutdown() - { - primary.shutdown(); - } - - private static final Pattern pattern = Pattern.compile("^select", Pattern.CASE_INSENSITIVE); - - public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) - { - if (pattern.matcher(statement).find()) - return primary.execute(statement, cl, bindings); - - secondary.execute(statement, cl, bindings); - return primary.execute(statement, cl, bindings); - } - - public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) - { - throw new UnsupportedOperationException("Not implemented (yet)"); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/PrintlnSut.java b/test/harry/main/org/apache/cassandra/harry/sut/PrintlnSut.java deleted file mode 100644 index 8d9196370a..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/PrintlnSut.java +++ /dev/null @@ -1,67 +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.harry.sut; - -import java.util.Arrays; -import java.util.concurrent.CompletableFuture; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; - -public class PrintlnSut implements SystemUnderTest -{ - public boolean isShutdown() - { - return false; - } - - public void shutdown() - { - } - - public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) - { - System.out.println(String.format("%s | %s | %s", - statement, - cl, - Arrays.toString(bindings))); - return new Object[0][]; - } - - public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) - { - return CompletableFuture.supplyAsync(() -> execute(statement, cl, bindings), - Runnable::run); - } - - @JsonTypeName("println") - public static class PrintlnSutConfiguration implements Configuration.SutConfiguration - { - @JsonCreator - public PrintlnSutConfiguration() - { - - } - public SystemUnderTest make() - { - return new PrintlnSut(); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/QueryModifyingSut.java b/test/harry/main/org/apache/cassandra/harry/sut/QueryModifyingSut.java deleted file mode 100644 index aae913d8ed..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/QueryModifyingSut.java +++ /dev/null @@ -1,80 +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.harry.sut; - -import java.util.concurrent.CompletableFuture; - -import org.apache.cassandra.harry.model.AgainstSutChecker; - -/** - * Best thing for sanity checking of tricky range tombstone issues: - * write to memtable instead of writing to the usual system under test. - * - * Usually used in conjunction with {@link AgainstSutChecker}: - * - * SchemaSpec doubleWriteSchema = schema.cloneWithName(schema.keyspace, schema.keyspace + "_debug"); - * - * sut.schemaChange(doubleWriteSchema.compile().cql()); - * - * QueryModifyingSut sut = new QueryModifyingSut(this.sut, - * schema.table, - * doubleWriteSchema.table); - * - * - * Model model = new AgainstSutChecker(tracker, history.clock(), sut, schema, doubleWriteSchema); - */ -public class QueryModifyingSut implements SystemUnderTest -{ - private final SystemUnderTest delegate; - private final String toReplace; - private final String replacement; - - public QueryModifyingSut(SystemUnderTest delegate, - String toReplace, - String replacement) - { - this.delegate = delegate; - this.toReplace = toReplace; - this.replacement = replacement; - } - - public boolean isShutdown() - { - return delegate.isShutdown(); - } - - public void shutdown() - { - delegate.shutdown(); - } - - public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) - { - return delegate.execute(statement.replaceAll(toReplace, replacement), - cl, - bindings); - } - - public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) - { - return delegate.executeAsync(statement.replaceAll(toReplace, replacement), - cl, - bindings); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/SystemUnderTest.java b/test/harry/main/org/apache/cassandra/harry/sut/SystemUnderTest.java deleted file mode 100644 index bc96e07b72..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/SystemUnderTest.java +++ /dev/null @@ -1,105 +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.harry.sut; - -import java.util.concurrent.CompletableFuture; - -import org.apache.cassandra.harry.operations.CompiledStatement; - -public interface SystemUnderTest -{ - public interface SUTFactory - { - public SystemUnderTest make(); - } - - public boolean isShutdown(); - - public void shutdown(); - - default void afterSchemaInit() - { - } - - default void schemaChange(String statement) - { - execute(statement, ConsistencyLevel.ALL, new Object[]{}); - } - - Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings); - - default Object[][] execute(CompiledStatement statement, ConsistencyLevel cl) - { - return execute(statement.cql(), cl, statement.bindings()); - } - - default Object[][] executeIdempotent(String statement, ConsistencyLevel cl, Object... bindings) - { - return execute(statement, cl, bindings); - } - - CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings); - - default CompletableFuture executeIdempotentAsync(String statement, ConsistencyLevel cl, Object... bindings) - { - return executeAsync(statement, cl, bindings); - } - - interface SystemUnderTestFactory - { - SystemUnderTest create(); - } - - enum ConsistencyLevel { - ALL, QUORUM, NODE_LOCAL, ONE - } - - public static final SystemUnderTest NO_OP = new NoOpSut(); - - public class NoOpSut implements SystemUnderTest - { - private NoOpSut() {} - public boolean isShutdown() - { - return false; - } - - public void shutdown() - { - } - - public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) - { - return new Object[0][]; - } - - public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) - { - return CompletableFuture.supplyAsync(() -> execute(statement, cl, bindings), - Runnable::run); - } - } - - public static interface FaultInjectingSut extends SystemUnderTest - { - public Object[][] executeWithWriteFailure(String statement, ConsistencyLevel cl, Object... bindings); - public CompletableFuture executeAsyncWithWriteFailure(String statement, ConsistencyLevel cl, Object... bindings); - } - -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/ClusterState.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/ClusterState.java deleted file mode 100644 index 97886edda8..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/injvm/ClusterState.java +++ /dev/null @@ -1,24 +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.harry.sut.injvm; - -public interface ClusterState -{ - boolean isDown(int i); -} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/ExistingClusterSUT.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/ExistingClusterSUT.java deleted file mode 100644 index 27d0c97ab6..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/injvm/ExistingClusterSUT.java +++ /dev/null @@ -1,96 +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.harry.sut.injvm; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -import com.google.common.util.concurrent.Uninterruptibles; - -import org.apache.cassandra.distributed.api.ICluster; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.sut.SystemUnderTest; - -public class ExistingClusterSUT implements Configuration.SutConfiguration -{ - private final ICluster cluster; - private final ClusterState clusterState; - - public ExistingClusterSUT(ICluster cluster, ClusterState clusterState) - { - this.cluster = cluster; - this.clusterState = clusterState; - } - - @Override - public SystemUnderTest make() - { - return new SystemUnderTest() - { - int toQuery = 0; - @Override - public boolean isShutdown() - { - return false; - } - - @Override - public void shutdown() - { - } - - @Override - public void schemaChange(String schemaChange) - { - cluster.schemaChange(schemaChange); - } - - @Override - public Object[][] execute(String s, SystemUnderTest.ConsistencyLevel consistencyLevel, Object... objects) - { - Exception lastEx = null; - for (int i = 0; i < 20; i++) - { - toQuery++; - if (cluster.size() == 0) - continue; - int coordinator = (toQuery % cluster.size()) + 1; - if (clusterState.isDown(coordinator)) - continue; - try - { - return cluster.coordinator(coordinator).execute(s, org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM, objects); - } - catch (Exception e) - { - lastEx = e; - Uninterruptibles.sleepUninterruptibly(50, TimeUnit.MILLISECONDS); - } - } - throw new RuntimeException(lastEx); - } - - @Override - public CompletableFuture executeAsync(String s, SystemUnderTest.ConsistencyLevel consistencyLevel, Object... objects) - { - return CompletableFuture.supplyAsync(() -> this.execute(s, consistencyLevel, objects)); - } - }; - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJVMTokenAwareVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJVMTokenAwareVisitExecutor.java deleted file mode 100644 index 5db0ee795d..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJVMTokenAwareVisitExecutor.java +++ /dev/null @@ -1,165 +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.harry.sut.injvm; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import com.google.common.util.concurrent.Uninterruptibles; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.distributed.api.ICoordinator; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.util.ByteUtils; -import org.apache.cassandra.harry.util.TokenUtil; -import org.apache.cassandra.harry.visitors.GeneratingVisitor; -import org.apache.cassandra.harry.visitors.LoggingVisitor; -import org.apache.cassandra.harry.visitors.OperationExecutor; -import org.apache.cassandra.harry.visitors.VisitExecutor; -import org.apache.cassandra.harry.visitors.Visitor; -import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.distributed.api.IInstance; - -import static org.apache.cassandra.harry.sut.TokenPlacementModel.peerStateToNodes; - -public class InJVMTokenAwareVisitExecutor extends LoggingVisitor.LoggingVisitorExecutor -{ - private static final Logger logger = LoggerFactory.getLogger(InJVMTokenAwareVisitExecutor.class); - - private final InJvmSut sut; - private final TokenPlacementModel.ReplicationFactor rf; - private final SystemUnderTest.ConsistencyLevel cl; - private final SchemaSpec schema; - private final int MAX_RETRIES = 10; - - public static Function factory(OperationExecutor.RowVisitorFactory rowVisitorFactory, - SystemUnderTest.ConsistencyLevel cl, - TokenPlacementModel.ReplicationFactor rf) - { - return (run) -> new InJVMTokenAwareVisitExecutor(run, rowVisitorFactory, cl, rf); - } - - public InJVMTokenAwareVisitExecutor(Run run, - OperationExecutor.RowVisitorFactory rowVisitorFactory, - SystemUnderTest.ConsistencyLevel cl, - TokenPlacementModel.ReplicationFactor rf) - { - super(run, rowVisitorFactory.make(run)); - this.sut = (InJvmSut) run.sut; - this.schema = run.schemaSpec; - this.cl = cl; - this.rf = rf; - } - - @Override - protected Object[][] executeWithRetries(long lts, long pd, CompiledStatement statement) - { - if (sut.isShutdown()) - throw new IllegalStateException("System under test is shut down"); - - int retries = 0; - - Object[] pk = schema.inflatePartitionKey(pd); - List replicas = getRing().replicasFor(TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(pk)))); - while (retries++ < MAX_RETRIES) - { - try - { - TokenPlacementModel.Replica replica = replicas.get((int) (lts % replicas.size())); - if (cl == SystemUnderTest.ConsistencyLevel.NODE_LOCAL) - { - return executeNodeLocal(statement.cql(), replica.node(), statement.bindings()); - } - else - { - return sut.cluster - .stream() - .filter((n) -> n.config().broadcastAddress().toString().contains(replica.node().id())) - .findFirst() - .get() - .coordinator() - .execute(statement.cql(), InJvmSut.toApiCl(cl), statement.bindings()); - } - } - catch (Throwable t) - { - int delaySecs = 1; - logger.error(String.format("Caught message while trying to execute: %s. Scheduled to retry in %s seconds", statement, delaySecs), t); - Uninterruptibles.sleepUninterruptibly(delaySecs, TimeUnit.SECONDS); - } - } - throw new IllegalStateException(String.format("Can not execute statement %s after %d retries", statement, retries)); - } - - protected TokenPlacementModel.ReplicatedRanges getRing() - { - ICoordinator coordinator = sut.firstAlive().coordinator(); - List other = peerStateToNodes(coordinator.execute("select peer, tokens, data_center, rack from system.peers", ConsistencyLevel.ONE)); - List self = peerStateToNodes(coordinator.execute("select broadcast_address, tokens, data_center, rack from system.local", ConsistencyLevel.ONE)); - List all = new ArrayList<>(); - all.addAll(self); - all.addAll(other); - all.sort(TokenPlacementModel.Node::compareTo); - return rf.replicate(all); - } - - protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) - { - IInstance instance = sut.cluster - .stream() - .filter((n) -> n.config().broadcastAddress().toString().contains(node.id())) - .findFirst() - .get(); - return instance.executeInternal(statement, bindings); - } - - - @JsonTypeName("in_jvm_token_aware") - public static class Configuration implements org.apache.cassandra.harry.core.Configuration.VisitorConfiguration - { - public final org.apache.cassandra.harry.core.Configuration.RowVisitorConfiguration row_visitor; - public final SystemUnderTest.ConsistencyLevel consistency_level; - public final int rf; - @JsonCreator - public Configuration(@JsonProperty("row_visitor") org.apache.cassandra.harry.core.Configuration.RowVisitorConfiguration rowVisitor, - @JsonProperty("consistency_level") SystemUnderTest.ConsistencyLevel consistencyLevel, - @JsonProperty("rf") int rf) - { - this.row_visitor = rowVisitor; - this.consistency_level = consistencyLevel; - this.rf = rf; - } - - @Override - public Visitor make(Run run) - { - return new GeneratingVisitor(run, new InJVMTokenAwareVisitExecutor(run, row_visitor, consistency_level, new TokenPlacementModel.SimpleReplicationFactor(rf))); - } - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSut.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSut.java deleted file mode 100644 index 16f5e13436..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSut.java +++ /dev/null @@ -1,117 +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.harry.sut.injvm; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.IInstanceConfig; -import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.tcm.ClusterMetadata; - -public class InJvmSut extends InJvmSutBase -{ - private static final Logger logger = LoggerFactory.getLogger(InJvmSut.class); - - public InJvmSut(Cluster cluster) - { - super(cluster, roundRobin(cluster), retryOnTimeout(), 10); - } - - public InJvmSut(Cluster cluster, Supplier loadBalancingStrategy, Function retryStrategy) - { - super(cluster, loadBalancingStrategy, retryStrategy, 10); - } - - @JsonTypeName("in_jvm") - public static class InJvmSutConfiguration extends InJvmSutBaseConfiguration - { - @JsonCreator - public InJvmSutConfiguration(@JsonProperty(value = "nodes", defaultValue = "3") int nodes, - @JsonProperty(value = "worker_threads", defaultValue = "10") int worker_threads, - @JsonProperty("root") String root) - { - super(nodes, worker_threads, root); - } - - protected Cluster cluster(Consumer cfg, int nodes, File root) - { - try - { - return Cluster.build().withConfig(cfg) - .withNodes(nodes) - .withRoot(root) - .createWithoutStarting(); - } - catch (IOException e) - { - throw new IllegalStateException(e); - } - } - - protected InJvmSutBase sut(Cluster cluster) - { - return new InJvmSut(cluster); - } - } - - // TODO: this would only return _read_ (or natural) replicas for the token - public int[] getReadReplicasFor(Object[] partitionKey, String keyspace, String table) - { - return cluster.get(1).appliesOnInstance(InJvmSut::getReadReplicasForCallable).apply(partitionKey, keyspace, table); - } - - public static int[] getReadReplicasForCallable(Object[] pk, String ks, String table) - { - String pkString = Arrays.stream(pk).map(Object::toString).collect(Collectors.joining(":")); - EndpointsForToken endpoints = StorageService.instance.getNaturalReplicasForToken(ks, table, pkString); - int[] nodes = new int[endpoints.size()]; - for (int i = 0; i < endpoints.size(); i++) - nodes[i] = endpoints.get(i).endpoint().getAddress().getAddress()[3]; - - sanity_check: - { - Keyspace ksp = Keyspace.open(ks); - Token token = DatabaseDescriptor.getPartitioner().getToken(ksp.getMetadata().getTableOrViewNullable(table).partitionKeyType.fromString(pkString)); - - ClusterMetadata metadata = ClusterMetadata.current(); - EndpointsForToken replicas = metadata.placements.get(ksp.getMetadata().params.replication).reads.forToken(token).get(); - - assert replicas.endpoints().equals(endpoints.endpoints()) : String.format("Consistent metadata endpoints %s disagree with token metadata computation %s", endpoints.endpoints(), replicas.endpoints()); - } - return nodes; - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSutBase.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSutBase.java deleted file mode 100644 index 4c19e67294..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSutBase.java +++ /dev/null @@ -1,331 +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.harry.sut.injvm; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Arrays; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import com.google.common.collect.Iterators; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.cassandra.distributed.api.ICluster; -import org.apache.cassandra.distributed.api.IInstance; -import org.apache.cassandra.distributed.api.IInstanceConfig; -import org.apache.cassandra.distributed.api.IMessage; -import org.apache.cassandra.distributed.api.IMessageFilters; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.sut.SystemUnderTest; - -public class InJvmSutBase> implements SystemUnderTest.FaultInjectingSut -{ - private static final Logger logger = LoggerFactory.getLogger(InJvmSutBase.class); - - private final ExecutorService executor; - public final CLUSTER cluster; - private final AtomicBoolean isShutdown = new AtomicBoolean(false); - private final Supplier loadBalancingStrategy; - private final Function retryStrategy; - - public InJvmSutBase(CLUSTER cluster) - { - this(cluster, roundRobin(cluster), retryOnTimeout(), 10); - } - - public InJvmSutBase(CLUSTER cluster, Supplier loadBalancingStrategy, Function retryStrategy, int threads) - { - this.cluster = cluster; - this.executor = Executors.newFixedThreadPool(threads); - this.loadBalancingStrategy = loadBalancingStrategy; - this.retryStrategy = retryStrategy; - } - - public static Supplier roundRobin(ICluster cluster) - { - return new Supplier() - { - private final AtomicLong cnt = new AtomicLong(); - - public Integer get() - { - for (int i = 0; i < 42; i++) - { - int selected = (int) (cnt.getAndIncrement() % cluster.size() + 1); - if (!cluster.get(selected).isShutdown()) - return selected; - } - throw new IllegalStateException("Unable to find an alive instance"); - } - }; - } - - public static Function retryOnTimeout() - { - return new Function() - { - public Boolean apply(Throwable t) - { - return t.getMessage().contains("timed out"); - } - }; - } - public CLUSTER cluster() - { - return cluster; - } - - @Override - public boolean isShutdown() - { - return isShutdown.get(); - } - - @Override - public void shutdown() - { - assert isShutdown.compareAndSet(false, true); - - try - { - cluster.close(); - executor.shutdown(); - if (!executor.awaitTermination(30, TimeUnit.SECONDS)) - throw new TimeoutException("Could not terminate cluster within expected timeout"); - } - catch (Throwable e) - { - logger.error("Could not terminate cluster.", e); - throw new RuntimeException(e); - } - } - - public void schemaChange(String statement) - { - cluster.schemaChange(statement); - } - - public IInstance firstAlive() - { - return cluster.stream().filter(i -> !i.isShutdown()).findFirst().get(); - } - - public Object[][] execute(String statement, ConsistencyLevel cl, int pageSize, Object... bindings) - { - return execute(statement, cl, loadBalancingStrategy.get(), pageSize, bindings); - } - - public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) - { - return execute(statement, cl, loadBalancingStrategy.get(), 1, bindings); - } - - public Object[][] execute(String statement, ConsistencyLevel cl, int coordinator, int pageSize, Object... bindings) - { - if (isShutdown.get()) - throw new RuntimeException("Instance is shut down"); - - while (true) - { - try - { - if (cl == ConsistencyLevel.NODE_LOCAL) - { - return cluster.get(coordinator) - .executeInternal(statement, bindings); - } - else if (StringUtils.startsWithIgnoreCase(statement, "SELECT")) - { - return Iterators.toArray(cluster - // round-robin - .coordinator(coordinator) - .executeWithPaging(statement, toApiCl(cl), pageSize, bindings), - Object[].class); - } - else - { - return cluster - // round-robin - .coordinator(coordinator) - .execute(statement, toApiCl(cl), bindings); - } - } - catch (Throwable t) - { - if (retryStrategy.apply(t)) - continue; - - logger.error(String.format("Caught error while trying execute statement %s (%s): %s", - statement, Arrays.toString(bindings), t.getMessage()), - t); - throw t; - } - } - } - - // TODO: Ideally, we need to be able to induce a failure of a single specific message - public Object[][] executeWithWriteFailure(String statement, ConsistencyLevel cl, Object... bindings) - { - if (isShutdown.get()) - throw new RuntimeException("Instance is shut down"); - - try - { - int coordinator = loadBalancingStrategy.get(); - IMessageFilters filters = cluster.filters(); - - // Drop exactly one coordinated message - int MUTATION_REQ = 0; - // TODO: make dropping deterministic - filters.verbs(MUTATION_REQ).from(coordinator).messagesMatching(new IMessageFilters.Matcher() - { - private final AtomicBoolean issued = new AtomicBoolean(); - public boolean matches(int from, int to, IMessage message) - { - if (from != coordinator || message.verb() != MUTATION_REQ) - return false; - - return !issued.getAndSet(true); - } - }).drop().on(); - Object[][] res = cluster - .coordinator(coordinator) - .execute(statement, toApiCl(cl), bindings); - filters.reset(); - return res; - } - catch (Throwable t) - { - logger.error(String.format("Caught error while trying execute statement %s", statement), - t); - throw t; - } - } - - public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) - { - return CompletableFuture.supplyAsync(() -> execute(statement, cl, bindings), executor); - } - - public CompletableFuture executeAsyncWithWriteFailure(String statement, ConsistencyLevel cl, Object... bindings) - { - return CompletableFuture.supplyAsync(() -> executeWithWriteFailure(statement, cl, bindings), executor); - } - - public static abstract class InJvmSutBaseConfiguration> implements Configuration.SutConfiguration - { - public final int nodes; - public final int worker_threads; - public final String root; - - @JsonCreator - public InJvmSutBaseConfiguration(@JsonProperty(value = "nodes", defaultValue = "3") int nodes, - @JsonProperty(value = "worker_threads", defaultValue = "10") int worker_threads, - @JsonProperty("root") String root) - { - this.nodes = nodes; - this.worker_threads = worker_threads; - if (root == null) - { - try - { - this.root = Files.createTempDirectory("cluster_" + nodes + "_nodes").toString(); - } - catch (IOException e) - { - throw new IllegalArgumentException(e); - } - } - else - { - this.root = root; - } - } - - protected abstract CLUSTER cluster(Consumer cfg, int nodes, File root); - protected abstract InJvmSutBase sut(CLUSTER cluster); - - public SystemUnderTest make() - { - try - { - ICluster.setup(); - } - catch (Throwable throwable) - { - throw new RuntimeException(throwable); - } - - CLUSTER cluster; - - cluster = cluster(defaultConfig(), - nodes, - new File(root)); - - cluster.startup(); - return sut(cluster); - } - } - - public static Consumer defaultConfig() - { - return (cfg) -> { - cfg.set("row_cache_size", "50MiB") - .set("index_summary_capacity", "50MiB") - .set("counter_cache_size", "50MiB") - .set("key_cache_size", "50MiB") - .set("file_cache_size", "50MiB") - .set("index_summary_capacity", "50MiB") - .set("memtable_heap_space", "128MiB") - .set("memtable_offheap_space", "128MiB") - .set("memtable_flush_writers", 1) - .set("concurrent_compactors", 1) - .set("concurrent_reads", 5) - .set("concurrent_writes", 5) - .set("compaction_throughput_mb_per_sec", 10) - .set("hinted_handoff_enabled", false); - }; - } - - public static org.apache.cassandra.distributed.api.ConsistencyLevel toApiCl(ConsistencyLevel cl) - { - switch (cl) - { - case ALL: return org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; - case QUORUM: return org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM; - case NODE_LOCAL: return org.apache.cassandra.distributed.api.ConsistencyLevel.NODE_LOCAL; - case ONE: return org.apache.cassandra.distributed.api.ConsistencyLevel.ONE; - } - throw new IllegalArgumentException("Don't know a CL: " + cl); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/QuiescentLocalStateChecker.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/QuiescentLocalStateChecker.java deleted file mode 100644 index f9ee0d0125..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/sut/injvm/QuiescentLocalStateChecker.java +++ /dev/null @@ -1,90 +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.harry.sut.injvm; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.cassandra.distributed.api.ICoordinator; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.model.QuiescentLocalStateCheckerBase; -import org.apache.cassandra.harry.model.reconciler.Reconciler; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.util.ByteUtils; -import org.apache.cassandra.harry.util.TokenUtil; -import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.distributed.api.IInstance; - -public class QuiescentLocalStateChecker extends QuiescentLocalStateCheckerBase -{ - public static ModelFactory factory(TokenPlacementModel.ReplicationFactor rf) - { - return (run) -> new QuiescentLocalStateChecker(run, rf); - } - - public QuiescentLocalStateChecker(Run run, TokenPlacementModel.ReplicationFactor rf) - { - super(run, rf); - } - - public QuiescentLocalStateChecker(OpSelectors.Clock clock, - OpSelectors.PdSelector pdSelector, - SystemUnderTest sut, - DataTracker tracker, - SchemaSpec schema, - Reconciler reconciler, - TokenPlacementModel.ReplicationFactor rf) - { - super(clock, pdSelector, sut, tracker, schema, reconciler, rf); - } - - @Override - protected TokenPlacementModel.ReplicatedRanges getRing() - { - IInstance node = ((InJvmSutBase) sut).firstAlive(); - ICoordinator coordinator = node.coordinator(); - List other = TokenPlacementModel.peerStateToNodes(coordinator.execute("select peer, tokens, data_center, rack from system.peers", ConsistencyLevel.ONE)); - List self = TokenPlacementModel.peerStateToNodes(coordinator.execute("select broadcast_address, tokens, data_center, rack from system.local", ConsistencyLevel.ONE)); - List all = new ArrayList<>(); - all.addAll(self); - all.addAll(other); - all.sort(TokenPlacementModel.Node::compareTo); - return rf.replicate(all); - } - - @Override - protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) - { - IInstance instance = ((InJvmSutBase) sut).cluster - .stream() - .filter((n) -> n.config().broadcastAddress().toString().contains(node.id())) - .findFirst() - .get(); - return instance.executeInternal(statement, bindings); - } - - protected long token(long pd) - { - return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(schema.inflatePartitionKey(pd)))); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderTest.java b/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderTest.java new file mode 100644 index 0000000000..ec864b10e4 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderTest.java @@ -0,0 +1,328 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.test; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.checker.ModelChecker; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.HistoryBuilderHelper; +import org.apache.cassandra.harry.execution.CQLTesterVisitExecutor; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; + +import static org.apache.cassandra.harry.Relations.RelationKind.GTE; +import static org.apache.cassandra.harry.Relations.RelationKind.LTE; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.SingleOperationBuilder.IdxRelation; + +public class HistoryBuilderTest extends CQLTester +{ + // TODO: go through all basic features of History builder here and test them!!! + // TODO: for example, inverse + private static final int STEPS_PER_ITERATION = 1_000; + + private static final Logger logger = LoggerFactory.getLogger(HistoryBuilderTest.class); + + private final Generator simple_schema = rng -> { + return new SchemaSpec(rng.next(), + 1000, + KEYSPACE, + "harry" + rng.nextLong(0, Long.MAX_VALUE), + 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("r1", ColumnSpec.asciiType), + ColumnSpec.regularColumn("r2", ColumnSpec.int64Type), + ColumnSpec.regularColumn("r3", ColumnSpec.asciiType)), + Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType), + ColumnSpec.staticColumn("s2", ColumnSpec.int64Type), + ColumnSpec.staticColumn("s3", ColumnSpec.asciiType))); + }; + + private final Generator simple_schema_with_desc_ck = rng -> { + return new SchemaSpec(rng.next(), + 1000, + KEYSPACE, + "harry" + rng.nextLong(0, Long.MAX_VALUE), + 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("r1", ColumnSpec.asciiType), + ColumnSpec.regularColumn("r2", ColumnSpec.int64Type), + ColumnSpec.regularColumn("r3", ColumnSpec.asciiType)), + Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType), + ColumnSpec.staticColumn("s2", ColumnSpec.int64Type), + ColumnSpec.staticColumn("s3", ColumnSpec.asciiType))); + }; + + @Test + public void orderByTest() + { + withRandom(rng -> { + for (Generator gen : new Generator[]{ simple_schema, simple_schema_with_desc_ck }) + { + SchemaSpec schema = gen.generate(rng); + createTable(schema.compile()); + + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + for (int i = 0; i < 100; i++) + history.insert(1); + + history.custom((lts, opId) -> new Operations.SelectPartition(lts, + history.valueGenerators().pkGen().descriptorAt(1), + Operations.ClusteringOrderBy.DESC)); + + replay(schema, history); + } + }); + } + + @Test + public void historyBuilderInsertTest() + { + withRandom(rng -> { + for (Generator gen : new Generator[]{ simple_schema, simple_schema_with_desc_ck }) + { + SchemaSpec schema = gen.generate(rng); + createTable(schema.compile()); + + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + for (int i = 0; i < 100; i++) + history.insert(1, i, values(i, i, i), values(i, i, i)); + + history.selectPartition(1); + + replay(schema, history); + } + }); + } + + @Test + public void historyBuilderInsertWithUnsetTest() + { + withRandom(rng -> { + for (Generator gen : new Generator[]{ simple_schema, simple_schema_with_desc_ck }) + { + SchemaSpec schema = gen.generate(rng); + createTable(schema.compile()); + + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + for (int i = 0; i < 100; i++) + { + int v = i % 2 == 0 ? MagicConstants.UNSET_IDX : i; + history.insert(1, i, values(v, v, v), values(v, v, v)); + } + + history.selectPartition(1); + + replay(schema, history); + } + }); + } + + @Test + public void historyBuilderFilteringTest() + { + withRandom(rng -> { + for (Generator gen : new Generator[]{ simple_schema, simple_schema_with_desc_ck }) + { + for (boolean useUnset : new boolean[]{ false, true }) + { + SchemaSpec schema = gen.generate(rng); + createTable(schema.compile()); + + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + for (int i = 0; i < 100; i++) + { + int v = (useUnset && i % 2 == 0) ? MagicConstants.UNSET_IDX : i; + history.insert(1, i, values(v, v, v), values(v, v, v)); + } + + history.select(1, + Arrays.asList().toArray(new IdxRelation[0]), + Arrays.asList(new IdxRelation(GTE, 20, 0), + new IdxRelation(LTE, 80, 0), + new IdxRelation(GTE, 30, 1), + new IdxRelation(LTE, 70, 1), + new IdxRelation(GTE, 40, 2), + new IdxRelation(LTE, 60, 2)) + .toArray(new IdxRelation[0]), + Arrays.asList().toArray(new IdxRelation[0])); + + replay(schema, history); + } + } + }); + } + + @Test + public void testSimpleFuzz() + { + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "harry", 100); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + // Generate at most X values, but not more than entropy allows + int maxPartitions = Math.min(1, schema.valueGenerators.pkPopulation()); + int maxPartitionSize = Math.min(100, schema.valueGenerators.ckPopulation()); + + Generator partitionPicker = Generators.pick(0, maxPartitions); + Generator rowPicker = Generators.int32(0, maxPartitionSize); + ModelChecker modelChecker = new ModelChecker<>(); + HistoryBuilder historyBuilder = new HistoryBuilder(schema.valueGenerators); + modelChecker.init(historyBuilder) + .step((history, rng_) -> { + HistoryBuilderHelper.insertRandomData(schema, partitionPicker.generate(rng), rowPicker.generate(rng), rng, history); + history.selectPartition(partitionPicker.generate(rng)); + }) + .step((history, rng_) -> { + history.deleteRow(partitionPicker.generate(rng), rowPicker.generate(rng)); + history.selectPartition(partitionPicker.generate(rng)); + }) + .step((history, rng_) -> { + history.deletePartition(partitionPicker.generate(rng)); + history.selectPartition(partitionPicker.generate(rng)); + }) + .step((history, rng_) -> { + HistoryBuilderHelper.deleteRandomColumns(schema, partitionPicker.generate(rng), rowPicker.generate(rng), rng, history); + history.selectPartition(partitionPicker.generate(rng)); + }) + .step((history, rng_) -> { + history.deleteRowRange(partitionPicker.generate(rng), + rowPicker.generate(rng), + rowPicker.generate(rng), + rng.nextInt(schema.clusteringKeys.size()), + rng.nextBoolean(), + rng.nextBoolean() + ); + history.selectPartition(partitionPicker.generate(rng)); + }) + .step((history, rng_) -> { + history.selectRow(partitionPicker.generate(rng), rowPicker.generate(rng)); + }) + .step((history, rng_) -> { + history.selectRowRange(partitionPicker.generate(rng), + rowPicker.generate(rng), + rowPicker.generate(rng), + rng.nextInt(schema.clusteringKeys.size()), + rng.nextBoolean(), + rng.nextBoolean()); + }) + .step((history, rng_) -> { + history.custom(() -> flush(schema.keyspace, schema.table), "FLUSH"); + }) + .exitCondition((history) -> { + if (historyBuilder.size() < 1000) + return false; + + createTable(schema.compile()); + replay(schema, historyBuilder); + + return true; + }) + .run(STEPS_PER_ITERATION, Long.MAX_VALUE, rng); + }); + } + + @Test + public void fuzzFiltering() + { + Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "fuzz_filtering", 100); + withRandom(rng -> { + SchemaSpec schema = schemaGen.generate(rng); + // Generate at most X values, but not more than entropy allows + int maxPartitions = Math.min(1, schema.valueGenerators.ckPopulation()); + int maxPartitionSize = Math.min(100, schema.valueGenerators.ckPopulation()); + + Generator pkGen = Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), maxPartitionSize)); + Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), maxPartitionSize)); + + ModelChecker modelChecker = new ModelChecker<>(); + HistoryBuilder historyBuilder = new HistoryBuilder(schema.valueGenerators); + + modelChecker.init(historyBuilder) + .step((history, rng_) -> HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history)) + .step((history, rng_) -> { + for (int i = 0; i < 10; i++) + { + List ckRelations = HistoryBuilderHelper.generateClusteringRelations(rng, schema.clusteringKeys.size(), ckGen); + List regularRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.regularColumns.size(), + column -> Math.min(schema.valueGenerators.regularPopulation(column), maxPartitionSize)); + List staticRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.staticColumns.size(), + column -> Math.min(schema.valueGenerators.staticPopulation(column), maxPartitionSize)); + history.select(rng.nextInt(maxPartitions), + ckRelations.toArray(new IdxRelation[0]), + regularRelations.toArray(new IdxRelation[0]), + staticRelations.toArray(new IdxRelation[0])); + } + }) + .exitCondition((history) -> { + if (historyBuilder.size() < 10) + return false; + + createTable(schema.compile()); + replay(schema, historyBuilder); + + return true; + }) + .run(STEPS_PER_ITERATION, Long.MAX_VALUE, rng); + }); + } + + public void replay(SchemaSpec schema, HistoryBuilder historyBuilder) + { + CQLVisitExecutor executor = create(schema, historyBuilder); + for (Visit visit : historyBuilder) + executor.execute(visit); + } + + public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder) + { + DataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, tracker, + new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder), + statement -> { + if (logger.isTraceEnabled()) + logger.trace(statement.toString()); + return execute(statement.cql(), statement.bindings()); + }); + } + + public int[] values(int... values) + { + return values; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/test/QueryBuilderTest.java b/test/harry/main/org/apache/cassandra/harry/test/QueryBuilderTest.java new file mode 100644 index 0000000000..55759019a4 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/QueryBuilderTest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.test; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; + +public class QueryBuilderTest +{ + @Test + public void testQueryBuilder() + { + withRandom(rng -> { + SchemaSpec schemaSpec = SchemaGenerators.trivialSchema("harry", "simplified", 10).generate(rng); + QueryBuildingVisitExecutor queryBuilder = new QueryBuildingVisitExecutor(schemaSpec, (v, q) -> String.format("__START__\n%s\n__END__;", q)); + CompiledStatement compiled = queryBuilder.compile(new Visit(1, + new Operations.Operation[]{ new Operations.SelectPartition(1, 1L) })); + Assert.assertTrue(compiled.cql().contains("SELECT")); + Assert.assertTrue(compiled.cql().contains("__START__")); + Assert.assertTrue(compiled.cql().contains("__END__")); + }); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/test/SimpleBijectionTest.java b/test/harry/main/org/apache/cassandra/harry/test/SimpleBijectionTest.java new file mode 100644 index 0000000000..fde20aa363 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/SimpleBijectionTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.test; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.gen.InvertibleGenerator; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.gen.InvertibleGenerator.MAX_ENTROPY; + +public class SimpleBijectionTest +{ + @Test + public void testOrder() + { + withRandom(rng -> { + for (ColumnSpec.DataType t : ColumnSpec.TYPES) + { + for (ColumnSpec.DataType type : new ColumnSpec.DataType[]{ t, ColumnSpec.ReversedType.cache.get(t) }) + { + ColumnSpec column = (ColumnSpec) ColumnSpec.regularColumn("regular", type); + InvertibleGenerator generator = InvertibleGenerator.fromType(rng,100, column); + + + Object previous = null; + for (int i = 0; i < generator.population(); i++) + { + Object next = generator.inflate(generator.descriptorAt(i)); + if (previous != null) + { + Invariants.checkState(column.type.comparator().compare(next, previous) > 0, + "%s should be > %s", next, previous); + } + previous = next; + } + } + } + }); + } + + @Test + public void testArrayOrder() + { + withRandom(rng -> { + for (boolean[] order : new boolean[][]{ { false, false }, { true, false }, { false, true }, { true, true } }) + { + List> columns = new ArrayList<>(); + for (int i = 0; i < order.length; i++) + columns.add(ColumnSpec.ck("test", ColumnSpec.asciiType, order[i])); + InvertibleGenerator generator = new InvertibleGenerator<>(rng, + MAX_ENTROPY, + 100, + SchemaSpec.forKeys(columns), + (Object[] a, Object[] b) -> HistoryBuilder.compareKeys(columns, a, b)); + Object[] previous = null; + for (int i = 0; i < 100; i++) + { + long descr = generator.descriptorAt(i); + Object[] next = generator.inflate(descr); + if (previous != null) + Assert.assertTrue( HistoryBuilder.compareKeys(columns, next, previous) > 0); + Assert.assertEquals(descr, generator.deflate(next)); + previous = next; + } + } + }); + } + + // TODO (now): negative tests +} diff --git a/test/harry/main/org/apache/cassandra/harry/test/TestStateTest.java b/test/harry/main/org/apache/cassandra/harry/test/TestStateTest.java new file mode 100644 index 0000000000..f63717e7d2 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/TestStateTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.test; + +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Test; + +import static org.apache.cassandra.harry.ColumnSpec.booleanType; +import static org.apache.cassandra.harry.ColumnSpec.int64Type; +import static org.apache.cassandra.harry.ColumnSpec.int8Type; +import static org.apache.cassandra.harry.ColumnSpec.regularColumn; +import static org.apache.cassandra.harry.gen.InvertibleGenerator.MAX_ENTROPY; +import static org.apache.cassandra.harry.SchemaSpec.cumulativeEntropy; + +public class TestStateTest +{ + @Test + public void testCumulativeEntropy() + { + Assert.assertEquals(512, + cumulativeEntropy(Arrays.asList(regularColumn("test", int8Type), + regularColumn("test", booleanType)))); + + Assert.assertEquals(256*256, + cumulativeEntropy(Arrays.asList(regularColumn("test", int8Type), + regularColumn("test", int8Type)))); + + Assert.assertEquals(MAX_ENTROPY, + cumulativeEntropy(Arrays.asList(regularColumn("test", int64Type), + regularColumn("test", int8Type)))); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/tracker/DataTracker.java b/test/harry/main/org/apache/cassandra/harry/tracker/DataTracker.java deleted file mode 100644 index 86c7bca613..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/tracker/DataTracker.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.harry.tracker; - -import java.util.function.LongConsumer; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; - -public interface DataTracker -{ - void onLtsStarted(LongConsumer onLts); - void onLtsFinished(LongConsumer onLts); - - void beginModification(long lts); - void endModification(long lts); - - default void beginValidation(long pd) {} - default void endValidation(long pd) {} - - long maxStarted(); - boolean isFinished(long lts); - - Configuration.DataTrackerConfiguration toConfig(); - - interface DataTrackerFactory - { - DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec); - } - - DataTracker NO_OP = new NoOpDataTracker(); - - class NoOpDataTracker implements DataTracker - { - private NoOpDataTracker() {} - - public void onLtsStarted(LongConsumer onLts){} - public void onLtsFinished(LongConsumer onLts){} - - public void beginModification(long lts){} - public void endModification(long lts){} - - public long maxStarted() { return 0; } - public boolean isFinished(long lts) { return false; } - - public Configuration.DataTrackerConfiguration toConfig(){ return null; } - } - -} diff --git a/test/harry/main/org/apache/cassandra/harry/tracker/DefaultDataTracker.java b/test/harry/main/org/apache/cassandra/harry/tracker/DefaultDataTracker.java deleted file mode 100644 index 2d6eae610a..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/tracker/DefaultDataTracker.java +++ /dev/null @@ -1,193 +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.harry.tracker; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.LongConsumer; - -import org.apache.cassandra.harry.core.Configuration; -import com.google.common.annotations.VisibleForTesting; -import org.apache.cassandra.utils.concurrent.WaitQueue; - -public class DefaultDataTracker implements DataTracker -{ - protected final AtomicLong maxSeenLts; - protected final AtomicLong maxCompleteLts; - protected final PriorityBlockingQueue reorderBuffer; - protected final DrainReorderQueueTask reorderTask; - - protected List onStarted = new ArrayList<>(); - protected List onFinished = new ArrayList<>(); - - public DefaultDataTracker() - { - this.maxSeenLts = new AtomicLong(-1); - this.maxCompleteLts = new AtomicLong(-1); - this.reorderBuffer = new PriorityBlockingQueue<>(100); - this.reorderTask = new DrainReorderQueueTask(); - this.reorderTask.start(); - } - - @Override - public void onLtsStarted(LongConsumer onLts) - { - this.onStarted.add(onLts); - } - - @Override - public void onLtsFinished(LongConsumer onLts) - { - this.onFinished.add(onLts); - } - - @Override - public void beginModification(long lts) - { - assert lts >= 0; - startedInternal(lts); - for (LongConsumer consumer : onStarted) - consumer.accept(lts); - } - - @Override - public void endModification(long lts) - { - finishedInternal(lts); - for (LongConsumer consumer : onFinished) - consumer.accept(lts); - } - - void startedInternal(long lts) - { - recordEvent(lts, false); - } - - void finishedInternal(long lts) - { - recordEvent(lts, true); - } - - private void recordEvent(long lts, boolean finished) - { - // all seen LTS are allowed to be "in-flight" - maxSeenLts.getAndUpdate((old) -> Math.max(lts, old)); - - if (!finished) - return; - - if (!maxCompleteLts.compareAndSet(lts - 1, lts)) - reorderBuffer.offer(lts); - - reorderTask.notify.signalAll(); - } - - private class DrainReorderQueueTask extends Thread - { - private final WaitQueue notify; - - private DrainReorderQueueTask() - { - super("DrainReorderQueueTask"); - this.notify = WaitQueue.newWaitQueue(); - } - - public void run() - { - while (!Thread.interrupted()) - { - try - { - WaitQueue.Signal signal = notify.register(); - runOnce(); - signal.awaitUninterruptibly(); - } - catch (Throwable t) - { - t.printStackTrace(); - } - } - } - - public void runOnce() - { - long maxAchievedConsecutive = maxCompleteLts.get(); - - Long smallest = reorderBuffer.peek(); - while (smallest != null && smallest == maxAchievedConsecutive + 1) - { - boolean res = maxCompleteLts.compareAndSet(maxAchievedConsecutive, smallest); - assert res : String.format("Should have exclusive access to maxCompleteLts, but someone wrote %d, while %d was expected", maxCompleteLts.get(), maxAchievedConsecutive); - maxAchievedConsecutive = smallest; - long removed = reorderBuffer.remove(); - assert smallest == removed : String.format("Tried to remove %d but removed %d", smallest, removed); - smallest = reorderBuffer.peek(); - } - } - } - - - public long maxStarted() - { - return maxSeenLts.get(); - } - - public long maxConsecutiveFinished() - { - return maxCompleteLts.get(); - } - - public boolean isFinished(long lts) - { - // Since we _first_ add the item to maxConsecutive, and only then yank it from reorderBuffer, - // it may happen that we have checked for lts against maxConsecutive while it was still in reorderBuffer - // but then, by the time we check for it in the reorderBuffer, it is already removed; - return reorderBuffer.contains(lts) || lts <= maxConsecutiveFinished(); - } - - public Configuration.DataTrackerConfiguration toConfig() - { - return new Configuration.DefaultDataTrackerConfiguration(maxSeenLts.get(), maxCompleteLts.get(), new ArrayList<>(reorderBuffer)); - } - - @VisibleForTesting - public void forceLts(long maxSeen, long maxComplete, List reorderBuffer) - { - System.out.printf("Forcing maxSeen: %d, maxComplete: %d, reorderBuffer: %s%n", maxSeen, maxComplete, reorderBuffer); - this.maxSeenLts.set(maxSeen); - this.maxCompleteLts.set(maxComplete); - if (reorderBuffer != null) - { - reorderBuffer.sort(Long::compareTo); - this.reorderBuffer.addAll(reorderBuffer); - } - } - - public String toString() - { - List buf = new ArrayList<>(reorderBuffer); - return "DataTracker{" + - "maxSeenLts=" + maxSeenLts + - ", maxCompleteLts=" + maxCompleteLts + - ", reorderBuffer=" + buf + - '}'; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/tracker/LockingDataTracker.java b/test/harry/main/org/apache/cassandra/harry/tracker/LockingDataTracker.java deleted file mode 100644 index 3edcf525f5..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/tracker/LockingDataTracker.java +++ /dev/null @@ -1,286 +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.harry.tracker; - -import java.util.ArrayList; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.utils.concurrent.WaitQueue; - -/** - * Locking data tracker, that can be used with a quiescent model checker while providing - * a high degree of concurrency. It works by isolating readers from writers. In other words, - * readers can intersect with other readers, and writers can coincide with other writers. - * - * We achieve quiescence on a partition level, not on LTS level, and we know for sure - * which operations have finished for a partition, even if their LTS are non-contiguous. - * - * We use a simple wait queue for queuing up waiters, and a compact long counter for - * tracking the number of concurrent readers and writers. Lower 32 bits hold a number of - * readers, and higher 32 bits - a number of writers. - */ -public class LockingDataTracker extends DefaultDataTracker -{ - private final Map locked = new ConcurrentHashMap<>(); - - private final WaitQueue readersQueue = WaitQueue.newWaitQueue(); - private final WaitQueue writersQueue = WaitQueue.newWaitQueue(); - private final OpSelectors.PdSelector pdSelector; - private final SchemaSpec schemaSpec; - - private final Set readingFrom = new ConcurrentSkipListSet<>(); - private final Set writingTo = new ConcurrentSkipListSet<>(); - - public LockingDataTracker(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) - { - this.pdSelector = pdSelector; - this.schemaSpec = schemaSpec; - } - - @Override - public void beginModification(long lts) - { - ReadersWritersLock partitionLock = getLockForLts(lts); - partitionLock.lockForWrite(); - assert !readingFrom.contains(partitionLock.descriptor) : String.format("Reading from should not have contained %d", partitionLock.descriptor); - writingTo.add(partitionLock.descriptor); - super.beginModification(lts); - } - - @Override - public void endModification(long lts) - { - super.endModification(lts); - ReadersWritersLock partitionLock = getLockForLts(lts); - assert !readingFrom.contains(partitionLock.descriptor) : String.format("Reading from should not have contained %d", partitionLock.descriptor); - writingTo.remove(partitionLock.descriptor); - partitionLock.unlockAfterWrite(); - } - - @Override - public void beginValidation(long pd) - { - ReadersWritersLock partitionLock = getLock(pd); - partitionLock.lockForRead(); - assert !writingTo.contains(pd) : String.format("Writing to should not have contained %d", pd); - readingFrom.add(pd); - super.beginValidation(pd); - } - - @Override - public void endValidation(long pd) - { - super.endValidation(pd); - ReadersWritersLock partitionLock = getLock(pd); - assert !writingTo.contains(pd) : String.format("Writing to should not have contained %d", pd); - readingFrom.remove(partitionLock.descriptor); - partitionLock.unlockAfterRead(); - } - - public void validate(long pd, Runnable runnable) - { - ReadersWritersLock partitionLock = getLockForLts(pd); - partitionLock.lockForRead(); - runnable.run(); - partitionLock.unlockAfterRead(); - } - - private ReadersWritersLock getLockForLts(long lts) - { - long pd = pdSelector.pd(lts, schemaSpec); - return getLock(pd); - } - - private ReadersWritersLock getLock(long pd) - { - return locked.computeIfAbsent(pd, (pd_) -> new ReadersWritersLock(readersQueue, writersQueue, pd)); - } - - /** - * Readers/writers lock. It was decided not to use signals here, and instead go for a - * busyspin instead, since we expect locks to be released briefly and contention to be minimal. - */ - public static class ReadersWritersLock - { - private static final AtomicLongFieldUpdater fieldUpdater = AtomicLongFieldUpdater.newUpdater(ReadersWritersLock.class, "lock"); - private volatile long lock; - - final long descriptor; - final WaitQueue readersQueue; - final WaitQueue writersQueue; - - public ReadersWritersLock(WaitQueue readersQueue, WaitQueue writersQueue, long descriptor) - { - this.readersQueue = readersQueue; - this.writersQueue = writersQueue; - this.lock = 0L; - this.descriptor = descriptor; - } - - @Override - public String toString() - { - long lock = this.lock; - return "PartitionLock{" + - "pd = " + descriptor + - ", readers = " + getReaders(lock) + - ", writers = " + getWriters(lock) + - '}'; - } - - public void lockForWrite() - { - while (true) - { - WaitQueue.Signal signal = writersQueue.register(); - long v = lock; - if (getReaders(v) == 0) - { - if (fieldUpdater.compareAndSet(this, v, incWriters(v))) - { - signal.cancel(); - return; - } - } - signal.awaitUninterruptibly(); - } - } - - public void unlockAfterWrite() - { - while (true) - { - long v = lock; - if (fieldUpdater.compareAndSet(this, v, decWriters(v))) - { - readersQueue.signalAll(); - writersQueue.signalAll(); - return; - } - } - } - - public void lockForRead() - { - while (true) - { - WaitQueue.Signal signal = readersQueue.register(); - long v = lock; - if (getWriters(v) == 0) - { - if (fieldUpdater.compareAndSet(this, v, incReaders(v))) - { - signal.cancel(); - return; - } - } - signal.awaitUninterruptibly(); - } - } - - public boolean tryLockForRead() - { - long v = lock; - if (getWriters(v) == 0 && fieldUpdater.compareAndSet(this, v, incReaders(v))) - return true; - - return false; - } - - public void unlockAfterRead() - { - while (true) - { - long v = lock; - if (fieldUpdater.compareAndSet(this, v, decReaders(v))) - { - writersQueue.signalAll(); - readersQueue.signalAll(); - return; - } - } - } - - private long incReaders(long v) - { - long readers = getReaders(v); - assert getWriters(v) == 0; - v &= ~0x00000000ffffffffL; // erase all readers - return v | (readers + 1L); - } - - private long decReaders(long v) - { - long readers = getReaders(v); - assert getWriters(v) == 0; - assert readers >= 1; - v &= ~0x00000000ffffffffL; // erase all readers - return v | (readers - 1L); - } - - private long incWriters(long v) - { - long writers = getWriters(v); - assert getReaders(v) == 0; - v &= ~0xffffffff00000000L; // erase all writers - return v | ((writers + 1L) << 32); - } - - private long decWriters(long v) - { - long writers = getWriters(v); - assert getReaders(v) == 0; - assert writers >= 1 : "Writers left " + writers; - v &= ~0xffffffff00000000L; // erase all writers - return v | ((writers - 1L) << 32); - } - - public int getReaders(long v) - { - v &= 0xffffffffL; - return (int) v; - } - - public int getWriters(long v) - { - v >>= 32; - v &= 0xffffffffL; - return (int) v; - } - } - - @Override - public Configuration.DataTrackerConfiguration toConfig() - { - return new Configuration.LockingDataTrackerConfiguration(maxSeenLts.get(), maxCompleteLts.get(), new ArrayList<>(reorderBuffer)); - } - - @Override - public String toString() - { - return "Locking" + super.toString(); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/ArrayWrapper.java b/test/harry/main/org/apache/cassandra/harry/util/ArrayWrapper.java similarity index 95% rename from test/harry/main/org/apache/cassandra/harry/dsl/ArrayWrapper.java rename to test/harry/main/org/apache/cassandra/harry/util/ArrayWrapper.java index bddc9ade1f..69c29b7676 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/ArrayWrapper.java +++ b/test/harry/main/org/apache/cassandra/harry/util/ArrayWrapper.java @@ -16,14 +16,14 @@ * limitations under the License. */ -package org.apache.cassandra.harry.dsl; +package org.apache.cassandra.harry.util; import java.util.Arrays; /** * A small wrapper to allow object arrays to be used as keys in maps. */ -class ArrayWrapper +public class ArrayWrapper { private final Object[] objects; diff --git a/test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java b/test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java index 86195421d5..4ebf7e9d57 100644 --- a/test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java +++ b/test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java @@ -18,14 +18,19 @@ package org.apache.cassandra.harry.util; +import java.math.BigDecimal; +import java.math.BigInteger; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Date; import java.util.List; import java.util.Set; import java.util.UUID; +import org.apache.cassandra.utils.ByteBufferUtil; + public class ByteUtils { public static ByteBuffer bytes(String s) @@ -78,6 +83,35 @@ public class ByteUtils return ByteBuffer.wrap(decompose(uuid)); } + public static ByteBuffer bytes(Date date) + { + return ByteBufferUtil.bytes(date.getTime()); + } + + public static ByteBuffer bytes(BigInteger value) + { + if (value == null) + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + + return ByteBuffer.wrap(value.toByteArray()); + } + + public static ByteBuffer bytes(BigDecimal value) + { + if (value == null) + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + + BigInteger bi = value.unscaledValue(); + int scale = value.scale(); + byte[] bibytes = bi.toByteArray(); + + ByteBuffer bytes = ByteBuffer.allocate(4 + bibytes.length); + bytes.putInt(scale); + bytes.put(bibytes); + bytes.rewind(); + return bytes; + } + public static byte[] decompose(UUID uuid) { long most = uuid.getMostSignificantBits(); @@ -121,6 +155,12 @@ public class ByteUtils return bytes((InetAddress) obj); else if (obj instanceof String) return bytes((String) obj); + else if (obj instanceof Date) + return bytes((Date) obj); + else if (obj instanceof BigInteger) + return bytes((BigInteger) obj); + else if (obj instanceof BigDecimal) + return bytes((BigDecimal) obj); else if (obj instanceof List) { throw new UnsupportedOperationException("Please use ByteUtils from integration package"); diff --git a/test/harry/main/org/apache/cassandra/harry/util/DescriptorRanges.java b/test/harry/main/org/apache/cassandra/harry/util/DescriptorRanges.java deleted file mode 100644 index 597d7e8bad..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/util/DescriptorRanges.java +++ /dev/null @@ -1,150 +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.harry.util; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -// TODO: this is not really an interval tree, just two sorted arrays. However, given that ExhaustiveChecker has -// to inflate a partition every time we execute the query, we always know all boundaries at any given point in time. -public class DescriptorRanges -{ - private List sortedByMin; - - public DescriptorRanges(List ranges) - { - this.sortedByMin = new ArrayList<>(ranges); - Collections.sort(sortedByMin, Comparator.comparingLong(a -> a.minBound)); - } - - public boolean isShadowed(long cd, long lts) - { - return !shadowedBy(cd, lts).isEmpty(); - } - - public List shadowedBy(long cd, long lts) - { - List shadowedBy = new ArrayList<>(); - for (DescriptorRange range : sortedByMin) - { - if (range.minBound > cd) - break; - - if (range.contains(cd, lts)) - shadowedBy.add(range); - } - - return shadowedBy; - } - - public List newerThan(long ts) - { - return sortedByMin.stream().filter((rt) -> { - return rt.timestamp >= ts; - }).collect(Collectors.toList()); - } - - - private static int toIdx(int idxOrIP) - { - if (idxOrIP >= 0) - return idxOrIP; - - return -1 * (idxOrIP + 1); - } - - public static class DescriptorRange - { - public final long minBound; - public final long maxBound; - public final boolean minInclusive; - public final boolean maxInclusive; - - public final long timestamp; - - public DescriptorRange(long minBound, long maxBound, boolean minInclusive, boolean maxInclusive, long timestamp) - { - this.minBound = minBound; - this.maxBound = maxBound; - this.minInclusive = minInclusive; - this.maxInclusive = maxInclusive; - - this.timestamp = timestamp; - } - - public boolean contains(long descriptor) - { - if (minInclusive && maxInclusive) - return descriptor >= minBound && descriptor <= maxBound; - - if (!minInclusive && !maxInclusive) - return descriptor > minBound && descriptor < maxBound; - - if (!minInclusive && maxInclusive) - return descriptor > minBound && descriptor <= maxBound; - - assert (minInclusive && !maxInclusive); - return descriptor >= minBound && descriptor < maxBound; - } - - public boolean contains(long descriptor, long ts) - { - return contains(descriptor) && timestamp >= ts; - } - - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DescriptorRange range = (DescriptorRange) o; - return minBound == range.minBound && - maxBound == range.maxBound && - minInclusive == range.minInclusive && - maxInclusive == range.maxInclusive && - timestamp == range.timestamp; - } - - public int hashCode() - { - return Objects.hash(minBound, maxBound, minInclusive, maxInclusive, timestamp); - } - - public String toString() - { - return "Range{" + - "minBound=" + minBound + - ", maxBound=" + maxBound + - ", minInclusive=" + minInclusive + - ", maxInclusive=" + maxInclusive + - ", timestamp=" + timestamp + - '}'; - } - } - - public String toString() - { - return "Ranges{" + - "sortedByMin=" + sortedByMin + - '}'; - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/util/IteratorsUtil.java b/test/harry/main/org/apache/cassandra/harry/util/IteratorsUtil.java new file mode 100644 index 0000000000..72984b30fa --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/IteratorsUtil.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.util; + +import java.util.Iterator; + +public class IteratorsUtil +{ + public static Iterable concat(Iterable... iterables) + { + assert iterables != null && iterables.length > 0; + if (iterables.length == 1) + return iterables[0]; + + return () -> { + return new Iterator() + { + int idx; + Iterator current; + boolean hasNext; + + { + idx = 0; + prepareNext(); + } + + private void prepareNext() + { + if (current != null && current.hasNext()) + { + hasNext = true; + return; + } + + while (idx < iterables.length) + { + current = iterables[idx].iterator(); + idx++; + if (current.hasNext()) + { + hasNext = true; + return; + } + } + + hasNext = false; + } + + public boolean hasNext() + { + return hasNext; + } + + public T next() + { + T next = current.next(); + prepareNext(); + return next; + } + }; + }; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/util/StringUtils.java b/test/harry/main/org/apache/cassandra/harry/util/StringUtils.java index 956b3a34bf..f6571c4478 100644 --- a/test/harry/main/org/apache/cassandra/harry/util/StringUtils.java +++ b/test/harry/main/org/apache/cassandra/harry/util/StringUtils.java @@ -18,18 +18,57 @@ package org.apache.cassandra.harry.util; +import org.apache.cassandra.harry.MagicConstants; + public class StringUtils { public static String toString(long[] arr) { - String s = ""; + if (arr.length == 0) + return "EMPTY"; + StringBuilder s = new StringBuilder(); for (int i = 0; i < arr.length; i++) { - s += arr[i]; - s += "L"; + if (arr[i] == MagicConstants.UNSET_DESCR) + s.append("UNSET"); + else if (arr[i] == MagicConstants.UNKNOWN_DESCR) + s.append("UNKNOWN"); + else if (arr[i] == MagicConstants.NIL_DESCR) + s.append("NIL"); + else + { + s.append(arr[i]); + s.append("L"); + } if (i < (arr.length - 1)) - s += ','; + s.append(','); } - return s; + return s.toString(); + } + + public static String toString(int[] arr) + { + if (arr.length == 0) + return "EMPTY"; + StringBuilder s = new StringBuilder(); + for (int i = 0; i < arr.length; i++) + { + s.append(toString(arr[i])); + if (i < (arr.length - 1)) + s.append(','); + } + return s.toString(); + } + + public static String toString(int idx) + { + if (idx == MagicConstants.UNSET_IDX) + return "UNSET"; + else if (idx == MagicConstants.UNKNOWN_IDX) + return "UNKNOWN"; + else if (idx == MagicConstants.NIL_IDX) + return "NIL"; + else + return Integer.toString(idx); } } diff --git a/test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java b/test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java index 0927217078..c75bc0dd7f 100644 --- a/test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java +++ b/test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java @@ -21,10 +21,10 @@ package org.apache.cassandra.harry.util; public interface ThrowingRunnable { void run() throws Throwable; - static Runnable toRunnable(ThrowingRunnable runnable) { + default Runnable toRunnable() { return () -> { try { - runnable.run(); + this.run(); } catch (Throwable var2) { throw new RuntimeException(var2); } diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/AllPartitionsValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/AllPartitionsValidator.java deleted file mode 100644 index 8a44b6a2a8..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/AllPartitionsValidator.java +++ /dev/null @@ -1,156 +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.harry.visitors; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -import org.junit.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.ExecutorFactory; -import org.apache.cassandra.concurrent.InfiniteLoopExecutor; -import org.apache.cassandra.concurrent.Interruptible; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.MetricReporter; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.runner.Runner; -import org.apache.cassandra.utils.concurrent.WaitQueue; - -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED; -import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; - -/** - * Concurrently validates all partitions that were visited during this run. - */ -public class AllPartitionsValidator implements Visitor -{ - private static final Logger logger = LoggerFactory.getLogger(AllPartitionsValidator.class); - - protected final Model model; - protected final SchemaSpec schema; - protected final QueryLogger queryLogger; - protected final OpSelectors.Clock clock; - protected final OpSelectors.PdSelector pdSelector; - protected final MetricReporter metricReporter; - protected final SystemUnderTest sut; - protected final DataTracker tracker; - - protected final int concurrency; - - public static Configuration.VisitorConfiguration factoryForTests(int concurrency, - Model.ModelFactory modelFactory) - { - return (r) -> new AllPartitionsValidator(r, concurrency, modelFactory, QueryLogger.NO_OP); - } - - public AllPartitionsValidator(Run run, - int concurrency, - Model.ModelFactory modelFactory) - { - this(run, concurrency, modelFactory, QueryLogger.NO_OP); - } - - public AllPartitionsValidator(Run run, - int concurrency, - Model.ModelFactory modelFactory, - QueryLogger logger) - { - this.metricReporter = run.metricReporter; - this.model = modelFactory.make(run); - this.schema = run.schemaSpec; - this.clock = run.clock; - this.sut = run.sut; - this.pdSelector = run.pdSelector; - this.concurrency = concurrency; - this.tracker = run.tracker; - this.queryLogger = logger; - } - - protected void validateAllPartitions() throws Throwable - { - List threads = new ArrayList<>(); - WaitQueue queue = WaitQueue.newWaitQueue(); - WaitQueue.Signal interrupt = queue.register(); - List errors = new CopyOnWriteArrayList<>(); - - final long maxPosition = pdSelector.maxPosition(tracker.maxStarted()); - - AtomicLong currentPosition = new AtomicLong(); - for (int i = 0; i < concurrency; i++) - { - Interruptible thread = ExecutorFactory.Global.executorFactory().infiniteLoop(String.format("AllPartitionsValidator-%d", i + 1), - Runner.wrapInterrupt((state) -> { - if (state == Interruptible.State.NORMAL) - { - metricReporter.validatePartition(); - long pos = currentPosition.getAndIncrement(); - if (pos < maxPosition) - { - for (boolean reverse : new boolean[]{ true, false }) - { - Query query = Query.selectAllColumns(schema, pdSelector.pd(pdSelector.minLtsAt(pos), schema), reverse); - model.validate(query); - queryLogger.logSelectQuery((int)pos, query); - } - } - else - { - interrupt.signalAll(); - } - } - }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); - threads.add(thread); - } - - interrupt.awaitUninterruptibly(); - - for (Interruptible thread : threads) - { - ((InfiniteLoopExecutor)thread).shutdown(false); - Assert.assertTrue(thread.awaitTermination(1, TimeUnit.MINUTES)); - } - - if (!errors.isEmpty()) - Runner.mergeAndThrow(errors); - } - - public void visit() - { - try - { - validateAllPartitions(); - } - catch (Throwable e) - { - throw new RuntimeException(e); - } - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/CorruptingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/CorruptingVisitor.java deleted file mode 100644 index 043a0804d9..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/CorruptingVisitor.java +++ /dev/null @@ -1,89 +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.harry.visitors; - -import java.util.Random; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.corruptor.AddExtraRowCorruptor; -import org.apache.cassandra.harry.corruptor.ChangeValueCorruptor; -import org.apache.cassandra.harry.corruptor.HideRowCorruptor; -import org.apache.cassandra.harry.corruptor.HideValueCorruptor; -import org.apache.cassandra.harry.corruptor.QueryResponseCorruptor; -import org.apache.cassandra.harry.runner.HarryRunner; -import org.apache.cassandra.harry.operations.Query; - -public class CorruptingVisitor implements Visitor -{ - public static final Logger logger = LoggerFactory.getLogger(HarryRunner.class); - - private final Run run; - private final QueryResponseCorruptor[] corruptors; - private final int triggerAfter; - - public CorruptingVisitor(int triggerAfter, - Run run) - { - this.run = run; - this.triggerAfter = triggerAfter; - - this.corruptors = new QueryResponseCorruptor[]{ - new QueryResponseCorruptor.SimpleQueryResponseCorruptor(run.schemaSpec, - run.clock, - HideRowCorruptor::new), - new AddExtraRowCorruptor(run.schemaSpec, - run.clock, - run.tracker, - run.descriptorSelector), - new QueryResponseCorruptor.SimpleQueryResponseCorruptor(run.schemaSpec, - run.clock, - HideValueCorruptor::new), - new QueryResponseCorruptor.SimpleQueryResponseCorruptor(run.schemaSpec, - run.clock, - ChangeValueCorruptor::new) - }; - } - - public void visit() - { - long lts = run.clock.peek(); - - if (lts > triggerAfter) - return; - - // TODO: switch to a better entropy source - Random random = new Random(1); - - QueryResponseCorruptor corruptor = corruptors[random.nextInt(corruptors.length)]; - long maxPos = run.pdSelector.maxPosition(run.tracker.maxStarted()); - long pd = run.pdSelector.pd(random.nextInt((int) maxPos), run.schemaSpec); - try - { - boolean success = corruptor.maybeCorrupt(Query.selectAllColumns(run.schemaSpec, pd, false), - run.sut); - logger.info("{} tried to corrupt a partition with a pd {}@{} my means of {}", success ? "Successfully" : "Unsuccessfully", pd, lts, corruptor.getClass()); - } - catch (Throwable t) - { - logger.error("Caught an exception while trying to corrupt a partition.", t); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/DoubleWriteVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/visitors/DoubleWriteVisitExecutor.java deleted file mode 100644 index 06bc1d7458..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/DoubleWriteVisitExecutor.java +++ /dev/null @@ -1,56 +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.harry.visitors; - -public class DoubleWriteVisitExecutor extends VisitExecutor -{ - private final VisitExecutor delegate1; - private final VisitExecutor delegate2; - - public DoubleWriteVisitExecutor(VisitExecutor delegate1, - VisitExecutor delegate2) - { - this.delegate1 = delegate1; - this.delegate2 = delegate2; - } - - protected void beforeLts(long lts, long pd) - { - delegate1.beforeLts(lts, pd); - delegate2.beforeLts(lts, pd); - } - - protected void afterLts(long lts, long pd) - { - delegate1.afterLts(lts, pd); - delegate2.afterLts(lts, pd); - } - - protected void operation(Operation operation) - { - delegate1.operation(operation); - delegate2.operation(operation); - } - - public void shutdown() throws InterruptedException - { - delegate1.shutdown(); - delegate2.shutdown(); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/FaultInjectingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/FaultInjectingVisitor.java deleted file mode 100644 index 428c699266..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/FaultInjectingVisitor.java +++ /dev/null @@ -1,97 +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.harry.visitors; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; - -/** - * Fault injecting visitor: randomly fails some of the queries. - * - * Requires {@code FaultInjectingSut} to function. - */ -public class FaultInjectingVisitor extends LoggingVisitor -{ - private final AtomicInteger cnt = new AtomicInteger(); - - private final SystemUnderTest.FaultInjectingSut sut; - protected final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); - - public FaultInjectingVisitor(Run run, OperationExecutor.RowVisitorFactory rowVisitorFactory) - { - super(run, rowVisitorFactory); - this.sut = (SystemUnderTest.FaultInjectingSut) run.sut; - } - - void executeAsyncWithRetries(CompletableFuture originator, CompiledStatement statement) - { - executeAsyncWithRetries(originator, statement, true); - } - - void executeAsyncWithRetries(CompletableFuture originator, CompiledStatement statement, boolean allowFailures) - { - if (sut.isShutdown()) - throw new IllegalStateException("System under test is shut down"); - - CompletableFuture future; - if (allowFailures && cnt.getAndIncrement() % 2 == 0) - { - future = sut.executeAsyncWithWriteFailure(statement.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, statement.bindings()); - } - else - { - future = sut.executeAsync(statement.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, statement.bindings()); - } - - future.whenComplete((res, t) -> { - if (t != null) - executor.schedule(() -> executeAsyncWithRetries(originator, statement, false), 1, TimeUnit.SECONDS); - else - originator.complete(res); - }); - } - - @JsonTypeName("fault_injecting") - public static class FaultInjectingVisitorConfiguration extends Configuration.MutatingVisitorConfiguation - { - @JsonCreator - public FaultInjectingVisitorConfiguration(@JsonProperty("row_visitor") Configuration.RowVisitorConfiguration row_visitor) - { - super(row_visitor); - } - - @Override - public Visitor make(Run run) - { - return new FaultInjectingVisitor(run, row_visitor); - } - } - -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/GeneratingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/GeneratingVisitor.java deleted file mode 100644 index 3cda4179d5..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/GeneratingVisitor.java +++ /dev/null @@ -1,232 +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.harry.visitors; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; -import org.apache.cassandra.harry.util.BitSet; - -public class GeneratingVisitor extends LtsVisitor -{ - private final OpSelectors.PdSelector pdSelector; - private final OpSelectors.DescriptorSelector descriptorSelector; - private final QueryGenerator rangeSelector; - private final SchemaSpec schema; - - public GeneratingVisitor(Run run, - VisitExecutor delegate) - { - super(delegate, run.clock::nextLts); - - this.pdSelector = run.pdSelector; - this.descriptorSelector = run.descriptorSelector; - this.schema = run.schemaSpec; - this.rangeSelector = run.rangeSelector; - } - - @Override - public void visit(long lts) - { - generate(lts, pdSelector.pd(lts, schema)); - } - - private void generate(long lts, long pd) - { - beforeLts(lts, pd); - int opsPerLts = descriptorSelector.operationsPerLts(lts); - for (long opId = 0; opId < opsPerLts; opId++) - { - OpSelectors.OperationKind kind = descriptorSelector.operationType(pd, lts, opId); - BaseOperation operation; - switch (kind) - { - case INSERT: - case UPDATE: - operation = writeOp(lts, pd, opId, kind); - break; - case INSERT_WITH_STATICS: - case UPDATE_WITH_STATICS: - operation = writeRegularAndStatic(lts, pd, opId, kind); - break; - case DELETE_ROW: - { - long cd = descriptorSelector.cd(pd, lts, opId, schema); - operation = new GeneratedDeleteRowOp(lts, pd, cd, opId, kind); - break; - } - case DELETE_COLUMN: - case DELETE_COLUMN_WITH_STATICS: - { - long cd = descriptorSelector.cd(pd, lts, opId, schema); - BitSet columns = descriptorSelector.columnMask(pd, lts, opId, kind); - operation = new GeneratedDeleteColumnsOp(lts, pd, cd, opId, kind, columns); - break; - } - case DELETE_PARTITION: - case DELETE_RANGE: - case DELETE_SLICE: - operation = new GeneratedDeleteOp(lts, pd, opId, kind, rangeSelector); - break; - default: - throw new IllegalStateException("All cases are covered but not " + kind); - } - operation(operation); - } - afterLts(lts, pd); - } - - public BaseOperation writeOp(long lts, long pd, long opId, OpSelectors.OperationKind kind) - { - long cd = descriptorSelector.cd(pd, lts, opId, schema); - - return new GeneratedWriteOp(lts, pd, cd, opId, kind) - { - @Override - public long[] vds() - { - return descriptorSelector.vds(pd, cd(), lts, opId, kind(), schema); - } - }; - } - - public BaseOperation writeRegularAndStatic(long lts, long pd, long opId, OpSelectors.OperationKind kind) - { - long cd = descriptorSelector.cd(pd, lts, opId, schema); - - return new GeneratedWriteWithStaticOp(lts, pd, cd, opId, kind) - { - public long[] sds() - { - return descriptorSelector.sds(pd, cd(), lts, opId, kind(), schema); - } - - @Override - public long[] vds() - { - return descriptorSelector.vds(pd, cd(), lts, opId, kind(), schema); - } - }; - } - - public abstract static class GeneratedWriteOp extends BaseOperation implements ReplayingVisitor.WriteOp - { - protected final long cd; - public GeneratedWriteOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind) - { - super(lts, pd, opId, kind); - this.cd = cd; - } - - @Override - public long cd() - { - return cd; - } - } - - public abstract static class GeneratedWriteWithStaticOp extends GeneratedWriteOp implements ReplayingVisitor.WriteStaticOp - { - public GeneratedWriteWithStaticOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind) - { - super(lts, pd, cd, opId, kind); - } - } - - public static class GeneratedDeleteRowOp extends BaseOperation implements ReplayingVisitor.DeleteRowOp - { - private final long cd; - public GeneratedDeleteRowOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind) - { - super(lts, pd, opId, kind); - this.cd = cd; - } - - @Override - public long cd() - { - return cd; - } - } - - public static class GeneratedDeleteOp extends BaseOperation implements ReplayingVisitor.DeleteOp - { - private final Query relations; - - public GeneratedDeleteOp(long lts, long pd, long opId, OpSelectors.OperationKind kind, QueryGenerator queryGenerator) - { - this(lts, pd, opId, kind, queryGenerator.inflate( lts, opId, queryKind(kind))); - } - - public GeneratedDeleteOp(long lts, long pd, long opId, OpSelectors.OperationKind kind, Query relations) - { - super(lts, pd, opId, kind); - this.relations = relations; - } - - @Override - public Query relations() - { - return relations; - } - - protected static Query.QueryKind queryKind(OpSelectors.OperationKind kind) - { - switch (kind) - { - case DELETE_PARTITION: - return Query.QueryKind.SINGLE_PARTITION; - case DELETE_ROW: - return Query.QueryKind.SINGLE_CLUSTERING; - case DELETE_RANGE: - return Query.QueryKind.CLUSTERING_RANGE; - case DELETE_SLICE: - return Query.QueryKind.CLUSTERING_SLICE; - default: - throw new IllegalStateException(String.format("Can not transform %s into delete", kind)); - } - } - } - - public static class GeneratedDeleteColumnsOp extends BaseOperation implements ReplayingVisitor.DeleteColumnsOp - { - private final long cd; - private final BitSet columnMask; - - public GeneratedDeleteColumnsOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind, BitSet columnMask) - { - super(lts, pd, opId, kind); - this.cd = cd; - this.columnMask = columnMask; - } - - public long cd() - { - return cd; - } - - @Override - public BitSet columns() - { - return columnMask; - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/LoggingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/LoggingVisitor.java deleted file mode 100644 index eee0996db9..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/LoggingVisitor.java +++ /dev/null @@ -1,85 +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.harry.visitors; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.operations.CompiledStatement; - -public class LoggingVisitor extends GeneratingVisitor -{ - public LoggingVisitor(Run run, - OperationExecutor.RowVisitorFactory rowVisitorFactory) - { - super(run, new LoggingVisitorExecutor(run, rowVisitorFactory.make(run))); - } - - public static class LoggingVisitorExecutor extends MutatingVisitor.MutatingVisitExecutor - { - private final BufferedWriter operationLog; - - public LoggingVisitorExecutor(Run run, OperationExecutor rowVisitor) - { - super(run, rowVisitor); - - File f = new File("operation.log"); - try - { - operationLog = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); - } - catch (FileNotFoundException e) - { - throw new RuntimeException(e); - } - } - - public void afterLts(long lts, long pd) - { - super.afterLts(lts, pd); - log("LTS: %d. Pd %d. Finished\n", lts, pd); - } - - @Override - protected void operationInternal(Operation operation, CompiledStatement statement) - { - super.operationInternal(operation, statement); - log(String.format("LTS: %d. Pd %d. Operation: %s Statement %s\n", - operation.lts(), operation.pd(), operation, statement)); - } - - private void log(String format, Object... objects) - { - try - { - operationLog.write(String.format(format, objects)); - operationLog.flush(); - } - catch (IOException e) - { - // ignore - } - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/LtsVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/LtsVisitor.java deleted file mode 100644 index f34b5df5a5..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/LtsVisitor.java +++ /dev/null @@ -1,83 +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.harry.visitors; - -import java.util.function.LongSupplier; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Common class for all visitors that support visits at a specific logical timestamp. - * - * Classes inheriting from LTS Visitor have to visit drawn LTS: every LTS that has been received - * from the consumer _has to_ actually be visited. If this is not done, during model checking - * drawn LTS will be considered visited, which will lead to data inconsistencies. - * - * This class and its implementations such as Mutating visitor are NOT thread safe. If you'd like - * to have several threads generating data, use multiple copies of delegating visitor, since - */ -public abstract class LtsVisitor extends VisitExecutor implements Visitor -{ - private static final Logger logger = LoggerFactory.getLogger(LtsVisitor.class); - protected final VisitExecutor delegate; - private final LongSupplier ltsSource; - - public LtsVisitor(VisitExecutor delegate, - LongSupplier ltsSource) - { - this.delegate = delegate; - this.ltsSource = ltsSource; - } - - public final void visit() - { - long lts = ltsSource.getAsLong(); - if (lts > 0 && lts % 10_000 == 0) - logger.info("Visiting lts {}...", lts); - visit(lts); - } - - public abstract void visit(long lts); - - @Override - protected void beforeLts(long lts, long pd) - { - delegate.beforeLts(lts, pd); - } - - @Override - protected void afterLts(long lts, long pd) - { - delegate.afterLts(lts, pd); - } - - @Override - protected void operation(Operation operation) - { - delegate.operation(operation); - } - - @Override - public void shutdown() throws InterruptedException - { - delegate.shutdown(); - } - -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/MutatingRowVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/MutatingRowVisitor.java deleted file mode 100644 index d394693c21..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/MutatingRowVisitor.java +++ /dev/null @@ -1,115 +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.harry.visitors; - -import org.apache.cassandra.harry.core.MetricReporter; -import org.apache.cassandra.harry.core.Run; -import com.google.common.annotations.VisibleForTesting; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.DeleteHelper; -import org.apache.cassandra.harry.operations.WriteHelper; -import org.apache.cassandra.harry.util.BitSet; - -public class MutatingRowVisitor implements OperationExecutor -{ - protected final SchemaSpec schema; - protected final OpSelectors.Clock clock; - protected final MetricReporter metricReporter; - - public MutatingRowVisitor(Run run) - { - this(run.schemaSpec, - run.clock, - run.metricReporter); - } - - @VisibleForTesting - public MutatingRowVisitor(SchemaSpec schema, - OpSelectors.Clock clock, - MetricReporter metricReporter) - { - this.metricReporter = metricReporter; - this.schema = schema; - this.clock = clock; - } - - public CompiledStatement insert(VisitExecutor.WriteOp op) - { - metricReporter.insert(); - return WriteHelper.inflateInsert(schema, op.pd(), op.cd(), op.vds(), null, clock.rts(op.lts())); - } - - public CompiledStatement insertWithStatics(VisitExecutor.WriteStaticOp op) - { - metricReporter.insert(); - return WriteHelper.inflateInsert(schema, op.pd(), op.cd(), op.vds(), op.sds(), clock.rts(op.lts())); - } - - public CompiledStatement update(VisitExecutor.WriteOp op) - { - metricReporter.insert(); - return WriteHelper.inflateUpdate(schema, op.pd(), op.cd(), op.vds(), null, clock.rts(op.lts())); - } - - public CompiledStatement updateWithStatics(VisitExecutor.WriteStaticOp op) - { - metricReporter.insert(); - return WriteHelper.inflateUpdate(schema, op.pd(), op.cd(), op.vds(), op.sds(), clock.rts(op.lts())); - } - - public CompiledStatement deleteColumn(VisitExecutor.DeleteColumnsOp op) - { - metricReporter.columnDelete(); - BitSet mask = schema.regularColumnsMask(); - return DeleteHelper.deleteColumn(schema, op.pd(), op.cd(), op.columns(), mask, clock.rts(op.lts())); - } - - public CompiledStatement deleteColumnWithStatics(VisitExecutor.DeleteColumnsOp op) - { - metricReporter.columnDelete(); - BitSet mask = schema.regularAndStaticColumnsMask(); - return DeleteHelper.deleteColumn(schema, op.pd(), op.cd(), op.columns(), mask, clock.rts(op.lts())); - } - - public CompiledStatement deleteRow(VisitExecutor.DeleteRowOp op) - { - metricReporter.rowDelete(); - return DeleteHelper.deleteRow(schema, op.pd(), op.cd(), clock.rts(op.lts())); - } - - public CompiledStatement deletePartition(VisitExecutor.DeleteOp op) - { - metricReporter.partitionDelete(); - return DeleteHelper.delete(schema, op.pd(), clock.rts(op.lts())); - } - - public CompiledStatement deleteRange(VisitExecutor.DeleteOp op) - { - metricReporter.rangeDelete(); - return op.relations().toDeleteStatement(clock.rts(op.lts())); - } - - public CompiledStatement deleteSlice(VisitExecutor.DeleteOp op) - { - metricReporter.rangeDelete(); - return op.relations().toDeleteStatement(clock.rts(op.lts())); - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/MutatingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/MutatingVisitor.java deleted file mode 100644 index 4380fd1de9..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/MutatingVisitor.java +++ /dev/null @@ -1,220 +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.harry.visitors; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import com.google.common.util.concurrent.Uninterruptibles; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.tracker.DataTracker; - -public class MutatingVisitor extends GeneratingVisitor -{ - private static final Logger logger = LoggerFactory.getLogger(MutatingVisitor.class); - - public MutatingVisitor(Run run) - { - this(run, MutatingRowVisitor::new); - } - - public MutatingVisitor(Run run, - OperationExecutor.RowVisitorFactory rowVisitorFactory) - { - this(run, new MutatingVisitExecutor(run, rowVisitorFactory.make(run))); - } - - public static Configuration.VisitorConfiguration factory() - { - return MutatingVisitor::new; - } - - public static Configuration.VisitorConfiguration factory(Function rowVisitorFactory) - { - return (r) -> new MutatingVisitor(r, rowVisitorFactory.apply(r)); - } - - public MutatingVisitor(Run run, - VisitExecutor visitExecutor) - { - super(run, visitExecutor); - } - - public static class LtsTrackingVisitExecutor extends MutatingVisitExecutor - { - public LtsTrackingVisitExecutor(OpSelectors.DescriptorSelector descriptorSelector, DataTracker tracker, SystemUnderTest sut, SchemaSpec schema, OperationExecutor rowVisitor, SystemUnderTest.ConsistencyLevel consistencyLevel) - { - super(descriptorSelector, tracker, sut, schema, rowVisitor, consistencyLevel); - assert schema.trackLts : "LTS Tracking visitor can only be used when tracking LTS in schema"; - } - - @Override - public void afterLts(long lts, long pd) - { - if (hadVisibleVisit) - { - StringBuilder sb = new StringBuilder(); - sb.append("UPDATE ").append(schema.keyspace).append(".").append(schema.table) - .append(" SET visited_lts = visited_lts + [").append(lts).append("] ") - .append("WHERE "); - Object[] pk = schema.inflatePartitionKey(pd); - for (int i = 0; i < pk.length; i++) - { - if (i > 0) - sb.append(" AND "); - sb.append(schema.partitionKeys.get(i).name + " = ?"); - bindings.add(pk[i]); - } - sb.append(";"); - statements.add(sb.toString()); - } - - super.afterLts(lts, pd); - } - } - - public static class MutatingVisitExecutor extends VisitExecutor - { - protected final List statements = new ArrayList<>(); - protected final List bindings = new ArrayList<>(); - - protected final OpSelectors.DescriptorSelector descriptorSelector; - protected final DataTracker tracker; - protected final SystemUnderTest sut; - protected final OperationExecutor rowVisitor; - protected final SystemUnderTest.ConsistencyLevel consistencyLevel; - protected final SchemaSpec schema; - private final int maxRetries = 10; - // Apart from partition deletion, we register all operations on partition level - protected Boolean hadVisibleVisit = null; - - public MutatingVisitExecutor(Run run, OperationExecutor rowVisitor) - { - this(run, rowVisitor, SystemUnderTest.ConsistencyLevel.QUORUM); - } - - public MutatingVisitExecutor(Run run, OperationExecutor rowVisitor, SystemUnderTest.ConsistencyLevel consistencyLevel) - { - this(run.descriptorSelector, run.tracker, run.sut, run.schemaSpec, rowVisitor, consistencyLevel); - } - - public MutatingVisitExecutor(OpSelectors.DescriptorSelector descriptorSelector, - DataTracker tracker, - SystemUnderTest sut, - SchemaSpec schema, - OperationExecutor rowVisitor, - SystemUnderTest.ConsistencyLevel consistencyLevel) - { - this.descriptorSelector = descriptorSelector; - this.tracker = tracker; - this.sut = sut; - this.schema = schema; - this.rowVisitor = rowVisitor; - this.consistencyLevel = consistencyLevel; - } - - @Override - public void beforeLts(long lts, long pd) - { - tracker.beginModification(lts); - } - - @Override - public void afterLts(long lts, long pd) - { - if (statements.isEmpty()) - { - logger.warn("Encountered an empty batch on {}", lts); - return; - } - - String query = String.join(" ", statements); - if (statements.size() > 1) - query = String.format("BEGIN UNLOGGED BATCH\n%s\nAPPLY BATCH;", query); - - Object[] bindingsArray = new Object[bindings.size()]; - bindings.toArray(bindingsArray); - statements.clear(); - bindings.clear(); - - CompiledStatement compiledStatement = new CompiledStatement(query, bindingsArray); - executeWithRetries(lts, pd, compiledStatement); - tracker.endModification(lts); - hadVisibleVisit = null; - } - - @Override - public void operation(Operation operation) - { - // Partition deletions have highest precedence even in batches, so we have to make - // a distinction between "we have not seen any operations yet" and "there was a partition deletion" - if (hadVisibleVisit == null) - hadVisibleVisit = operation.kind().hasVisibleVisit(); - else - hadVisibleVisit &= operation.kind().hasVisibleVisit(); - - operationInternal(operation, rowVisitor.perform(operation)); - } - - protected void operationInternal(Operation operation, CompiledStatement statement) - { - statements.add(statement.cql()); - Collections.addAll(bindings, statement.bindings()); - } - - protected Object[][] executeWithRetries(long lts, long pd, CompiledStatement statement) - { - if (sut.isShutdown()) - throw new IllegalStateException("System under test is shut down"); - - int retries = 0; - - while (retries++ < maxRetries) - { - try - { - return sut.execute(statement.cql(), consistencyLevel, statement.bindings()); - } - catch (Throwable t) - { - int delaySecs = 1; - logger.error(String.format("Caught message while trying to execute: %s. Scheduled to retry in %s seconds", statement, delaySecs), t); - Uninterruptibles.sleepUninterruptibly(delaySecs, TimeUnit.SECONDS); - } - } - - throw new IllegalStateException(String.format("Can not execute statement %s after %d retries", statement, retries)); - } - - public void shutdown() throws InterruptedException - { - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/OperationExecutor.java b/test/harry/main/org/apache/cassandra/harry/visitors/OperationExecutor.java deleted file mode 100644 index a5997a2a1d..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/OperationExecutor.java +++ /dev/null @@ -1,78 +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.harry.visitors; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.operations.CompiledStatement; - -public interface OperationExecutor -{ - interface RowVisitorFactory - { - OperationExecutor make(Run run); - } - - default CompiledStatement perform(VisitExecutor.Operation operation) - { - switch (operation.kind()) - { - // TODO: switch to EnumMap - // TODO: pluggable capabilities; OperationKind can/should bear its own logic - case INSERT: - return insert((VisitExecutor.WriteOp) operation); - case UPDATE: - return update((VisitExecutor.WriteOp) operation); - case DELETE_ROW: - return deleteRow((VisitExecutor.DeleteRowOp) operation); - case INSERT_WITH_STATICS: - return insertWithStatics((VisitExecutor.WriteStaticOp) operation); - case UPDATE_WITH_STATICS: - return updateWithStatics((VisitExecutor.WriteStaticOp) operation); - case DELETE_PARTITION: - return deletePartition((VisitExecutor.DeleteOp) operation); - case DELETE_COLUMN: - return deleteColumn((VisitExecutor.DeleteColumnsOp) operation); - case DELETE_COLUMN_WITH_STATICS: - return deleteColumnWithStatics((VisitExecutor.DeleteColumnsOp) operation); - case DELETE_RANGE: - return deleteRange((VisitExecutor.DeleteOp) operation); - case DELETE_SLICE: - return deleteSlice((VisitExecutor.DeleteOp) operation); - default: - throw new IllegalStateException(); - } - } - - CompiledStatement insert(VisitExecutor.WriteOp operation); - CompiledStatement update(VisitExecutor.WriteOp operation); - - CompiledStatement insertWithStatics(VisitExecutor.WriteStaticOp operation); - CompiledStatement updateWithStatics(VisitExecutor.WriteStaticOp operation); - - CompiledStatement deleteColumn(VisitExecutor.DeleteColumnsOp operation); - CompiledStatement deleteColumnWithStatics(VisitExecutor.DeleteColumnsOp operation); - - CompiledStatement deleteRow(VisitExecutor.DeleteRowOp operation); - - CompiledStatement deletePartition(VisitExecutor.DeleteOp deleteOp); - CompiledStatement deleteRange(VisitExecutor.DeleteOp deleteOp); - CompiledStatement deleteSlice(VisitExecutor.DeleteOp deleteOp); - - -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/ParallelRecentValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/ParallelRecentValidator.java deleted file mode 100644 index 61cd1b2122..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/ParallelRecentValidator.java +++ /dev/null @@ -1,149 +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.harry.visitors; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicLong; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; -import org.apache.cassandra.harry.tracker.DataTracker; - -public class ParallelRecentValidator extends ParallelValidator -{ - private static final Logger logger = LoggerFactory.getLogger(ParallelRecentValidator.class); - - private final int partitionCount; - private final int queries; - private final QueryGenerator.TypedQueryGenerator querySelector; - private final Model model; - private final QueryLogger queryLogger; - private final OpSelectors.PdSelector pdSelector; - private final DataTracker tracker; - - public ParallelRecentValidator(int partitionCount, int concurrency, int queries, - Run run, - Model.ModelFactory modelFactory, - QueryLogger queryLogger) - { - super(concurrency, run); - this.pdSelector = run.pdSelector; - this.tracker = run.tracker; - this.partitionCount = partitionCount; - this.queries = Math.max(queries, 1); - this.querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, - // TODO: make query kind configurable - Surjections.enumValues(Query.QueryKind.class), - run.rangeSelector); - this.model = modelFactory.make(run); - this.queryLogger = queryLogger; - } - - protected void doOne(State state) - { - long claim = state.claim(); - if (claim < 0) - return; - - long visitLts = run.pdSelector.minLtsAt(state.position - claim); - for (int i = 0; i < queries; i++) - { - run.metricReporter.validateRandomQuery(); - Query query = querySelector.inflate(visitLts, i); - model.validate(query); - queryLogger.logSelectQuery(i, query); - } - } - - protected CompletableFuture startThreads(ExecutorService executor, int parallelism) - { - logger.info("Validating {} recent partitions", partitionCount); - return super.startThreads(executor, parallelism); - } - - protected State initialState() - { - return new State(pdSelector.maxPosition(tracker.maxStarted())); - } - - public class State extends ParallelValidator.State - { - private final long position; - private final AtomicLong counter; - - public State(long maxPos) - { - this.position = maxPos; - this.counter = new AtomicLong(partitionCount); - } - - public long claim() - { - long v = counter.getAndDecrement(); - if (v <= 0) - signal(); - - return v; - } - } - - @JsonTypeName("parallel_validate_recent_partitions") - public static class ParallelRecentValidatorConfig implements Configuration.VisitorConfiguration - { - public final int partition_count; - public final int queries; - public final int concurrency; - public final Configuration.ModelConfiguration modelConfiguration; - public final Configuration.QueryLoggerConfiguration query_logger; - - // TODO: make query selector configurable - @JsonCreator - public ParallelRecentValidatorConfig(@JsonProperty("partition_count") int partition_count, - @JsonProperty("concurrency") int concurrency, - @JsonProperty("queries_per_partition") int queries, - @JsonProperty("model") Configuration.ModelConfiguration model, - @JsonProperty("query_logger") Configuration.QueryLoggerConfiguration query_logger) - { - this.partition_count = partition_count; - this.concurrency = concurrency; - this.queries = Math.max(queries, 1); - this.modelConfiguration = model; - this.query_logger = QueryLogger.thisOrDefault(query_logger); - } - - @Override - public Visitor make(Run run) - { - return new ParallelRecentValidator(partition_count, concurrency, queries, run, modelConfiguration, query_logger.make()); - } - } - -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/ParallelValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/ParallelValidator.java deleted file mode 100644 index 4c2dea20fc..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/ParallelValidator.java +++ /dev/null @@ -1,103 +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.harry.visitors; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Run; - -public abstract class ParallelValidator implements Visitor -{ - private static final Logger logger = LoggerFactory.getLogger(AllPartitionsValidator.class); - - protected final Run run; - protected final int parallelism; - protected final ExecutorService executor; - - public ParallelValidator(int parallelism, - Run run) - { - this.run = run; - this.parallelism = parallelism; - this.executor = Executors.newFixedThreadPool(parallelism); - } - - protected abstract void doOne(T state); - protected abstract T initialState(); - - protected CompletableFuture startThreads(ExecutorService executor, int parallelism) - { - CompletableFuture[] futures = new CompletableFuture[parallelism]; - T shared = initialState(); - - for (int i = 0; i < parallelism; i++) - { - futures[i] = CompletableFuture.supplyAsync(() -> { - while (!shared.signalled()) - doOne(shared); - - return null; - }, executor); - } - - return CompletableFuture.allOf(futures); - } - - public abstract static class State - { - private final AtomicBoolean isDone = new AtomicBoolean(false); - - public void signal() - { - isDone.set(true); - } - - public boolean signalled() - { - return isDone.get(); - } - } - - public void visit() - { - try - { - startThreads(executor, parallelism).get(); - } - catch (Throwable e) - { - throw new RuntimeException(e); - } - logger.info("Finished validations"); - } - - @Override - public void shutdown() throws InterruptedException - { - executor.shutdown(); - executor.awaitTermination(60, TimeUnit.SECONDS); - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/QueryLogger.java b/test/harry/main/org/apache/cassandra/harry/visitors/QueryLogger.java deleted file mode 100644 index 3eb60df742..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/QueryLogger.java +++ /dev/null @@ -1,97 +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.harry.visitors; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.operations.Query; - -public interface QueryLogger -{ - static Configuration.QueryLoggerConfiguration thisOrDefault(Configuration.QueryLoggerConfiguration config) - { - if (config == null) - return () -> NO_OP; - return config; - } - - QueryLogger NO_OP = new NoOpQueryLogger(); - - void println(String a, Object... interpolate); - default void logSelectQuery(int modifier, Query query) - { - println(String.format("PD: %d. Modifier: %d.\t%s", query.pd, modifier, query.toSelectStatement())); - } - interface QueryLoggerFactory - { - QueryLogger make(); - } - - class FileQueryLogger implements QueryLogger - { - private static final Logger logger = LoggerFactory.getLogger(FileQueryLogger.class); - private final BufferedWriter log; - - public FileQueryLogger(String filename) - { - File f = new File(filename); - try - { - log = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); - } - catch (FileNotFoundException e) - { - throw new RuntimeException(e); - } - } - - public void println(String s, Object... interpolate) - { - try - { - log.write(String.format(s, interpolate)); - } - catch (IOException e) - { - logger.error("Could not log line", e); - } - } - } - - class NoOpQueryLogger implements QueryLogger - { - public void println(String a, Object... interpolate) - { - } - - public void logSelectQuery(int modifier, Query query) - { - - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/RandomPartitionValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/RandomPartitionValidator.java deleted file mode 100644 index d1bd1d931e..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/RandomPartitionValidator.java +++ /dev/null @@ -1,58 +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.harry.visitors; - -import java.util.concurrent.atomic.AtomicLong; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.QueryGenerator; - -public class RandomPartitionValidator implements Visitor -{ - protected final Model model; - protected final QueryGenerator.TypedQueryGenerator queryGenerator; - protected final Run run; - protected final AtomicLong modifier; - protected final QueryLogger logger; - - public RandomPartitionValidator(Run run, - Model.ModelFactory modelFactory, - QueryLogger logger) - { - this.model = modelFactory.make(run); - this.queryGenerator = new QueryGenerator.TypedQueryGenerator(run); - this.run = run; - this.modifier = new AtomicLong(); - this.logger = logger; - } - - public void visit() - { - if (run.tracker.maxStarted() == 0) - return; - - long modifier = this.modifier.incrementAndGet(); - long pd = ((OpSelectors.DefaultPdSelector)run.pdSelector).randomVisitedPd(run.tracker.maxStarted(), - modifier, - run.schemaSpec); - model.validate(queryGenerator.inflate(run.pdSelector.maxLtsFor(pd), modifier)); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/RandomValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/RandomValidator.java deleted file mode 100644 index f159e1475d..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/RandomValidator.java +++ /dev/null @@ -1,93 +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.harry.visitors; - -import java.util.concurrent.atomic.AtomicLong; - -import org.apache.cassandra.harry.core.MetricReporter; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; -import org.apache.cassandra.harry.tracker.DataTracker; - -public class RandomValidator implements Visitor -{ - private final QueryLogger logger; - private final Model model; - - private final OpSelectors.DefaultPdSelector pdSelector; - private final QueryGenerator.TypedQueryGenerator querySelector; - private final MetricReporter metricReporter; - private final DataTracker tracker; - private final AtomicLong modifier; - private final SchemaSpec schemaSpec; - - private final int partitionCount; - private final int queries; - - public RandomValidator(int partitionCount, - int queries, - Run run, - Model.ModelFactory modelFactory, - QueryLogger logger) - { - this.partitionCount = partitionCount; - this.queries = Math.max(queries, 1); - this.metricReporter = run.metricReporter; - this.pdSelector = (OpSelectors.DefaultPdSelector) run.pdSelector; - this.querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, - Surjections.pick(Query.QueryKind.SINGLE_PARTITION), - run.rangeSelector); - this.model = modelFactory.make(run); - this.logger = logger; - this.tracker = run.tracker; - this.schemaSpec = run.schemaSpec; - - this.modifier = new AtomicLong(); - } - - // TODO: expose metric, how many times validated recent partitions - private int validateRandomPartitions() - { - for (int i = 0; i < partitionCount && !Thread.currentThread().isInterrupted(); i++) - { - metricReporter.validateRandomQuery(); - long modifier = this.modifier.incrementAndGet(); - long pd = pdSelector.randomVisitedPd(tracker.maxStarted(), modifier, schemaSpec); - for (int j = 0; j < queries && !Thread.currentThread().isInterrupted(); j++) - { - Query query = querySelector.inflate(pdSelector.maxLtsFor(pd), j); - logger.logSelectQuery(j, query); - model.validate(query); - } - } - - return partitionCount; - } - - @Override - public void visit() - { - validateRandomPartitions(); - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/RecentValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/RecentValidator.java deleted file mode 100644 index 30f34c56c8..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/RecentValidator.java +++ /dev/null @@ -1,96 +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.harry.visitors; - -import org.apache.cassandra.harry.core.MetricReporter; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; - -public class RecentValidator implements Visitor -{ - private final QueryLogger queryLogger; - private final Model model; - - private final OpSelectors.PdSelector pdSelector; - private final QueryGenerator.TypedQueryGenerator querySelector; - private final MetricReporter metricReporter; - private final OpSelectors.Clock clock; - - private final int partitionCount; - private final int queries; - - public RecentValidator(int partitionCount, - int queries, - Run run, - Model.ModelFactory modelFactory, - QueryLogger queryLogger) - { - this.partitionCount = partitionCount; - this.queries = Math.max(queries, 1); - this.metricReporter = run.metricReporter; - this.pdSelector = run.pdSelector; - this.clock = run.clock; - this.querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, - // TODO: make query kind configurable - Surjections.enumValues(Query.QueryKind.class), - run.rangeSelector); - this.model = modelFactory.make(run); - this.queryLogger = queryLogger; - } - - // TODO: expose metric, how many times validated recent partitions - private int validateRecentPartitions() - { - long pos = pdSelector.maxPosition(clock.peek()); - - int maxPartitions = partitionCount; - while (pos >= 0 && maxPartitions > 0 && !Thread.currentThread().isInterrupted()) - { - long visitLts = pdSelector.minLtsAt(pos); - for (int i = 0; i < queries; i++) - { - metricReporter.validateRandomQuery(); - Query query = querySelector.inflate(visitLts, i); - // TODO: add pd skipping from shrinker here, too - log(i, query); - model.validate(query); - } - - pos--; - maxPartitions--; - } - - return partitionCount - maxPartitions; - } - - @Override - public void visit() - { - validateRecentPartitions(); - } - - private void log(int modifier, Query query) - { - queryLogger.println(String.format("PD: %d. Modifier: %d.\t%s", query.pd, modifier, query.toSelectStatement())); - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/ReplayingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/ReplayingVisitor.java deleted file mode 100644 index 65fcdb87d9..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/ReplayingVisitor.java +++ /dev/null @@ -1,73 +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.harry.visitors; - -import java.util.Arrays; -import java.util.function.LongSupplier; - -public abstract class ReplayingVisitor extends LtsVisitor -{ - public ReplayingVisitor(VisitExecutor delegate, LongSupplier ltsSource) - { - super(delegate, ltsSource); - } - - @Override - public void visit(long lts) - { - replay(getVisit(lts)); - } - - public abstract Visit getVisit(long lts); - - public abstract void replayAll(); - - private void replay(Visit visit) - { - beforeLts(visit.lts, visit.pd); - for (Operation operation : visit.operations) - operation(operation); - afterLts(visit.lts, visit.pd); - } - - public static class Visit - { - public final long lts; - public final long pd; - public final Operation[] operations; - - public Visit(long lts, long pd, Operation[] operations) - { - this.lts = lts; - this.pd = pd; - this.operations = operations; - } - - public String toString() - { - return "Visit{" + - "lts=" + lts + - ", pd=" + pd + - ", operations=[" + Arrays.toString(operations) + - "]}"; - } - - - } -} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/Sampler.java b/test/harry/main/org/apache/cassandra/harry/visitors/Sampler.java deleted file mode 100644 index 0b2dced728..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/Sampler.java +++ /dev/null @@ -1,97 +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.harry.visitors; - -import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.rng.RngUtils; -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.tracker.DataTracker; - -public class Sampler implements Visitor -{ - private static final Logger logger = LoggerFactory.getLogger(AllPartitionsValidator.class); - - private final SystemUnderTest sut; - private final OpSelectors.PdSelector pdSelector; - private final OpSelectors.Clock clock; - private final DataTracker tracker; - private final SchemaSpec schema; - private final int samplePartitions; - - public Sampler(Run run, int samplePartitions) - { - this.sut = run.sut; - this.pdSelector = run.pdSelector; - this.clock = run.clock; - this.tracker = run.tracker; - this.schema = run.schemaSpec; - this.samplePartitions = samplePartitions; - } - - public void visit() - { - long lts = clock.peek(); - long max = pdSelector.maxPosition(tracker.maxStarted()); - DescriptiveStatistics ds = new DescriptiveStatistics(); - int empty = 0; - - long n = RngUtils.next(lts); - for (long i = 0; i < this.samplePartitions; i++) - { - long posLts = pdSelector.minLtsAt(RngUtils.asInt(n, (int) max)); - n = RngUtils.next(n); - // TODO: why not just pd at pos? - long pd = pdSelector.pd(posLts, schema); - long count = (long) sut.execute(SelectHelper.count(schema, pd), SystemUnderTest.ConsistencyLevel.ONE)[0][0]; - if (count == 0) - empty++; - ds.addValue(count); - } - logger.info("Visited {} partitions (sampled {} empty out of {}), with mean size of {}. Median: {}. Min: {}. Max: {}", - max, empty, samplePartitions, ds.getMean(), ds.getPercentile(0.5), ds.getMin(), ds.getMax()); - } - - @JsonTypeName("sampler") - public static class SamplerConfiguration implements Configuration.VisitorConfiguration - { - public final int sample_partitions; - - @JsonCreator - public SamplerConfiguration(@JsonProperty(value = "sample_partitions", defaultValue = "10") int sample_partitions) - { - this.sample_partitions = sample_partitions; - } - - public Visitor make(Run run) - { - return new Sampler(run, sample_partitions); - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/SingleValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/SingleValidator.java deleted file mode 100644 index 3783958e4d..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/SingleValidator.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.harry.visitors; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.operations.QueryGenerator; - -public class SingleValidator implements Visitor -{ - protected final int iterations; - protected final Model model; - protected final QueryGenerator queryGenerator; - protected final Run run; - - public SingleValidator(int iterations, - Run run, - Model.ModelFactory modelFactory) - { - this.iterations = iterations; - this.model = modelFactory.make(run); - this.queryGenerator = new QueryGenerator(run); - this.run = run; - } - - @Override - public void visit() - { - visit(run.clock.peek()); - } - - public void visit(long lts) - { - model.validate(queryGenerator.inflate(lts, 0, Query.QueryKind.SINGLE_PARTITION)); - - for (boolean reverse : new boolean[]{ true, false }) - { - model.validate(Query.selectAllColumns(run.schemaSpec, run.pdSelector.pd(lts, run.schemaSpec), reverse)); - } - - for (Query.QueryKind queryKind : new Query.QueryKind[]{ Query.QueryKind.CLUSTERING_RANGE, Query.QueryKind.CLUSTERING_SLICE, Query.QueryKind.SINGLE_CLUSTERING }) - { - for (int i = 0; i < iterations; i++) - { - model.validate(queryGenerator.inflate(lts, i, queryKind)); - } - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/SkippingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/SkippingVisitor.java deleted file mode 100644 index a8c5fb6ccb..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/SkippingVisitor.java +++ /dev/null @@ -1,56 +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.harry.visitors; - -import java.util.Set; -import java.util.function.LongSupplier; - -public class SkippingVisitor extends LtsVisitor -{ - private final Set ltsToSkip; - private final Set pdsToSkip; - private final LtsToPd ltsToPd; - // Use DelegatingVisitor class instead of VisitExecutor available via protected field - private LtsVisitor delegateShadow; - - public SkippingVisitor(LtsVisitor delegate, - LongSupplier ltsSupplier, - LtsToPd ltsToPd, - Set ltsToSkip, - Set pdsToSkip) - { - super(delegate, ltsSupplier); - this.ltsToSkip = ltsToSkip; - this.pdsToSkip = pdsToSkip; - this.ltsToPd = ltsToPd; - } - - public void visit(long lts) - { - if (ltsToSkip.contains(lts) || pdsToSkip.contains(ltsToPd.convert(lts))) - return; - - delegateShadow.visit(lts); - } - - public static interface LtsToPd - { - public long convert(long lts); - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/VisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/visitors/VisitExecutor.java deleted file mode 100644 index a0e9c8e73c..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/VisitExecutor.java +++ /dev/null @@ -1,122 +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.harry.visitors; - -import org.apache.cassandra.harry.model.OpSelectors; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.util.BitSet; - -public abstract class VisitExecutor -{ - protected abstract void beforeLts(long lts, long pd); - - protected abstract void afterLts(long lts, long pd); - - protected abstract void operation(Operation operation); - - public abstract void shutdown() throws InterruptedException; - - public interface WriteOp extends Operation - { - long cd(); - long[] vds(); - } - - public interface WriteStaticOp extends WriteOp - { - long[] sds(); - } - - public interface DeleteRowOp extends Operation - { - long cd(); - } - - public interface DeleteOp extends Operation - { - Query relations(); - } - - public interface DeleteColumnsOp extends Operation - { - long cd(); - BitSet columns(); - } - - public interface Operation - { - long pd(); - long lts(); - long opId(); - OpSelectors.OperationKind kind(); - } - - public static abstract class BaseOperation implements Operation - { - public final long lts; - public final long opId; - public final long pd; - public final OpSelectors.OperationKind kind; - - public BaseOperation(long lts, long pd, long opId, OpSelectors.OperationKind kind) - { - assert opId >= 0; - this.pd = pd; - this.lts = lts; - this.opId = opId; - this.kind = kind; - } - - @Override - public final long pd() - { - return pd; - - } - - @Override - public final long lts() - { - return lts; - } - - @Override - public final long opId() - { - return opId; - } - - @Override - public final OpSelectors.OperationKind kind() - { - return kind; - - } - - public String toString() - { - return "Operation{" + - " lts=" + lts + - " pd=" + pd + - " opId=" + opId + - ", kind=" + kind + - '}'; - } - } -} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/Visitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/Visitor.java deleted file mode 100644 index ccac2ded57..0000000000 --- a/test/harry/main/org/apache/cassandra/harry/visitors/Visitor.java +++ /dev/null @@ -1,33 +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.harry.visitors; - -import org.apache.cassandra.harry.core.Run; - -public interface Visitor -{ - void visit(); - - default void shutdown() throws InterruptedException {} - - interface VisitorFactory - { - Visitor make(Run run); - } -} \ No newline at end of file diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java index 4b6caa0164..5d32ab6eaa 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java @@ -33,7 +33,7 @@ import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionList; import org.apache.cassandra.simulator.ActionListener; diff --git a/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java b/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java deleted file mode 100644 index 05620b5517..0000000000 --- a/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java +++ /dev/null @@ -1,146 +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.simulator.harry; - -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.data.ResultSetRow; -import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.model.SelectHelper; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.IInstance; -import org.apache.cassandra.harry.sut.TokenPlacementModel; -import org.apache.cassandra.harry.sut.injvm.QuiescentLocalStateChecker; -import org.apache.cassandra.simulator.systems.InterceptedExecution; -import org.apache.cassandra.simulator.systems.InterceptingExecutor; -import org.apache.cassandra.simulator.systems.SimulatedAction; -import org.apache.cassandra.simulator.systems.SimulatedSystems; - -public class HarryValidatingQuery extends SimulatedAction -{ - private static final Logger logger = LoggerFactory.getLogger(HarryValidatingQuery.class); - private final Run run; - private final Query query; - private final InterceptingExecutor on; - private final Cluster cluster; - private final List owernship; - private final TokenPlacementModel.ReplicationFactor rf; - - public HarryValidatingQuery(SimulatedSystems simulated, - Cluster cluster, - TokenPlacementModel.ReplicationFactor rf, - Run run, - List owernship, - Query query) - { - super(query, Modifiers.RELIABLE_NO_TIMEOUTS, Modifiers.RELIABLE_NO_TIMEOUTS, null, simulated); - this.rf = rf; - this.cluster = cluster; - this.on = (InterceptingExecutor) cluster.get(1).executor(); - this.run = run; - this.query = query; - this.owernship = owernship; - } - - protected InterceptedExecution task() - { - return new InterceptedExecution.InterceptedTaskExecution(on) - { - public void run() - { - try - { - QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, rf) - { - @Override - protected TokenPlacementModel.ReplicatedRanges getRing() - { - return rf.replicate(owernship); - } - - @Override - protected void validate(Query query, TokenPlacementModel.ReplicatedRanges ring) - { - CompiledStatement compiled = query.toSelectStatement(); - List replicas = ring.replicasFor(this.token(query.pd)); - logger.trace("Predicted {} as replicas for {}. Ring: {}", new Object[]{ replicas, query.pd, ring }); - List throwables = new ArrayList<>(); - for (TokenPlacementModel.Replica replica : replicas) - { - try - { - validate(() -> { - Object[][] objects = this.executeNodeLocal(compiled.cql(), replica.node(), compiled.bindings()); - List result = new ArrayList(); - int length = objects.length; - for (int i = 0; i < length; ++i) - { - Object[] res = objects[i]; - result.add(SelectHelper.resultSetToRow(query.schemaSpec, this.clock, res)); - } - - return result; - }, query); - } - catch (Model.ValidationException t) - { - throwables.add(new AssertionError(String.format("Caught an exception while validating %s on %s", query, replica), t)); - } - } - - if (!throwables.isEmpty()) - { - AssertionError error = new AssertionError(String.format("Could not validate %d out of %d replicas %s", throwables.size(), replicas.size(), replicas)); - throwables.forEach(error::addSuppressed); - throw error; - } - } - - - @Override - protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) - { - IInstance instance = cluster - .stream() - .filter((n) -> n.config().broadcastAddress().toString().equals(node.id())) - .findFirst() - .get(); - return instance.executeInternal(statement, bindings); - } - }; - - model.validate(query); - - } - catch (Throwable t) - { - logger.error("Caught an exception while validating", t); - throw t; - } - } - }; - } -} diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java index e98b8ddd0d..9fc7e69832 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java @@ -25,13 +25,13 @@ import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -41,18 +41,7 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.harry.clock.OffsetClock; -import org.apache.cassandra.harry.core.Configuration; -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.ddl.ColumnSpec; -import org.apache.cassandra.harry.ddl.SchemaGenerators; -import org.apache.cassandra.harry.ddl.SchemaSpec; -import org.apache.cassandra.harry.gen.Surjections; -import org.apache.cassandra.harry.operations.Query; -import org.apache.cassandra.harry.sut.SystemUnderTest; -import org.apache.cassandra.harry.sut.injvm.InJvmSut; -import org.apache.cassandra.harry.tracker.DefaultDataTracker; -import org.apache.cassandra.harry.visitors.GeneratingVisitor; +import accord.utils.Invariants; import io.airlift.airline.Command; import io.airlift.airline.HelpOption; import io.airlift.airline.Option; @@ -63,9 +52,22 @@ import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IIsolatedExecutor; -import org.apache.cassandra.harry.HarryHelper; +import org.apache.cassandra.distributed.impl.Query; import org.apache.cassandra.distributed.shared.WithProperties; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.gen.OperationsGenerators; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Verb; import org.apache.cassandra.schema.ReplicationParams; @@ -88,10 +90,10 @@ import org.apache.cassandra.simulator.SimulatorUtils; import org.apache.cassandra.simulator.cluster.ClusterActionListener.NoOpListener; import org.apache.cassandra.simulator.cluster.ClusterActions; import org.apache.cassandra.simulator.cluster.ClusterActions.Options; -import org.apache.cassandra.simulator.harry.HarryValidatingQuery; import org.apache.cassandra.simulator.systems.Failures; import org.apache.cassandra.simulator.systems.InterceptedExecution; import org.apache.cassandra.simulator.systems.InterceptingExecutor; +import org.apache.cassandra.simulator.systems.SimulatedActionCallable; import org.apache.cassandra.simulator.systems.SimulatedActionTask; import org.apache.cassandra.simulator.systems.SimulatedSystems; import org.apache.cassandra.simulator.systems.SimulatedTime; @@ -103,10 +105,11 @@ import org.apache.cassandra.tcm.membership.NodeState; import org.apache.cassandra.tcm.sequences.SingleNodeSequences; import org.apache.cassandra.tcm.transformations.PrepareJoin; import org.apache.cassandra.utils.CloseableIterator; +import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; -import static org.apache.cassandra.harry.sut.TokenPlacementModel.constantLookup; +import static org.apache.cassandra.harry.model.TokenPlacementModel.constantLookup; import static org.apache.cassandra.simulator.ActionSchedule.Mode.UNLIMITED; import static org.apache.cassandra.simulator.cluster.ClusterActions.Options.noActions; @@ -205,9 +208,9 @@ public class HarrySimulatorTest @Test public void test() throws Exception { - rowsPerPhase = 1; // To rerun a failing test for a given seed, uncomment the below and set the seed // this.seed = ""; + this.seed = "0xdd3bb3793a6b925a"; harryTest(); } @@ -248,17 +251,6 @@ public class HarrySimulatorTest // Backoff should be larger than read timeout, since otherwise we will simply saturate the stage with retries .set("progress_barrier_backoff", "1000ms") .set("cms_await_timeout", "600000ms"), - HarryHelper.defaultConfiguration() - .setSchemaProvider(new Configuration.SchemaProviderConfiguration() - { - private final Surjections.Surjection schema = schemaSpecGen("harry", "tbl"); - public SchemaSpec make(long l, SystemUnderTest systemUnderTest) - { - return schema.inflate(l); - } - }) - .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(2, 1)) - .setClusteringDescriptorSelector(HarryHelper.singleRowPerModification().setMaxPartitionSize(100).build()), arr(), (simulation) -> { simulation.cluster.stream().forEach((IInvokableInstance i) -> { @@ -285,16 +277,16 @@ public class HarrySimulatorTest work.add(work("Create Keyspace", simulation.clusterActions.schemaChange(1, String.format("CREATE KEYSPACE %s WITH replication = {'class': 'NetworkTopologyStrategy', " + rfString + "};", - simulation.harryRun.schemaSpec.keyspace)))); + simulation.schema.keyspace)))); work.add(work("Create table", simulation.clusterActions.schemaChange(1, - simulation.harryRun.schemaSpec.compile().cql()))); + simulation.schema.compile()))); simulation.cluster.stream().forEach(i -> { work.add(work("Output epoch", lazy(simulation.simulated, i, () -> logger.warn(ClusterMetadata.current().epoch.toString())))); }); - work.add(interleave("Start generating", HarrySimulatorTest.generate(rowsPerPhase, simulation, cl))); + work.add(interleave("Start generating", HarrySimulatorTest.generateWrites(rowsPerPhase, simulation, cl))); work.add(work("Validate all data locally", lazy(() -> validateAllLocal(simulation, simulation.nodeState.ring, rf)))); @@ -319,7 +311,7 @@ public class HarrySimulatorTest long token = simulation.simulated.random.uniform(Long.MIN_VALUE, Long.MAX_VALUE); work.add(interleave("Bootstrap and generate data", ActionList.of(bootstrap(simulation.simulated, simulation.cluster, token, node)), - generate(rowsPerPhase, simulation, cl) + generateWrites(rowsPerPhase, simulation, cl) )); simulation.cluster.stream().forEach(i -> { work.add(work("Output epoch", @@ -337,7 +329,7 @@ public class HarrySimulatorTest node = bootstrappedNodes.remove(0); work.add(interleave("Decommission and generate data", ActionList.of(decommission(simulation.simulated, simulation.cluster, node)), - generate(rowsPerPhase, simulation, cl) + generateWrites(rowsPerPhase, simulation, cl) )); simulation.cluster.stream().forEach(i -> { work.add(work("Output epoch", @@ -372,41 +364,94 @@ public class HarrySimulatorTest { protected final ClusterActions clusterActions; protected final SimulatedNodeState nodeState; - protected final Run harryRun; protected final SimulatedSystems simulated; protected final RunnableActionScheduler scheduler; protected final Cluster cluster; protected final Function schedule; - public HarrySimulation(SimulatedSystems simulated, RunnableActionScheduler scheduler, Cluster cluster, Run run, Function schedule) - { - this(simulated, scheduler, cluster, run, SimulatedNodeState::new, schedule); - } + protected final EntropySource rng; + protected final SchemaSpec schema; + protected final Generator insertGen; + protected final QueryBuildingVisitExecutor queryBuilder; + protected final QuiescentChecker model; - protected HarrySimulation(SimulatedSystems simulated, - RunnableActionScheduler scheduler, - Cluster cluster, - Run run, - Function nodeState, - Function schedule) + protected final Map log; + protected final Generator ltsGen; + protected final DataTracker tracker; + + private HarrySimulation(SchemaSpec schema, + EntropySource rng, + SimulatedSystems simulated, + RunnableActionScheduler scheduler, + Cluster cluster, + Function nodeState, + Function schedule, + Map log, + Generator ltsGen, + DataTracker tracker) { + this.rng = rng; + this.schema = schema; + this.insertGen = OperationsGenerators.writeOp(schema); + this.queryBuilder = new QueryBuildingVisitExecutor(schema, QueryBuildingVisitExecutor.WrapQueries.UNLOGGED_BATCH); + this.model = new QuiescentChecker(schema.valueGenerators, tracker, new Model.Replay() + { + @Override + public Visit replay(long lts) + { + return log.get(lts); + } + + @Override + public Operations.Operation replay(long lts, int opId) + { + return log.get(lts).operations[opId]; + } + + @Override + public Iterator iterator() + { + List visited = new ArrayList<>(log.keySet()); + visited.sort(Long::compare); + return new Iterator<>() + { + int idx = 0; + + @Override + public boolean hasNext() + { + return idx < visited.size(); + } + + @Override + public Visit next() + { + return replay(visited.get(idx++)); + } + }; + } + }); + this.simulated = simulated; this.scheduler = scheduler; this.cluster = cluster; - this.harryRun = run; Options options = noActions(cluster.size()); this.clusterActions = new ClusterActions(simulated, cluster, options, new NoOpListener(), new Debug(new EnumMap<>(Debug.Info.class), new int[0])); this.nodeState = nodeState.apply(this); this.schedule = schedule; + + this.log = log; + this.ltsGen = ltsGen; + this.tracker = tracker; } public HarrySimulation withScheduler(RunnableActionScheduler scheduler) { - return new HarrySimulation(simulated, scheduler, cluster, harryRun, (ignore) -> nodeState, schedule); + return new HarrySimulation(schema, rng, simulated, scheduler, cluster, (ignore) -> nodeState, schedule, log, ltsGen, tracker); } public HarrySimulation withSchedulers(Function> schedulers) @@ -423,12 +468,12 @@ public class HarrySimulatorTest perVerbFutureActionScheduler, this.simulated.debug, this.simulated.failures); - return new HarrySimulation(simulated, scheduler, cluster, harryRun, (ignore) -> nodeState, schedule); + return new HarrySimulation(schema, rng, simulated, scheduler, cluster, (ignore) -> nodeState, schedule, log, ltsGen, tracker); } public HarrySimulation withSchedule(Function schedule) { - return new HarrySimulation(simulated, scheduler, cluster, harryRun, (ignore) -> nodeState, schedule); + return new HarrySimulation(schema, rng, simulated, scheduler, cluster, (ignore) -> nodeState, schedule, log, ltsGen, tracker); } @Override @@ -470,13 +515,10 @@ public class HarrySimulatorTest static class HarrySimulationBuilder extends ClusterSimulation.Builder { - protected final Configuration.ConfigurationBuilder harryConfig; protected final Consumer configUpdater; - HarrySimulationBuilder(Configuration.ConfigurationBuilder harryConfig, - Consumer configUpdater) + HarrySimulationBuilder(Consumer configUpdater) { - this.harryConfig = harryConfig; this.configUpdater = configUpdater; } @@ -497,29 +539,22 @@ public class HarrySimulatorTest { RandomSource random = new RandomSource.Default(); random.reset(seed); - this.harryConfig.setSeed(seed); - return new ClusterSimulation<>(random, seed, 1, this, configUpdater, (simulated, scheduler, cluster, options) -> { - - InJvmSut sut = new InJvmSut(cluster) - { - public void shutdown() - { - // Let simulation shut down the cluster, as it uses `nanoTime` - } - }; - - Configuration configuration = harryConfig.setClock(() -> new OffsetClock(1000)) - .setSUT(() -> sut) - .build(); - return new HarrySimulation(simulated, + EntropySource rng = new JdkRandomEntropySource(seed); + SchemaSpec schema = schemaSpecGen("harry", "tbl").generate(rng); + return new HarrySimulation(schema, + rng, + simulated, scheduler, cluster, - configuration.createRun(), + SimulatedNodeState::new, // No work initially - (sim) -> new ActionSchedule.Work[0]); + (sim) -> new ActionSchedule.Work[0], + new HashMap<>(), + OperationsGenerators.lts(), + new DataTracker.SimpleDataTracker()); }); } } @@ -529,13 +564,12 @@ public class HarrySimulatorTest */ void simulate(Consumer> configure, Consumer instanceConfigUpdater, - Configuration.ConfigurationBuilder harryConfig, String[] properties, Function... phases) throws IOException { try (WithProperties p = new WithProperties().with(properties)) { - HarrySimulationBuilder factory = new HarrySimulationBuilder(harryConfig, instanceConfigUpdater); + HarrySimulationBuilder factory = new HarrySimulationBuilder(instanceConfigUpdater); SimulationRunner.beforeAll(); long seed = SimulationRunner.parseHex(Optional.ofNullable(this.seed)).orElseGet(() -> new Random().nextLong()); @@ -569,7 +603,6 @@ public class HarrySimulatorTest { Set extremelyLossy = new HashSet<>(Arrays.asList(Verb.TCM_ABORT_MIG, Verb.TCM_REPLICATION, Verb.TCM_COMMIT_REQ, Verb.TCM_NOTIFY_REQ, - Verb.TCM_FETCH_CMS_LOG_REQ, Verb.TCM_FETCH_PEER_LOG_REQ, Verb.TCM_INIT_MIG_REQ, Verb.TCM_INIT_MIG_RSP, Verb.TCM_DISCOVER_REQ, Verb.TCM_DISCOVER_RSP)); @@ -578,6 +611,7 @@ public class HarrySimulatorTest Set somewhatLossy = new HashSet<>(Arrays.asList(Verb.TCM_CURRENT_EPOCH_REQ, Verb.TCM_NOTIFY_RSP, Verb.TCM_FETCH_CMS_LOG_RSP, Verb.TCM_FETCH_PEER_LOG_RSP, Verb.TCM_COMMIT_RSP, + Verb.TCM_FETCH_CMS_LOG_REQ, Verb.TCM_FETCH_PEER_LOG_REQ, Verb.PAXOS2_COMMIT_REMOTE_REQ, Verb.PAXOS2_COMMIT_REMOTE_RSP, Verb.PAXOS2_PREPARE_REQ, Verb.PAXOS2_PREPARE_RSP, Verb.PAXOS2_PROPOSE_REQ, Verb.PAXOS2_PROPOSE_RSP, @@ -713,74 +747,101 @@ public class HarrySimulatorTest /** * Creates an action list with a fixed number of data-generating operations that conform to the given Harry configuration. */ - public static ActionList generate(int ops, HarrySimulation simulation, ConsistencyLevel cl) + public static ActionList generateWrites(int ops, HarrySimulation simulation, ConsistencyLevel cl) { Action[] actions = new Action[ops]; OrderOn orderOn = new OrderOn.Strict(actions, 2); - generate(ops, simulation, new Consumer() - { - int i = 0; + for (int i = 0; i < ops; i++) + { + long lts = simulation.ltsGen.generate(simulation.rng); - public void accept(Action action) - { - actions[i++] = action; - } - }, - cl); + Visit visit = new Visit(lts, new Operations.Operation[]{ simulation.insertGen.generate(simulation.rng).toOp(lts) }); + Visit prev_ = simulation.log.put(lts, visit); + Invariants.checkState(prev_ == null); + + actions[i] = new Actions.LambdaAction("", Action.Modifiers.RELIABLE_NO_TIMEOUTS, () -> { + CompiledStatement compiledStatement = simulation.queryBuilder.compile(visit); + DataTracker tracker = simulation.tracker; + + RetryingQuery query = new RetryingQuery(compiledStatement.cql(), cl, compiledStatement.bindings()); + Action wrapper = new SimulatedActionCallable<>("Query", + Action.Modifiers.RELIABLE_NO_TIMEOUTS, + Action.Modifiers.RELIABLE_NO_TIMEOUTS, + simulation.simulated, + simulation.cluster.get((int) ((lts % simulation.cluster.size()) + 1)), + query) + { + @Override + protected InterceptedExecution.InterceptedTaskExecution task() + { + return new InterceptedExecution.InterceptedTaskExecution((InterceptingExecutor) on.executor()) + { + public void run() + { + tracker.begin(visit); + System.out.println("Started visit = " + visit); + // we'll be invoked on the node's executor, but we need to ensure the task is loaded on its classloader + try + { + accept(on.unsafeCallOnThisThread(execute), null); + } + catch (Throwable t) + { + accept(null, t); + } + finally + { + execute = null; + } + } + }; + } + + @Override + public void accept(Object[][] result, Throwable failure) + { + if (failure != null) + simulated.failures.accept(failure); + else + { + System.out.println("Finished visit = " + visit); + tracker.end(visit); + } + } + }; + + return ActionList.of(wrapper); + }); + } return ActionList.of(actions).orderOn(orderOn); } - public static void generate(int ops, HarrySimulation simulation, Consumer add, org.apache.cassandra.distributed.api.ConsistencyLevel cl) + public static class RetryingQuery extends Query { - SimulatedVisitExectuor visitExectuor = new SimulatedVisitExectuor(simulation, - simulation.harryRun, - cl); - GeneratingVisitor generatingVisitor = new GeneratingVisitor(simulation.harryRun, visitExectuor); - - for (int i = 0; i < ops; i++) + public RetryingQuery(String query, ConsistencyLevel cl, Object[] boundValues) { - generatingVisitor.visit(simulation.harryRun.clock.nextLts()); - // A tiny chance of executing a multi-partition batch - if (ops % 10 == 0) - generatingVisitor.visit(simulation.harryRun.clock.nextLts()); - add.accept(visitExectuor.build()); + super(query, -1, cl, null, boundValues); } - } - - /** - * Create an infinite stream to generate data. - */ - public static Supplier generate(HarrySimulation simulation, org.apache.cassandra.distributed.api.ConsistencyLevel cl) - { - SimulatedVisitExectuor visitExectuor = new SimulatedVisitExectuor(simulation, - simulation.harryRun, - cl); - GeneratingVisitor generatingVisitor = new GeneratingVisitor(simulation.harryRun, visitExectuor); - - DefaultDataTracker tracker = (DefaultDataTracker) simulation.harryRun.tracker; - return new Supplier() + @Override + public Object[][] call() { - public Action get() + while (true) { - // Limit how many queries can be in-flight simultaneously to reduce noise - if (tracker.maxStarted() - tracker.maxConsecutiveFinished() == 0) + try { - generatingVisitor.visit(); - return visitExectuor.build(); + return super.call(); } - else + catch (UncheckedInterruptedException e) { - // No-op - return run(() -> {}); + throw new RuntimeException(e); + } + catch (Throwable t) + { + logger.error("Caught error while executing query. Will ignore and retry: " + t.getMessage()); } } - - public String toString() - { - return "Query Generator"; - } - }; + } } /** @@ -791,24 +852,37 @@ public class HarrySimulatorTest { return new Actions.LambdaAction("Validate", Action.Modifiers.RELIABLE_NO_TIMEOUTS, () -> { - if (!simulation.harryRun.tracker.isFinished(simulation.harryRun.tracker.maxStarted())) - throw new IllegalStateException("Can not begin validation, as writing has not quiesced yet: " + simulation.harryRun.tracker); + if (!simulation.tracker.allFinished()) + throw new IllegalStateException("Can not begin validation, as writing has not quiesced yet: " + simulation.tracker); + + logger.warn("Starting validation. Ring view: {}", simulation.nodeState); + Set pds = visitedPds(simulation); List actions = new ArrayList<>(); - long maxLts = simulation.harryRun.tracker.maxStarted(); - long maxPosition = simulation.harryRun.pdSelector.maxPosition(maxLts); - logger.warn("Starting validation of {} written partitions. Highest LTS is {}. Ring view: {}", maxPosition, maxLts, simulation.nodeState); - for (int position = 0; position < maxPosition; position++) + for (Long pd : pds) { - long minLts = simulation.harryRun.pdSelector.minLtsAt(position); - long pd = simulation.harryRun.pdSelector.pd(minLts, simulation.harryRun.schemaSpec); - Query query = Query.selectAllColumns(simulation.harryRun.schemaSpec, pd, false); - actions.add(new HarryValidatingQuery(simulation.simulated, simulation.cluster, rf, - simulation.harryRun, owernship, query)); + Operations.SelectPartition select = new Operations.SelectPartition(Long.MAX_VALUE, pd); + actions.add(new HarryValidatingQuery(simulation, simulation.cluster, rf, + owernship, new Visit(Long.MAX_VALUE, new Operations.Operation[]{ select }), + simulation.queryBuilder)); } return ActionList.of(actions).setStrictlySequential(); }); } + private static Set visitedPds(HarrySimulation simulation) + { + Set pds = new HashSet<>(); + for (Visit visit : simulation.log.values()) + { + for (Operations.Operation operation : visit.operations) + { + if (operation instanceof Operations.PartitionOperation) + pds.add(((Operations.PartitionOperation) operation).pd); + } + } + return pds; + } + private static ActionSchedule.Work work(String toString, Action... actions) { return new ActionSchedule.Work(UNLIMITED, Collections.singletonList(ActionList.of(actions).setStrictlySequential())) { @@ -963,29 +1037,9 @@ public class HarrySimulatorTest return arr; } - // Use only types that can guarantee we will et 64 bits of entropy here, at least for now. - public static Surjections.Surjection schemaSpecGen(String keyspace, String prefix) + public static Generator schemaSpecGen(String keyspace, String prefix) { - AtomicInteger counter = new AtomicInteger(); - return new SchemaGenerators.Builder(keyspace, () -> prefix + counter.getAndIncrement()) - .partitionKeySpec(1, 2, - ColumnSpec.int64Type, - ColumnSpec.asciiType, - ColumnSpec.textType) - .clusteringKeySpec(1, 1, - ColumnSpec.int64Type, - ColumnSpec.asciiType, - ColumnSpec.textType, - ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), - ColumnSpec.ReversedType.getInstance(ColumnSpec.asciiType), - ColumnSpec.ReversedType.getInstance(ColumnSpec.textType)) - .regularColumnSpec(5, 5, - ColumnSpec.int64Type, - ColumnSpec.asciiType(4, 128)) - .staticColumnSpec(5, 5, - ColumnSpec.int64Type, - ColumnSpec.asciiType(4, 128)) - .surjection(); + return SchemaGenerators.schemaSpecGen(keyspace, prefix, 1000); } public static class HaltOnError extends Failures diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/HarryValidatingQuery.java b/test/simulator/test/org/apache/cassandra/simulator/test/HarryValidatingQuery.java new file mode 100644 index 0000000000..2a011c944c --- /dev/null +++ b/test/simulator/test/org/apache/cassandra/simulator/test/HarryValidatingQuery.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.simulator.test; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.utils.Invariants; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInstance; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; +import org.apache.cassandra.harry.execution.ResultSetRow; +import org.apache.cassandra.harry.model.TokenPlacementModel; +import org.apache.cassandra.harry.util.ByteUtils; +import org.apache.cassandra.harry.util.TokenUtil; +import org.apache.cassandra.simulator.systems.InterceptedExecution; +import org.apache.cassandra.simulator.systems.InterceptingExecutor; +import org.apache.cassandra.simulator.systems.SimulatedAction; + +public class HarryValidatingQuery extends SimulatedAction +{ + private static final Logger logger = LoggerFactory.getLogger(HarryValidatingQuery.class); + + private final InterceptingExecutor on; + private final Cluster cluster; + private final List owernship; + private final TokenPlacementModel.ReplicationFactor rf; + + private final HarrySimulatorTest.HarrySimulation simulation; + private final Visit visit; + private final QueryBuildingVisitExecutor queryBuilder; + + public HarryValidatingQuery(HarrySimulatorTest.HarrySimulation simulation, + Cluster cluster, + TokenPlacementModel.ReplicationFactor rf, + List owernship, + Visit visit, + QueryBuildingVisitExecutor queryBuilder) + { + super(visit, Modifiers.RELIABLE_NO_TIMEOUTS, Modifiers.RELIABLE_NO_TIMEOUTS, null, simulation.simulated); + this.rf = rf; + this.cluster = cluster; + this.on = (InterceptingExecutor) cluster.get(1).executor(); + this.owernship = owernship; + this.visit = visit; + this.queryBuilder = queryBuilder; + this.simulation = simulation; + + } + + protected InterceptedExecution task() + { + return new InterceptedExecution.InterceptedTaskExecution(on) + { + public void run() + { + try + { + TokenPlacementModel.ReplicatedRanges ring = rf.replicate(owernship); + Invariants.checkState(visit.operations.length == 1); + Invariants.checkState(visit.operations[0] instanceof Operations.SelectStatement); + Operations.SelectStatement select = (Operations.SelectStatement) visit.operations[0]; + for (TokenPlacementModel.Replica replica : ring.replicasFor(token(select.pd))) + { + CompiledStatement compiled = queryBuilder.compile(visit); + Object[][] objects = executeNodeLocal(compiled.cql(), replica.node(), compiled.bindings()); + List actualRows = InJvmDTestVisitExecutor.rowsToResultSet(simulation.schema, select, objects); + simulation.model.validate(select, actualRows); + } + } + catch (Throwable t) + { + logger.error("Caught an exception while validating", t); + throw t; + } + } + }; + } + + protected long token(long pd) + { + return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(simulation.schema.valueGenerators.pkGen().inflate(pd)))); + } + + protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) + { + IInstance instance = cluster + .stream() + .filter((n) -> n.config().broadcastAddress().toString().equals(node.id())) + .findFirst() + .get(); + return instance.executeInternal(statement, bindings); + } +} diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java b/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java deleted file mode 100644 index d676ec8409..0000000000 --- a/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java +++ /dev/null @@ -1,175 +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.simulator.test; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.harry.core.Run; -import org.apache.cassandra.harry.operations.CompiledStatement; -import org.apache.cassandra.harry.tracker.DataTracker; -import org.apache.cassandra.harry.visitors.MutatingRowVisitor; -import org.apache.cassandra.harry.visitors.VisitExecutor; -import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.distributed.impl.Query; -import org.apache.cassandra.simulator.Action; -import org.apache.cassandra.simulator.systems.InterceptedExecution; -import org.apache.cassandra.simulator.systems.InterceptingExecutor; -import org.apache.cassandra.simulator.systems.SimulatedActionCallable; -import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; - -/** - * Point of integration between Harry and Simulator. Creates series of stictly-ordered tasks that constitute a visit - * of a single LTS. - */ -public class SimulatedVisitExectuor extends VisitExecutor -{ - private static final Logger logger = LoggerFactory.getLogger(SimulatedVisitExectuor.class); - - private Action action = null; - private final List lts = new ArrayList<>(); - private final List statements = new ArrayList<>(); - private final List bindings = new ArrayList<>(); - private final MutatingRowVisitor rowVisitor; - private final DataTracker tracker; - private final HarrySimulatorTest.HarrySimulation simulation; - private final ConsistencyLevel cl; - - public SimulatedVisitExectuor(HarrySimulatorTest.HarrySimulation simulation, - Run run, - ConsistencyLevel cl) - { - this.rowVisitor = new MutatingRowVisitor(run); - this.simulation = simulation; - this.tracker = run.tracker; - this.cl = cl; - } - - public Action build() - { - String query = String.join(" ", statements); - - if (statements.size() > 1) - query = String.format("BEGIN BATCH\n%s\nAPPLY BATCH;", query); - - Object[] bindingsArray = new Object[bindings.size()]; - bindings.toArray(bindingsArray); - - action = new SimulatedActionCallable("Batch", - Action.Modifiers.RELIABLE_NO_TIMEOUTS, - Action.Modifiers.RELIABLE_NO_TIMEOUTS, - simulation.simulated, - simulation.cluster.get((int) ((lts.get(0) % simulation.cluster.size()) + 1)), - new RetryingQuery(query, cl, bindingsArray)) - { - private final List localLts = new ArrayList<>(SimulatedVisitExectuor.this.lts); - - @Override - protected InterceptedExecution.InterceptedTaskExecution task() - { - return new InterceptedExecution.InterceptedTaskExecution((InterceptingExecutor) on.executor()) - { - public void run() - { - for (Long l : localLts) - tracker.beginModification(l); - - // we'll be invoked on the node's executor, but we need to ensure the task is loaded on its classloader - try { accept(on.unsafeCallOnThisThread(execute), null); } - catch (Throwable t) { accept(null, t); } - finally { execute = null; } - } - }; - } - - @Override - public void accept(Object[][] result, Throwable failure) - { - if (failure != null) - simulated.failures.accept(failure); - else - for (Long l : localLts) - tracker.endModification(l); - } - }; - - statements.clear(); - bindings.clear(); - lts.clear(); - - Action current = action; - action = null; - return current; - } - - @Override - protected void beforeLts(long lts, long pd) - { - this.lts.add(lts); - } - - @Override - protected void afterLts(long lts, long pd) - { - } - - @Override - protected void operation(Operation operation) - { - CompiledStatement statement = rowVisitor.perform(operation); - statements.add(statement.cql()); - Collections.addAll(bindings, statement.bindings()); - } - - public void shutdown() throws InterruptedException - { - } - - private static class RetryingQuery extends Query - { - public RetryingQuery(String query, ConsistencyLevel cl, Object[] boundValues) - { - super(query, -1, cl, null, boundValues); - } - - @Override - public Object[][] call() - { - while (true) - { - try - { - return super.call(); - } - catch (UncheckedInterruptedException e) - { - throw new RuntimeException(e); - } - catch (Throwable t) - { - logger.error("Caught error while executing query. Will ignore and retry: " + t.getMessage()); - } - } - } - } -} \ No newline at end of file diff --git a/test/unit/accord/utils/Invariants.java b/test/unit/accord/utils/Invariants.java index b960d8b268..2977272d4a 100644 --- a/test/unit/accord/utils/Invariants.java +++ b/test/unit/accord/utils/Invariants.java @@ -22,6 +22,7 @@ import net.nicoulaj.compilecommand.annotations.Inline; import javax.annotation.Nullable; import java.util.function.Predicate; +import java.util.function.Supplier; import static java.lang.String.format; @@ -39,9 +40,14 @@ public class Invariants return DEBUG; } - private static void illegalState(String msg) + public static IllegalStateException createIllegalState(String msg) { - throw new IllegalStateException(msg); + return new IllegalStateException(msg); + } + + public static IllegalStateException illegalState(String msg) + { + throw createIllegalState(msg); } private static void illegalState() @@ -91,6 +97,12 @@ public class Invariants illegalState(); } + public static void checkState(boolean condition, Supplier msg) + { + if (!condition) + throw illegalState(msg.get()); + } + public static void checkState(boolean condition, String msg) { if (!condition) diff --git a/test/unit/accord/utils/Property.java b/test/unit/accord/utils/Property.java index e45642c1f1..e6d0d1fcc9 100644 --- a/test/unit/accord/utils/Property.java +++ b/test/unit/accord/utils/Property.java @@ -19,6 +19,7 @@ package accord.utils; import accord.utils.async.TimeoutUtils; +import org.agrona.collections.LongArrayList; import java.time.Duration; import java.util.ArrayList; @@ -32,6 +33,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -212,9 +214,21 @@ public class Property String stateStr = state == null ? null : state.toString().replace("\n", "\n\t\t"); sb.append("\tState: ").append(stateStr).append(": ").append(state == null ? "unknown type" : state.getClass().getCanonicalName()).append('\n'); sb.append("\tHistory:").append('\n'); + addList(sb, "\t\t", history); + return sb.toString(); + } + + private static void addList(StringBuilder sb, String prefix, List list) + { int idx = 0; - for (var event : history) - sb.append("\t\t").append(++idx).append(": ").append(event).append('\n'); + for (var event : list) + sb.append(prefix).append(++idx).append(": ").append(event).append('\n'); + } + + public static String formatList(String prefix, List list) + { + StringBuilder sb = new StringBuilder(); + addList(sb, prefix, list); return sb.toString(); } @@ -432,6 +446,7 @@ public class Property { State state = null; List history = new ArrayList<>(steps); + LongArrayList historyTiming = stepTimeout == null ? null : new LongArrayList(); try { checkInterrupted(); @@ -456,17 +471,18 @@ public class Property for (Command sub : ((MultistepCommand) cmd)) { history.add(sub.detailed(state)); - process(sub, state, sut, history.size()); + process(sub, state, sut, history.size(), historyTiming); } } else { history.add(cmd.detailed(state)); - process(cmd, state, sut, history.size()); + process(cmd, state, sut, history.size(), historyTiming); } } commands.destroySut(sut, null); commands.destroyState(state, null); + commands.onSuccess(state, sut, maybeRewriteHistory(history, historyTiming)); } catch (Throwable t) { @@ -484,7 +500,8 @@ public class Property } catch (Throwable t) { - throw new PropertyError(statefulPropertyError(this, t, state, history), t); + + throw new PropertyError(statefulPropertyError(this, t, state, maybeRewriteHistory(history, historyTiming)), t); } if (pure) { @@ -494,14 +511,35 @@ public class Property } } - private void process(Command cmd, State state, SystemUnderTest sut, int id) throws Throwable + private static List maybeRewriteHistory(List history, @Nullable LongArrayList historyTiming) + { + if (historyTiming == null) return history; + List newHistory = new ArrayList<>(history.size()); + for (int i = 0; i < history.size(); i++) + { + String step = history.get(i); + long timeNanos = historyTiming.getLong(i); + newHistory.add(step + ";\tDuration " + Duration.ofNanos(timeNanos)); + } + return newHistory; + } + + private void process(Command cmd, State state, SystemUnderTest sut, int id, @Nullable LongArrayList stepTiming) throws Throwable { if (stepTimeout == null) { cmd.process(state, sut); return; } - TimeoutUtils.runBlocking(stepTimeout, "Stateful Step " + id, () -> cmd.process(state, sut)); + long startNanos = System.nanoTime(); + try + { + TimeoutUtils.runBlocking(stepTimeout, "Stateful Step " + id + ": " + cmd.detailed(state), () -> cmd.process(state, sut)); + } + finally + { + stepTiming.add(System.nanoTime() - startNanos); + } } } @@ -517,7 +555,59 @@ public class Property default void process(State state, SystemUnderTest sut) throws Throwable { checkPostconditions(state, apply(state), - sut, run(sut)); + sut, run(sut)); + } + } + + public static class ForwardingCommand implements Command + { + private final Command delegate; + + public ForwardingCommand(Command delegate) + { + this.delegate = delegate; + } + + protected Command delegate() + { + return delegate; + } + + @Override + public PreCheckResult checkPreconditions(State state) + { + return delegate().checkPreconditions(state); + } + + @Override + public Result apply(State state) throws Throwable + { + return delegate().apply(state); + } + + @Override + public Result run(SystemUnderTest sut) throws Throwable + { + return delegate().run(sut); + } + + @Override + public void checkPostconditions(State state, Result expected, SystemUnderTest sut, Result actual) throws Throwable + { + delegate().checkPostconditions(state, expected, sut, actual); + } + + @Override + public String detailed(State state) + { + return delegate().detailed(state); + } + + @Override + public void process(State state, SystemUnderTest sut) throws Throwable + { + // don't call delegate here else the process function calls the delegate and not this class + Command.super.process(state, sut); } } @@ -682,6 +772,7 @@ public class Property { Gen genInitialState() throws Throwable; SystemUnderTest createSut(State state) throws Throwable; + default void onSuccess(State state, SystemUnderTest sut, List history) throws Throwable {} default void destroyState(State state, @Nullable Throwable cause) throws Throwable {} default void destroySut(SystemUnderTest sut, @Nullable Throwable cause) throws Throwable {} Gen> commands(State state) throws Throwable; @@ -697,6 +788,11 @@ public class Property return new CommandsBuilder<>(stateGen, ignore -> null); } + public interface StatefulSuccess + { + void apply(State state, SystemUnderTest sut, List history) throws Throwable; + } + public static class CommandsBuilder { public interface Setup @@ -708,6 +804,8 @@ public class Property private final Map, Integer> knownWeights = new LinkedHashMap<>(); @Nullable private Set> unknownWeights = null; + @Nullable + private Map, List>> conditionalCommands = null; private Gen.IntGen unknownWeightGen = Gens.ints().between(1, 10); @Nullable private FailingConsumer preCommands = null; @@ -715,6 +813,9 @@ public class Property private FailingBiConsumer destroyState = null; @Nullable private FailingBiConsumer destroySut = null; + @Nullable + private BiFunction>, Gen>> commandsTransformer = null; + private final List> onSuccess = new ArrayList<>(); public CommandsBuilder(Supplier> stateGen, Function sutFactory) { @@ -792,18 +893,15 @@ public class Property public CommandsBuilder addIf(Predicate predicate, Gen> cmd) { - return add((rs, state) -> { - if (!predicate.test(state)) return ignoreCommand(); - return cmd.next(rs); - }); + return addIf(predicate, (rs, state) -> cmd.next(rs)); } public CommandsBuilder addIf(Predicate predicate, Setup cmd) { - return add((rs, state) -> { - if (!predicate.test(state)) return ignoreCommand(); - return cmd.setup(rs, state); - }); + if (conditionalCommands == null) + conditionalCommands = new LinkedHashMap<>(); + conditionalCommands.computeIfAbsent(predicate, i -> new ArrayList<>()).add(cmd); + return this; } public CommandsBuilder addAllIf(Predicate predicate, Consumer> sub) @@ -816,6 +914,12 @@ public class Property CommandsBuilder.this.addIf(predicate, cmd); return this; } + + @Override + public IfBuilder addIf(Predicate nextPredicate, Setup cmd) { + CommandsBuilder.this.addIf(predicate.and(nextPredicate), cmd); + return this; + } }); return this; } @@ -823,6 +927,7 @@ public class Property public interface IfBuilder { IfBuilder add(Setup cmd); + IfBuilder addIf(Predicate predicate, Setup cmd); } public CommandsBuilder unknownWeight(Gen.IntGen unknownWeightGen) @@ -831,10 +936,22 @@ public class Property return this; } + public CommandsBuilder commandsTransformer(BiFunction>, Gen>> commandsTransformer) + { + this.commandsTransformer = commandsTransformer; + return this; + } + + public CommandsBuilder onSuccess(StatefulSuccess fn) + { + onSuccess.add(fn); + return this; + } + public Commands build() { Gen> commandsGen; - if (unknownWeights == null) + if (unknownWeights == null && conditionalCommands == null) { commandsGen = Gens.pick(new LinkedHashMap<>(knownWeights)); } @@ -842,25 +959,52 @@ public class Property { class DynamicWeightsGen implements Gen>, Gens.Reset { - Gen> gen; + LinkedHashMap, Integer> weights; + LinkedHashMap, Integer> conditionalWeights; + Gen> nonConditional; @Override public Setup next(RandomSource rs) { - if (gen == null) + if (weights == null) { // create random weights - LinkedHashMap, Integer> clone = new LinkedHashMap<>(knownWeights); - for (Setup s : unknownWeights) - clone.put(s, unknownWeightGen.nextInt(rs)); - gen = Gens.pick(clone); + weights = new LinkedHashMap<>(knownWeights); + if (unknownWeights != null) + { + for (Setup s : unknownWeights) + weights.put(s, unknownWeightGen.nextInt(rs)); + } + nonConditional = Gens.pick(weights); + if (conditionalCommands != null) + { + conditionalWeights = new LinkedHashMap<>(); + for (List> commands : conditionalCommands.values()) + { + for (Setup c : commands) + conditionalWeights.put(c, unknownWeightGen.nextInt(rs)); + } + } } - return gen.next(rs); + if (conditionalWeights == null) return nonConditional.next(rs); + return (r, s) -> { + // need to figure out what conditions apply... + LinkedHashMap, Integer> clone = new LinkedHashMap<>(weights); + for (Map.Entry, List>> e : conditionalCommands.entrySet()) + { + if (e.getKey().test(s)) + e.getValue().forEach(c -> clone.put(c, conditionalWeights.get(c))); + } + Setup select = Gens.pick(clone).next(r); + return select.setup(r, s); + }; } @Override public void reset() { - gen = null; + weights = null; + nonConditional = null; + conditionalWeights = null; } } commandsGen = new DynamicWeightsGen(); @@ -884,7 +1028,8 @@ public class Property { if (preCommands != null) preCommands.accept(state); - return commandsGen.map((rs, setup) -> setup.setup(rs, state)); + Gen> map = commandsGen.map((rs, setup) -> setup.setup(rs, state)); + return commandsTransformer == null ? map : commandsTransformer.apply(state, map); } @Override @@ -901,6 +1046,13 @@ public class Property if (destroySut != null) destroySut.accept(sut, cause); } + + @Override + public void onSuccess(State state, SystemUnderTest sut, List history) throws Throwable + { + for (var fn : onSuccess) + fn.apply(state, sut, history); + } }; } diff --git a/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java index c17067521b..e6d7243a46 100644 --- a/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java +++ b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.distributed.test.log.CMSTestBase; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; import org.apache.cassandra.schema.KeyspaceMetadata; @@ -37,7 +37,6 @@ import org.apache.cassandra.tcm.ownership.DataPlacement; import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.getLeavePlan; import static org.junit.Assert.assertTrue; - public class ClusterMetadataTest { @BeforeClass diff --git a/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java index 322acdc4ed..00c9d27441 100644 --- a/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java +++ b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java @@ -32,7 +32,7 @@ import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.distributed.test.log.CMSTestBase; import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.distributed.test.log.MetadataChangeSimulationTest; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.ReplicationParams; import org.apache.cassandra.tcm.AtomicLongBackedProcessor; diff --git a/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java b/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java index 91bd26d479..fe73677e25 100644 --- a/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java +++ b/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java @@ -43,7 +43,7 @@ import org.apache.cassandra.harry.gen.Surjections; import org.apache.cassandra.harry.gen.rng.PCGFastPure; import org.apache.cassandra.harry.gen.rng.PcgRSUFast; import org.apache.cassandra.harry.gen.rng.RngUtils; -import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.model.TokenPlacementModel; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.metrics.TCMMetrics; import org.apache.cassandra.net.ConnectionType;