diff --git a/.build/build-rat.xml b/.build/build-rat.xml index 65f79e2c24fc..39808eb2b226 100644 --- a/.build/build-rat.xml +++ b/.build/build-rat.xml @@ -60,6 +60,7 @@ + diff --git a/build.xml b/build.xml index cedd0c567dba..1366d13a9822 100644 --- a/build.xml +++ b/build.xml @@ -1939,7 +1939,7 @@ - + diff --git a/run_gen.sh b/run_gen.sh new file mode 100755 index 000000000000..40c20682774a --- /dev/null +++ b/run_gen.sh @@ -0,0 +1,31 @@ +#!/bin/bash +java \ + --add-exports java.base/jdk.internal.misc=ALL-UNNAMED \ + --add-exports java.base/java.lang.ref=ALL-UNNAMED \ + --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED \ + --add-exports java.management/com.sun.jmx.remote.security=ALL-UNNAMED \ + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED \ + --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED \ + --add-exports java.sql/java.sql=ALL-UNNAMED \ + --add-exports jdk.unsupported/sun.misc=ALL-UNNAMED \ + --add-opens java.base/java.io=ALL-UNNAMED \ + --add-opens java.base/java.lang=ALL-UNNAMED \ + --add-opens java.base/java.lang.module=ALL-UNNAMED \ + --add-opens java.base/java.lang.reflect=ALL-UNNAMED \ + --add-opens java.base/java.nio=ALL-UNNAMED \ + --add-opens java.base/java.util=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.math=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.module=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED \ + --add-opens java.base/sun.nio.ch=ALL-UNNAMED \ + --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED \ + -cp 'build/dtest-7.0.jar' \ + org.apache.cassandra.harry.stress.RangeAwareSSTableGenerator \ + --contact-points 127.0.0.1 \ + --schema test/resources/harry/stress/levelled-sstable-schema.yaml \ + --output-dir /tmp/range-sstables \ + --visits 10000000 \ + --sstable-size-mib 64 diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java index 723c773920d7..8eec2cd93e41 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java @@ -1097,6 +1097,17 @@ public TableMetadata metadata() return metadata; } + /** + * Returns the number of non-static rows accumulated so far. + * Accurate when rows are added in clustering order; may overcount with unordered input. + */ + public int rowCount() + { + if (rowBuilder != null) + return rowBuilder.count(); + return firstRow != null ? 1 : 0; + } + private static final UpdateFunction ROWS_MERGE_FUNCTION = UpdateFunction.Simple.of(Rows::merge); public PartitionUpdate build() diff --git a/src/java/org/apache/cassandra/db/virtual/DataPlacementsTable.java b/src/java/org/apache/cassandra/db/virtual/DataPlacementsTable.java new file mode 100644 index 000000000000..8fe3a7ec98ff --- /dev/null +++ b/src/java/org/apache/cassandra/db/virtual/DataPlacementsTable.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.virtual; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.ReversedLongLocalPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; + +final class DataPlacementsTable extends AbstractVirtualTable +{ + static final String TABLE_COMMENT = "Data placement information showing read and write endpoints per range"; + + static final String TABLE_NAME = "data_placements"; + + // Partition key columns + static final String KEYSPACE_NAME = "keyspace_name"; + static final String TABLE_NAME_COLUMN = "table_name"; + static final String RANGE_START = "range_start"; + static final String RANGE_END = "range_end"; + + // Regular columns + static final String RANGE_START_BYTES = "range_start_bytes"; + static final String RANGE_END_BYTES = "range_end_bytes"; + static final String TOKEN_TYPE = "token_type"; + static final String READ_ENDPOINTS = "read_endpoints"; + static final String WRITE_ENDPOINTS = "write_endpoints"; + static final String READ_REPLICAS = "read_replicas"; + static final String WRITE_REPLICAS = "write_replicas"; + + DataPlacementsTable(String keyspace) + { + super(buildTableMetadata(keyspace)); + } + + @Override + public DataSet data() + { + SimpleDataSet result = new SimpleDataSet(metadata()); + + ClusterMetadata metadata = ClusterMetadata.current(); + DataPlacements placements = metadata.placements; + + for (KeyspaceMetadata ksm : metadata.schema.getKeyspaces()) + { + ReplicationParams params = ksm.params.replication; + DataPlacement placement = placements.get(params); + + for (TableMetadata table : ksm.tables) + addPlacementRows(result, ksm.name, table.name, params, placement, metadata, null, null); + } + + return result; + } + + @Override + public DataSet data(DecoratedKey partitionKey) + { + SimpleDataSet result = new SimpleDataSet(metadata()); + + ByteBuffer keyBytes = partitionKey.getKey(); + + Preconditions.checkArgument(metadata().partitionKeyType instanceof CompositeType, + "Expected CompositeType partition key, got %s", metadata().partitionKeyType.getClass()); + + ByteBuffer[] components = ((CompositeType) metadata().partitionKeyType).split(keyBytes); + Preconditions.checkArgument(components.length == 1 || components.length == 2 || components.length == 4, + "Expected 1, 2, or 4 partition key components (keyspace[, table[, range_start, range_end]]), got %d", + components.length); + + String keyspaceName = UTF8Type.instance.compose(components[0]); + String tableName = components.length >= 2 ? UTF8Type.instance.compose(components[1]) : null; + String rangeStartFilter = components.length == 4 ? UTF8Type.instance.compose(components[2]) : null; + String rangeEndFilter = components.length == 4 ? UTF8Type.instance.compose(components[3]) : null; + + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ksm = metadata.schema.getKeyspaces().getNullable(keyspaceName); + if (ksm == null) + return result; + + ReplicationParams params = ksm.params.replication; + DataPlacement placement = metadata.placements.get(params); + Preconditions.checkState(placement != null, "Placements should never be null for %s", keyspaceName); + + if (tableName != null) + { + TableMetadata table = ksm.getTableOrViewNullable(tableName); + Preconditions.checkArgument(table != null, "Couldn't find table %s", tableName); + addPlacementRows(result, ksm.name, table.name, params, placement, metadata, rangeStartFilter, rangeEndFilter); + } + else + { + for (TableMetadata table : ksm.tables) + addPlacementRows(result, ksm.name, table.name, params, placement, metadata, null, null); + } + + return result; + } + + private void addPlacementRows(SimpleDataSet result, String keyspaceName, String tableName, ReplicationParams params, DataPlacement placement, ClusterMetadata metadata, String rangeStartFilter, String rangeEndFilter) + { + Preconditions.checkState(placement.reads.ranges.equals(placement.writes.ranges), + "Read ranges (%s) are not the same as write ranges (%s)", + placement.reads.ranges, placement.writes.ranges); + + List> ranges = placement.reads.ranges; + for (int i = 0; i < ranges.size(); i++) + { + Range range = ranges.get(i); + if (rangeStartFilter != null && !range.left.toString().equals(rangeStartFilter)) + continue; + if (rangeEndFilter != null && !range.right.toString().equals(rangeEndFilter)) + continue; + + VersionedEndpoints.ForRange readEndpoints = placement.reads.forRange(range); + VersionedEndpoints.ForRange writeEndpoints = placement.writes.forRange(range); + + Set readEndpointSet = toEndpointStrings(readEndpoints.get()); + Set writeEndpointSet = toEndpointStrings(writeEndpoints.get()); + + Set readReplicaSet = toNodeIds(readEndpoints.get(), metadata); + Set writeReplicaSet = toNodeIds(writeEndpoints.get(), metadata); + + IPartitioner partitioner = params.isMeta() ? ReversedLongLocalPartitioner.instance : metadata.partitioner; + result.row(keyspaceName, tableName, range.left.toString(), range.right.toString()) + .column(TOKEN_TYPE, range.left.getClass().getCanonicalName()) + .column(RANGE_START_BYTES, partitioner.getTokenFactory().toByteArray(range.left)) + .column(RANGE_END_BYTES, partitioner.getTokenFactory().toByteArray(range.right)) + .column(READ_ENDPOINTS, readEndpointSet) + .column(WRITE_ENDPOINTS, writeEndpointSet) + .column(READ_REPLICAS, readReplicaSet) + .column(WRITE_REPLICAS, writeReplicaSet); + } + } + + static Set toEndpointStrings(EndpointsForRange endpoints) + { + return endpoints.stream() + .map(Replica::endpoint) + .map(Object::toString) + .collect(Collectors.toSet()); + } + + static Set toNodeIds(EndpointsForRange endpoints, ClusterMetadata metadata) + { + return endpoints.stream() + .map(Replica::endpoint) + .map(metadata.directory::peerId) + .map(NodeId::id) + .collect(Collectors.toSet()); + } + + private static TableMetadata buildTableMetadata(String keyspace) + { + return TableMetadata.builder(keyspace, TABLE_NAME) + .comment(TABLE_COMMENT) + .kind(TableMetadata.Kind.VIRTUAL) + .partitioner(new LocalPartitioner(CompositeType.getInstance(UTF8Type.instance, UTF8Type.instance, UTF8Type.instance, UTF8Type.instance))) + .addPartitionKeyColumn(KEYSPACE_NAME, UTF8Type.instance) + .addPartitionKeyColumn(TABLE_NAME_COLUMN, UTF8Type.instance) + .addPartitionKeyColumn(RANGE_START, UTF8Type.instance) + .addPartitionKeyColumn(RANGE_END, UTF8Type.instance) + .addRegularColumn(TOKEN_TYPE, UTF8Type.instance) + .addRegularColumn(RANGE_START_BYTES, BytesType.instance) + .addRegularColumn(RANGE_END_BYTES, BytesType.instance) + .addRegularColumn(READ_ENDPOINTS, SetType.getInstance(UTF8Type.instance, false)) + .addRegularColumn(WRITE_ENDPOINTS, SetType.getInstance(UTF8Type.instance, false)) + .addRegularColumn(READ_REPLICAS, SetType.getInstance(Int32Type.instance, false)) + .addRegularColumn(WRITE_REPLICAS, SetType.getInstance(Int32Type.instance, false)) + .build(); + } +} diff --git a/src/java/org/apache/cassandra/db/virtual/PartitionLocationTable.java b/src/java/org/apache/cassandra/db/virtual/PartitionLocationTable.java new file mode 100644 index 000000000000..af4d239b8828 --- /dev/null +++ b/src/java/org/apache/cassandra/db/virtual/PartitionLocationTable.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.virtual; + +import java.nio.ByteBuffer; +import java.util.Set; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.ReversedLongLocalPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; + +import static com.google.common.base.Preconditions.*; + +final class PartitionLocationTable extends AbstractVirtualTable +{ + static final String TABLE_NAME = "partition_location"; + static final String TABLE_COMMENT = "shows the token range and replicas (read and write) for a given partition"; + + // Partition keys + static final String COLUMN_KEYSPACE_NAME = "keyspace_name"; + static final String COLUMN_TABLE_NAME = "table_name"; + static final String COLUMN_KEY = "key"; + + // Regular columns + static final String COLUMN_TOKEN = "tkn"; // can't use "token" because this is reserved in CQL + static final String COLUMN_TOKEN_BYTES = "tkn_bytes"; + static final String COLUMN_RANGE_START = "range_start"; + static final String COLUMN_RANGE_END = "range_end"; + static final String COLUMN_RANGE_START_BYTES = "range_start_bytes"; + static final String COLUMN_RANGE_END_BYTES = "range_end_bytes"; + static final String COLUMN_READ_ENDPOINTS = "read_endpoints"; + static final String COLUMN_WRITE_ENDPOINTS = "write_endpoints"; + static final String COLUMN_READ_REPLICAS = "read_replicas"; + static final String COLUMN_WRITE_REPLICAS = "write_replicas"; + + PartitionLocationTable(String keyspace) + { + super(buildTableMetadata(keyspace)); + } + + @Override + public DataSet data() + { + throw new InvalidRequestException("Partition location table requires a partition key, for example: " + + "SELECT * FROM system_views.partition_location WHERE keyspace_name = 'ks' AND table_name = 'tbl' AND key = '1:a'"); + } + + @Override + public DataSet data(DecoratedKey partitionKey) + { + SimpleDataSet result = new SimpleDataSet(metadata()); + + // Partition key is (keyspace_name, table_name, key) + ByteBuffer keyBytes = partitionKey.getKey(); + + checkArgument(metadata().partitionKeyType instanceof CompositeType, + "PartitionLocationTable partition key type must be CompositeType, got %s", + metadata().partitionKeyType.getClass().getName()); + + ByteBuffer[] components = ((CompositeType) metadata().partitionKeyType).split(keyBytes); + checkArgument(components.length == 3, + "PartitionLocationTable partition key must have exactly 3 components: keyspace_name, table_name, key; got %d", + components.length); + + String keyspaceName = UTF8Type.instance.compose(components[0]); + String tableName = UTF8Type.instance.compose(components[1]); + String keyString = UTF8Type.instance.compose(components[2]); + + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ksm = checkNotNull(metadata.schema.getKeyspaceMetadata(keyspaceName), + "Keyspace %s is not found in metadata", keyspaceName); + TableMetadata table = checkNotNull(ksm.getTableOrViewNullable(tableName), + "Table %s is not found in metadata (within keyspace %s)", tableName, keyspaceName); + + ByteBuffer partitionKeyBytes = table.partitionKeyType.fromString(keyString); + + DecoratedKey dk = table.partitioner.decorateKey(partitionKeyBytes); + Token token = dk.getToken(); + + ReplicationParams replicationParams = ksm.params.replication; + DataPlacement placement = metadata.placements.get(replicationParams); + + VersionedEndpoints.ForRange readEndpoints = placement.reads.forRange(token); + VersionedEndpoints.ForRange writeEndpoints = placement.writes.forRange(token); + + Range range = readEndpoints.get().range(); + + Set readEndpointSet = DataPlacementsTable.toEndpointStrings(readEndpoints.get()); + Set writeEndpointSet = DataPlacementsTable.toEndpointStrings(writeEndpoints.get()); + + Set readReplicaSet = DataPlacementsTable.toNodeIds(readEndpoints.get(), metadata); + Set writeReplicaSet = DataPlacementsTable.toNodeIds(writeEndpoints.get(), metadata); + + IPartitioner partitioner = replicationParams.isMeta() ? ReversedLongLocalPartitioner.instance : metadata.partitioner; + result.row(keyspaceName, tableName, keyString) + .column(COLUMN_TOKEN, token.toString()) + .column(COLUMN_TOKEN_BYTES, partitioner.getTokenFactory().toByteArray(token)) + .column(COLUMN_RANGE_START, range.left.toString()) + .column(COLUMN_RANGE_END, range.right.toString()) + .column(COLUMN_RANGE_START_BYTES, partitioner.getTokenFactory().toByteArray(range.left)) + .column(COLUMN_RANGE_END_BYTES, partitioner.getTokenFactory().toByteArray(range.right)) + .column(COLUMN_READ_ENDPOINTS, readEndpointSet) + .column(COLUMN_WRITE_ENDPOINTS, writeEndpointSet) + .column(COLUMN_READ_REPLICAS, readReplicaSet) + .column(COLUMN_WRITE_REPLICAS, writeReplicaSet); + + return result; + } + + private static TableMetadata buildTableMetadata(String keyspace) { + return TableMetadata.builder(keyspace, TABLE_NAME) + .comment(TABLE_COMMENT) + .kind(TableMetadata.Kind.VIRTUAL) + .partitioner(new LocalPartitioner(CompositeType.getInstance(UTF8Type.instance, UTF8Type.instance, UTF8Type.instance))) + .addPartitionKeyColumn(COLUMN_KEYSPACE_NAME, UTF8Type.instance) + .addPartitionKeyColumn(COLUMN_TABLE_NAME, UTF8Type.instance) + .addPartitionKeyColumn(COLUMN_KEY, UTF8Type.instance) + .addRegularColumn(COLUMN_TOKEN, UTF8Type.instance) + .addRegularColumn(COLUMN_TOKEN_BYTES, BytesType.instance) + .addRegularColumn(COLUMN_RANGE_START, UTF8Type.instance) + .addRegularColumn(COLUMN_RANGE_END, UTF8Type.instance) + .addRegularColumn(COLUMN_RANGE_START_BYTES, BytesType.instance) + .addRegularColumn(COLUMN_RANGE_END_BYTES, BytesType.instance) + .addRegularColumn(COLUMN_READ_ENDPOINTS, SetType.getInstance(UTF8Type.instance, false)) + .addRegularColumn(COLUMN_WRITE_ENDPOINTS, SetType.getInstance(UTF8Type.instance, false)) + .addRegularColumn(COLUMN_READ_REPLICAS, SetType.getInstance(Int32Type.instance, false)) + .addRegularColumn(COLUMN_WRITE_REPLICAS, SetType.getInstance(Int32Type.instance, false)) + .build(); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java index 9a80d1e9b930..d953310165d7 100644 --- a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java +++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java @@ -69,6 +69,8 @@ private SystemViewsKeyspace() .add(new LocalTable(VIRTUAL_VIEWS)) .add(new ClusterMetadataLogTable(VIRTUAL_VIEWS)) .add(new ClusterMetadataDirectoryTable(VIRTUAL_VIEWS)) + .add(new DataPlacementsTable(VIRTUAL_VIEWS)) + .add(new PartitionLocationTable(VIRTUAL_VIEWS)) .addAll(LocalRepairTables.getAll(VIRTUAL_VIEWS)) .addAll(CIDRFilteringMetricsTable.getAll(VIRTUAL_VIEWS)) .addAll(StorageAttachedIndexTables.getAll(VIRTUAL_VIEWS)) diff --git a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java index 2487e66fd280..19501fcc9b55 100644 --- a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java @@ -27,6 +27,7 @@ import java.util.Collection; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Stream; @@ -56,10 +57,12 @@ public abstract class AbstractSSTableSimpleWriter implements Closeable protected static final AtomicReference id = new AtomicReference<>(SSTableIdFactory.instance.defaultBuilder().generator(Stream.empty()).get()); protected boolean makeRangeAware = false; protected final Collection indexGroups; - protected Consumer> sstableProducedListener; + protected BiConsumer> sstableProducedListener; protected boolean openSSTableOnProduced = false; protected CompressionDictionary compressionDictionary; protected SSTable.Owner owner; + protected int sstableLevel = 0; + protected long reparedAtMillis = ActiveRepairService.UNREPAIRED_SSTABLE; protected AbstractSSTableSimpleWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns) { @@ -69,11 +72,21 @@ protected AbstractSSTableSimpleWriter(File directory, TableMetadataRef metadata, indexGroups = new ArrayList<>(); } + protected void setReparedAtMillis(long reparedAtMillis) + { + this.reparedAtMillis = reparedAtMillis; + } + protected void setSSTableFormatType(SSTableFormat type) { this.format = type; } + protected void setSSTableLevel(int level) + { + this.sstableLevel = level; + } + protected void setRangeAwareWriting(boolean makeRangeAware) { this.makeRangeAware = makeRangeAware; @@ -90,6 +103,12 @@ public void setCompressionDictionary(CompressionDictionary compressionDictionary } protected void setSSTableProducedListener(Consumer> listener) + { + Objects.requireNonNull(listener, "sstableProducedListener cannot be null"); + this.sstableProducedListener = (writer, readers) -> listener.accept(readers); + } + + protected void setSSTableProducedListener(BiConsumer> listener) { this.sstableProducedListener = Objects.requireNonNull(listener, "sstableProducedListener cannot be null"); } @@ -107,12 +126,12 @@ protected boolean shouldOpenSSTables() return openSSTableOnProduced; } - protected void notifySSTableProduced(Collection sstables) + protected void notifySSTableProduced(SSTableTxnWriter writer, Collection sstables) { if (sstableProducedListener == null) return; - sstableProducedListener.accept(sstables); + sstableProducedListener.accept(writer, sstables); } protected SSTableTxnWriter createWriter(SSTable.Owner owner) throws IOException @@ -120,7 +139,7 @@ protected SSTableTxnWriter createWriter(SSTable.Owner owner) throws IOException SerializationHeader header = new SerializationHeader(true, metadata.get(), columns, EncodingStats.NO_STATS); if (makeRangeAware) - return SSTableTxnWriter.createRangeAware(metadata, 0, ActiveRepairService.UNREPAIRED_SSTABLE, ActiveRepairService.NO_PENDING_REPAIR, false, format, header); + return SSTableTxnWriter.createRangeAware(metadata, 0, reparedAtMillis, ActiveRepairService.NO_PENDING_REPAIR, false, format, header); SSTable.Owner effectiveOwner; @@ -139,9 +158,10 @@ protected SSTableTxnWriter createWriter(SSTable.Owner owner) throws IOException return SSTableTxnWriter.create(metadata, createDescriptor(directory, metadata.keyspace, metadata.name, format), 0, - ActiveRepairService.UNREPAIRED_SSTABLE, + reparedAtMillis, ActiveRepairService.NO_PENDING_REPAIR, false, + sstableLevel, header, indexGroups, effectiveOwner); diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java index 8c9b2f979621..bf1f04c9b784 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java @@ -54,13 +54,14 @@ * * @see SSTableSimpleWriter */ -class SSTableSimpleUnsortedWriter extends AbstractSSTableSimpleWriter +public class SSTableSimpleUnsortedWriter extends AbstractSSTableSimpleWriter { private static final Buffer SENTINEL = new Buffer(); private Buffer buffer = new Buffer(); private final long maxSStableSizeInBytes; private long currentSize; + private boolean syncPending; // Used to compute the row serialized size private final SerializationHeader header; @@ -91,6 +92,18 @@ PartitionUpdate.Builder getUpdateFor(DecoratedKey key) PartitionUpdate.Builder previous = buffer.get(key); if (previous == null) { + if (syncPending) + { + syncPending = false; + try + { + sync(); + } + catch (IOException e) + { + throw new SyncException(e); + } + } // todo: inefficient - we create and serialize a PU just to get its size, then recreate it // todo: either allow PartitionUpdateBuilder to have .build() called several times or pre-calculate the size currentSize += PartitionUpdate.serializer.serializedSize(createPartitionUpdateBuilder(key).build(), format.getLatestVersion().correspondingMessagingVersion()); @@ -110,19 +123,10 @@ private void countRow(Row row) currentSize += UnfilteredSerializer.serializer.serializedSize(row, helper, 0, format.getLatestVersion().correspondingMessagingVersion()); } - private void maybeSync() throws SyncException + private void maybeSync() { - try - { - if (currentSize > maxSStableSizeInBytes) - sync(); - } - catch (IOException e) - { - // addColumn does not throw IOException but we want to report this to the user, - // so wrap it in a temporary RuntimeException that we'll catch in rawAddRow above. - throw new SyncException(e); - } + if (currentSize > maxSStableSizeInBytes) + syncPending = true; } private PartitionUpdate.Builder createPartitionUpdateBuilder(DecoratedKey key) @@ -132,8 +136,11 @@ private PartitionUpdate.Builder createPartitionUpdateBuilder(DecoratedKey key) @Override public void add(Row row) { + int before = rowCount(); super.add(row); - countRow(row); + + if (rowCount() > before) + countRow(row); maybeSync(); } }; @@ -229,7 +236,7 @@ public void run() for (Map.Entry entry : b.entrySet()) writer.append(entry.getValue().build().unfilteredIterator()); Collection finished = writer.finish(shouldOpenSSTables()); - notifySSTableProduced(finished); + notifySSTableProduced(writer, finished); } } catch (Throwable e) diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java index 7b5fb8a89e45..ed29e7db709d 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java @@ -44,7 +44,7 @@ * The output will be a series of SSTables that do not exceed a specified size. * By default, all sorted data are written into a single SSTable. */ -class SSTableSimpleWriter extends AbstractSSTableSimpleWriter +public class SSTableSimpleWriter extends AbstractSSTableSimpleWriter { private final long maxSSTableSizeInBytes; @@ -161,7 +161,7 @@ private void maybeCloseWriter(SSTableTxnWriter writer) return; Collection finished = writer.finish(shouldOpenSSTables()); - notifySSTableProduced(finished); + notifySSTableProduced(writer, finished); } catch (Throwable t) { diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java index 3b43dcfdf4fb..51f66b0ddc62 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableTxnWriter.java @@ -151,10 +151,25 @@ public static SSTableTxnWriter create(TableMetadataRef metadata, SerializationHeader header, Collection indexGroups, SSTable.Owner owner) + { + return create(metadata, descriptor, keyCount, repairedAt, pendingRepair, isTransient, 0, header, indexGroups, owner); + } + + @SuppressWarnings({"resource", "RedundantSuppression"}) // log and writer closed during doPostCleanup + public static SSTableTxnWriter create(TableMetadataRef metadata, + Descriptor descriptor, + long keyCount, + long repairedAt, + TimeUUID pendingRepair, + boolean isTransient, + int sstableLevel, + SerializationHeader header, + Collection indexGroups, + SSTable.Owner owner) { // if the column family store does not exist, we create a new default SSTableMultiWriter to use: LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.WRITE); - SSTableMultiWriter writer = SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, pendingRepair, isTransient, metadata, null, 0, header, indexGroups, txn, owner); + SSTableMultiWriter writer = SimpleSSTableMultiWriter.create(descriptor, keyCount, repairedAt, pendingRepair, isTransient, metadata, null, sstableLevel, header, indexGroups, txn, owner); return new SSTableTxnWriter(txn, writer); } } diff --git a/src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java b/src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java index fd1a2618db6b..f021b41bcd8c 100644 --- a/src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java +++ b/src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java @@ -24,6 +24,7 @@ import java.util.Objects; import java.util.Set; +import com.google.common.base.Preconditions; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; @@ -62,6 +63,9 @@ public DataPlacement(ReplicaGroups reads, this.writes = reads; // performance optimization for a typical case, to not search for endpoints twice else this.writes = writes; + + Preconditions.checkArgument(reads.ranges.equals(writes.ranges), + "Read ranges (%s) are not the same as write ranges (%s)", reads.ranges, writes.ranges); } /** diff --git a/src/java/org/apache/cassandra/utils/btree/BTree.java b/src/java/org/apache/cassandra/utils/btree/BTree.java index 2d06c4db1520..fc3418cc3b61 100644 --- a/src/java/org/apache/cassandra/utils/btree/BTree.java +++ b/src/java/org/apache/cassandra/utils/btree/BTree.java @@ -1611,6 +1611,11 @@ public Builder copy() return new Builder<>(this); } + public int count() + { + return count; + } + public Builder setQuickResolver(QuickResolver quickResolver) { this.quickResolver = quickResolver; diff --git a/test/distributed/org/apache/cassandra/distributed/test/DataPlacementsTableTest.java b/test/distributed/org/apache/cassandra/distributed/test/DataPlacementsTableTest.java new file mode 100644 index 000000000000..304ffe358870 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/DataPlacementsTableTest.java @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; +import org.apache.cassandra.utils.Shared; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DataPlacementsTableTest extends TestBaseImpl +{ + private static final int NUM_FAKE_NODES = 60; + private static final String DC1 = "datacenter0"; + private static final String DC2 = "datacenter1"; + private static final String SIMPLE_KS = "test_ks_simple"; + private static final String NTS_KS = "test_ks_nts"; + + private static final String SIMPLE_TABLE1 = "tbl1"; + private static final String SIMPLE_TABLE2 = "tbl2"; + private static final String NTS_TABLE1 = "nts_tbl1"; + private static final String NTS_TABLE2 = "nts_tbl2"; + + private static final long REAL_NODE_TOKEN = Long.MAX_VALUE - 1; + + private static Cluster CLUSTER; + private static Map> SIMPLE_EXPECTED; + private static Map> NTS_EXPECTED; + + @BeforeClass + public static void setup() throws IOException + { + CLUSTER = Cluster.build(1) + .withConfig(c -> c.set("num_tokens", 1) + .set("initial_token", Long.toString(REAL_NODE_TOKEN))) + .start(); + + // Register 60 fake nodes split evenly across the two DCs, evenly spaced around the ring. + CLUSTER.get(1).runOnInstance(() -> { + try + { + for (int i = 0; i < NUM_FAKE_NODES; i++) + { + String dc = (i % 2 == 0) ? DC1 : DC2; + String address = "127.0.1." + (i + 2); + + InetAddressAndPort addr = InetAddressAndPort.getByName(address); + NodeAddresses nodeAddresses = new NodeAddresses(addr); + Location location = new Location(dc, "rack1"); + + ClusterMetadata metadata = ClusterMetadataService.instance().commit( + new Register(nodeAddresses, location, NodeVersion.CURRENT)); + + NodeId nodeId = metadata.directory.peerId(addr); + + long token = Long.MIN_VALUE + (long) i * (Long.MAX_VALUE / (NUM_FAKE_NODES / 2)); + Set tokens = new HashSet<>(); + tokens.add(new Murmur3Partitioner.LongToken(token)); + + UnsafeJoin.unsafeJoin(nodeId, tokens); + } + } + catch (Exception e) + { + throw new RuntimeException("Failed to register fake nodes", e); + } + }); + + CLUSTER.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}", SIMPLE_KS)); + CLUSTER.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'NetworkTopologyStrategy', '%s': 2, '%s': 2}", NTS_KS, DC1, DC2)); + + CLUSTER.schemaChange(String.format("CREATE TABLE %s.%s (id int PRIMARY KEY, val text)", SIMPLE_KS, SIMPLE_TABLE1)); + CLUSTER.schemaChange(String.format("CREATE TABLE %s.%s (id int PRIMARY KEY, val text)", SIMPLE_KS, SIMPLE_TABLE2)); + CLUSTER.schemaChange(String.format("CREATE TABLE %s.%s (id int PRIMARY KEY, val text)", NTS_KS, NTS_TABLE1)); + CLUSTER.schemaChange(String.format("CREATE TABLE %s.%s (id int PRIMARY KEY, val text)", NTS_KS, NTS_TABLE2)); + + SIMPLE_EXPECTED = fetchExpectedPlacements(SIMPLE_KS); + NTS_EXPECTED = fetchExpectedPlacements(NTS_KS); + } + + + @AfterClass + public static void cleanup() + { + if (CLUSTER != null) + CLUSTER.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testPlacements() + { + Map>> allExpected = new HashMap<>(); + allExpected.put(SIMPLE_KS, SIMPLE_EXPECTED); + allExpected.put(NTS_KS, NTS_EXPECTED); + + Object[][] result = CLUSTER.coordinator(1) + .execute("SELECT keyspace_name, table_name, range_start, range_end, token_type, " + + "range_start_bytes, range_end_bytes, read_endpoints, write_endpoints, " + + "read_replicas, write_replicas " + + "FROM system_views.data_placements", + ConsistencyLevel.ONE); + + assertThat(result).isNotEmpty(); + + Map> tablesFound = new HashMap<>(); + Map> rangesFound = new HashMap<>(); + for (String ks : allExpected.keySet()) + { + tablesFound.put(ks, new HashSet<>()); + rangesFound.put(ks, new HashSet<>()); + } + + for (Object[] row : result) + { + String keyspace = (String) row[0]; + Map> expected = allExpected.get(keyspace); + if (expected == null) + continue; // skip system keyspaces + + String tableName = (String) row[1]; + String rangeStart = (String) row[2]; + String rangeEnd = (String) row[3]; + String tokenType = (String) row[4]; + ByteBuffer rangeStartBytes = (ByteBuffer) row[5]; + ByteBuffer rangeEndBytes = (ByteBuffer) row[6]; + Set readEndpoints = (Set) row[7]; + Set writeEndpoints = (Set) row[8]; + Set readReplicas = (Set) row[9]; + Set writeReplicas = (Set) row[10]; + + assertThat(tokenType).as("token_type for %s.%s", keyspace, tableName) + .contains("Murmur3Partitioner"); + assertThat(rangeStartBytes).as("range_start_bytes for %s.%s", keyspace, tableName).isNotNull(); + assertThat(rangeEndBytes).as("range_end_bytes for %s.%s", keyspace, tableName).isNotNull(); + + Range range = new Range(Long.parseLong(rangeStart), Long.parseLong(rangeEnd)); + Set exp = expected.get(range); + assertThat(exp).as("range (%s, %s] not in expected for %s", rangeStart, rangeEnd, keyspace) + .isNotNull(); + + Set expectedIps = exp.stream().map(e -> e.ip).collect(Collectors.toSet()); + Set expectedNodeIds = exp.stream().map(e -> e.nodeId).collect(Collectors.toSet()); + + assertThat(readEndpoints).as("read endpoints for %s.%s (%s,%s]", keyspace, tableName, rangeStart, rangeEnd) + .isEqualTo(expectedIps); + assertThat(writeEndpoints).as("write endpoints for %s.%s (%s,%s]", keyspace, tableName, rangeStart, rangeEnd) + .isEqualTo(expectedIps); + assertThat(readReplicas).as("read replica node-ids for %s.%s (%s,%s]", keyspace, tableName, rangeStart, rangeEnd) + .isEqualTo(expectedNodeIds); + assertThat(writeReplicas).as("write replica node-ids for %s.%s (%s,%s]", keyspace, tableName, rangeStart, rangeEnd) + .isEqualTo(expectedNodeIds); + + tablesFound.get(keyspace).add(tableName); + rangesFound.get(keyspace).add(range); + } + + assertThat(tablesFound.get(SIMPLE_KS)).as("tables in " + SIMPLE_KS).contains(SIMPLE_TABLE1, SIMPLE_TABLE2); + assertThat(rangesFound.get(SIMPLE_KS)).as("unique ranges in " + SIMPLE_KS).hasSize(SIMPLE_EXPECTED.size()); + assertThat(tablesFound.get(NTS_KS)).as("tables in " + NTS_KS).contains(NTS_TABLE1, NTS_TABLE2); + assertThat(rangesFound.get(NTS_KS)).as("unique ranges in " + NTS_KS).hasSize(NTS_EXPECTED.size()); + } + + @Test + @SuppressWarnings("unchecked") + public void testQueryByKeyspaceAndTable() + { + Object[][] result = CLUSTER.coordinator(1) + .execute("SELECT keyspace_name, table_name, range_start, range_end, " + + "read_endpoints, write_endpoints " + + "FROM system_views.data_placements WHERE keyspace_name = ? AND table_name = ?", + ConsistencyLevel.ONE, NTS_KS, NTS_TABLE1); + + assertThat(result).isNotEmpty(); + + Set rangesFound = new HashSet<>(); + for (Object[] row : result) + { + assertThat(row[0]).isEqualTo(NTS_KS); + assertThat(row[1]).isEqualTo(NTS_TABLE1); + + String rangeStart = (String) row[2]; + String rangeEnd = (String) row[3]; + Set readEndpoints = (Set) row[4]; + Set writeEndpoints = (Set) row[5]; + + Range range = new Range(Long.parseLong(rangeStart), Long.parseLong(rangeEnd)); + Set expected = NTS_EXPECTED.get(range); + assertThat(expected).as("range (%s, %s] not found in simulation", rangeStart, rangeEnd) + .isNotNull(); + + Set expectedIps = expected.stream().map(e -> e.ip).collect(Collectors.toSet()); + assertThat(readEndpoints).as("read endpoints for (%s,%s]", rangeStart, rangeEnd) + .isEqualTo(expectedIps); + assertThat(writeEndpoints).as("write endpoints for (%s,%s]", rangeStart, rangeEnd) + .isEqualTo(expectedIps); + + rangesFound.add(range); + } + + // The 2-component query must return ALL ranges for the table, not a subset. + assertThat(rangesFound).as("ranges for " + NTS_KS + "." + NTS_TABLE1) + .hasSize(NTS_EXPECTED.size()); + } + + @Test + @SuppressWarnings("unchecked") + public void testQueryByFullPartitionKey() + { + String rangeStart, rangeEnd; + { + Range range = NTS_EXPECTED.keySet().iterator().next(); + rangeStart = String.valueOf(range.start); + rangeEnd = String.valueOf(range.end); + } + + Object[][] result = CLUSTER.coordinator(1) + .execute("SELECT keyspace_name, table_name, range_start, range_end, " + + "read_endpoints, write_endpoints, read_replicas, write_replicas " + + "FROM system_views.data_placements " + + "WHERE keyspace_name = ? AND table_name = ? AND range_start = ? AND range_end = ?", + ConsistencyLevel.ONE, NTS_KS, NTS_TABLE1, rangeStart, rangeEnd); + + assertThat(result).as("full-PK query must return exactly one row").hasNumberOfRows(1); + + Object[] row = result[0]; + assertThat(row[0]).as("keyspace_name").isEqualTo(NTS_KS); + assertThat(row[1]).as("table_name").isEqualTo(NTS_TABLE1); + assertThat(row[2]).as("range_start").isEqualTo(rangeStart); + assertThat(row[3]).as("range_end").isEqualTo(rangeEnd); + + Set readEndpoints = (Set) row[4]; + Set writeEndpoints = (Set) row[5]; + Set readReplicas = (Set) row[6]; + Set writeReplicas = (Set) row[7]; + + Range range = new Range(Long.parseLong(rangeStart), Long.parseLong(rangeEnd)); + Set expected = NTS_EXPECTED.get(range); + assertThat(expected).as("range (%s, %s] not found in simulation", rangeStart, rangeEnd) + .isNotNull(); + + Set expectedIps = expected.stream().map(e -> e.ip).collect(Collectors.toSet()); + Set expectedNodeIds = expected.stream().map(e -> e.nodeId).collect(Collectors.toSet()); + + assertThat(readEndpoints).as("read endpoints").isEqualTo(expectedIps); + assertThat(writeEndpoints).as("write endpoints").isEqualTo(expectedIps); + assertThat(readReplicas).as("read replica node-ids").isEqualTo(expectedNodeIds); + assertThat(writeReplicas).as("write replica node-ids").isEqualTo(expectedNodeIds); + } + + @Shared + public static final class Range + { + public final long start; + public final long end; + + public Range(long start, long end) + { + this.start = start; + this.end = end; + } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof Range)) return false; + Range r = (Range) o; + return r.start == start && r.end == end; + } + + @Override + public int hashCode() + { + return Long.hashCode(start) * 31 + Long.hashCode(end); + } + + @Override + public String toString() + { + return "(" + start + ", " + end + "]"; + } + } + + @Shared + public static final class Endpoint + { + public final String ip; + public final int nodeId; + + public Endpoint(String ip, int nodeId) + { + this.ip = ip; + this.nodeId = nodeId; + } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof Endpoint)) return false; + Endpoint e = (Endpoint) o; + return e.nodeId == nodeId && e.ip.equals(ip); + } + + @Override + public int hashCode() + { + return ip.hashCode() * 31 + nodeId; + } + } + + private static Map> fetchExpectedPlacements(String keyspaceName) + { + return CLUSTER.get(1).callOnInstance(() -> { + Map> result = new HashMap<>(); + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ksm = metadata.schema.getKeyspaceMetadata(keyspaceName); + DataPlacement placement = metadata.placements.get(ksm.params.replication); + for (org.apache.cassandra.dht.Range range : placement.reads.ranges) + { + VersionedEndpoints.ForRange re = placement.reads.forRange(range); + Set endpoints = new HashSet<>(); + for (Replica r : re.get()) + { + NodeId nid = metadata.directory.peerId(r.endpoint()); + endpoints.add(new Endpoint(r.endpoint().toString(), nid.id())); + } + result.put(new Range(Long.parseLong(range.left.toString()), + Long.parseLong(range.right.toString())), + endpoints); + } + return result; + }); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/PartitionLocationTableTest.java b/test/distributed/org/apache/cassandra/distributed/test/PartitionLocationTableTest.java new file mode 100644 index 000000000000..86c39dc6ca90 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/PartitionLocationTableTest.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.io.IOException; +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.utils.Shared; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PartitionLocationTableTest extends TestBaseImpl +{ + private static final int NUM_NODES = 3; + private static final String KEYSPACE = "test_ks"; + + private static Cluster CLUSTER; + + @BeforeClass + public static void setup() throws IOException + { + CLUSTER = Cluster.build(NUM_NODES).start(); + } + + @AfterClass + public static void cleanup() + { + if (CLUSTER != null) + CLUSTER.close(); + } + + + @Test + @SuppressWarnings("unchecked") + public void testSimplePartitionKey() + { + CLUSTER.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}"); + CLUSTER.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".tbl1 (col1 int PRIMARY KEY, col2 text)"); + + ExpectedLocation expected = computeExpectedLocation(KEYSPACE, "tbl1", "123"); + + Object[][] result = CLUSTER.coordinator(1).execute( + "SELECT tkn, range_start, range_end, range_start_bytes, range_end_bytes, " + + "read_endpoints, write_endpoints, read_replicas, write_replicas " + + "FROM system_views.partition_location " + + "WHERE keyspace_name = ? AND table_name = ? AND key = ?", + ConsistencyLevel.ONE, + KEYSPACE, "tbl1", "123"); + + Assert.assertEquals(1, result.length); + Object[] row = result[0]; + String token = (String) row[0]; + String rangeStart = (String) row[1]; + String rangeEnd = (String) row[2]; + ByteBuffer rangeStartBytes = (ByteBuffer) row[3]; + ByteBuffer rangeEndBytes = (ByteBuffer) row[4]; + Set readEndpoints = (Set) row[5]; + Set writeEndpoints = (Set) row[6]; + Set readReplicas = (Set) row[7]; + Set writeReplicas = (Set) row[8]; + + assertThat(token).as("token").isEqualTo(expected.token); + assertThat(rangeStart).as("range_start").isEqualTo(expected.rangeStart); + assertThat(rangeEnd).as("range_end").isEqualTo(expected.rangeEnd); + assertThat(rangeStartBytes).as("range_start_bytes").isNotNull(); + assertThat(rangeEndBytes).as("range_end_bytes").isNotNull(); + + assertThat(readEndpoints).as("read endpoints").isEqualTo(expected.readEndpoints); + assertThat(writeEndpoints).as("write endpoints").isEqualTo(expected.writeEndpoints); + assertThat(readReplicas).as("read replica node-ids").isEqualTo(expected.readReplicas); + assertThat(writeReplicas).as("write replica node-ids").isEqualTo(expected.writeReplicas); + } + + @Test + @SuppressWarnings("unchecked") + public void testCompositePartitionKey() + { + CLUSTER.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}"); + CLUSTER.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".tbl2 (col1 int, col2 text, col3 int, PRIMARY KEY ((col1, col2), col3))"); + + ExpectedLocation expected = computeExpectedLocation(KEYSPACE, "tbl2", "123:value1"); + + Object[][] result = CLUSTER.coordinator(1).execute( + "SELECT tkn, range_start, range_end, range_start_bytes, range_end_bytes, " + + "read_endpoints, write_endpoints, read_replicas, write_replicas " + + "FROM system_views.partition_location " + + "WHERE keyspace_name = ? AND table_name = ? AND key = ?", + ConsistencyLevel.ONE, + KEYSPACE, "tbl2", "123:value1"); + + Assert.assertEquals(1, result.length); + Object[] row = result[0]; + String token = (String) row[0]; + String rangeStart = (String) row[1]; + String rangeEnd = (String) row[2]; + ByteBuffer rangeStartBytes = (ByteBuffer) row[3]; + ByteBuffer rangeEndBytes = (ByteBuffer) row[4]; + Set readEndpoints = (Set) row[5]; + Set writeEndpoints = (Set) row[6]; + Set readReplicas = (Set) row[7]; + Set writeReplicas = (Set) row[8]; + + assertThat(token).as("token").isEqualTo(expected.token); + assertThat(rangeStart).as("range_start").isEqualTo(expected.rangeStart); + assertThat(rangeEnd).as("range_end").isEqualTo(expected.rangeEnd); + assertThat(rangeStartBytes).as("range_start_bytes").isNotNull(); + assertThat(rangeEndBytes).as("range_end_bytes").isNotNull(); + + assertThat(readEndpoints).as("read endpoints").isEqualTo(expected.readEndpoints); + assertThat(writeEndpoints).as("write endpoints").isEqualTo(expected.writeEndpoints); + assertThat(readReplicas).as("read replica node-ids").isEqualTo(expected.readReplicas); + assertThat(writeReplicas).as("write replica node-ids").isEqualTo(expected.writeReplicas); + } + + @Shared + public static final class ExpectedLocation // class needs to be public since it's shared + { + public final String token; + public final String rangeStart; + public final String rangeEnd; + public final Set readEndpoints; + public final Set writeEndpoints; + public final Set readReplicas; + public final Set writeReplicas; + + public ExpectedLocation(String token, String rangeStart, String rangeEnd, + Set readEndpoints, Set writeEndpoints, + Set readReplicas, Set writeReplicas) + { + this.token = token; + this.rangeStart = rangeStart; + this.rangeEnd = rangeEnd; + this.readEndpoints = readEndpoints; + this.writeEndpoints = writeEndpoints; + this.readReplicas = readReplicas; + this.writeReplicas = writeReplicas; + } + } + + private static ExpectedLocation computeExpectedLocation(String ks, String tbl, String key) + { + return CLUSTER.get(1).callOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ksm = metadata.schema.getKeyspaceMetadata(ks); + TableMetadata table = ksm.getTableOrViewNullable(tbl); + + ByteBuffer partitionKeyBytes = table.partitionKeyType.fromString(key); + DecoratedKey dk = table.partitioner.decorateKey(partitionKeyBytes); + Token token = dk.getToken(); + + DataPlacement placement = metadata.placements.get(ksm.params.replication); + VersionedEndpoints.ForRange readEps = placement.reads.forRange(token); + VersionedEndpoints.ForRange writeEps = placement.writes.forRange(token); + Range range = readEps.get().range(); + + Set readEndpoints = readEps.get().stream() + .map(r -> r.endpoint().toString()) + .collect(Collectors.toSet()); + Set writeEndpoints = writeEps.get().stream() + .map(r -> r.endpoint().toString()) + .collect(Collectors.toSet()); + Set readReplicas = readEps.get().stream() + .map(r -> metadata.directory.peerId(r.endpoint()).id()) + .collect(Collectors.toSet()); + Set writeReplicas = writeEps.get().stream() + .map(r -> metadata.directory.peerId(r.endpoint()).id()) + .collect(Collectors.toSet()); + + return new ExpectedLocation(token.toString(), + range.left.toString(), + range.right.toString(), + readEndpoints, + writeEndpoints, + readReplicas, + writeReplicas); + }); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordHostReplacementTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordHostReplacementTest.java index ceebb3933dca..e8587ff56606 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordHostReplacementTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordHostReplacementTest.java @@ -32,6 +32,7 @@ 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.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.RingAwareInJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.Generator; @@ -43,6 +44,7 @@ import static org.apache.cassandra.distributed.shared.ClusterUtils.stopUnchecked; import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.*; public class AccordHostReplacementTest extends TestBaseImpl { @@ -72,9 +74,10 @@ public void hostReplace() throws IOException SchemaSpec.optionsBuilder().withTransactionalMode(transactionalModeGen.generate(rng)) .withSpeculativeRetry("ALWAYS")); SchemaSpec schema = schemaGen.generate(rng); - Generators.TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), 1000))); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(Generators.int64(0, Math.min(valueGenerators.pkGen().population(), 1000)))); - HistoryBuilder history = historyBuilder(schema, cluster); + HistoryBuilder history = historyBuilder(schema, valueGenerators, cluster); waitForCMSToQuiesce(cluster, cluster.get(1)); for (int i = 0; i < 1000; i++) @@ -93,13 +96,13 @@ public void hostReplace() throws IOException } } - private static HistoryBuilder historyBuilder(SchemaSpec schema, Cluster cluster) + private static HistoryBuilder historyBuilder(SchemaSpec schema, IndexedValueGenerators valueGenerators, Cluster cluster) { - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, hb -> RingAwareInJvmDTestVisitExecutor.builder() .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(3)) .consistencyLevel(ConsistencyLevel.ALL) - .build(schema, hb, cluster)); + .build(schema, hb.valueGenerators(), cluster)); history.customThrowing(() -> cluster.schemaChange(schema.compile()), "Setup"); return history; } diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/AlterTopologyTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/AlterTopologyTest.java index 6d05fc981748..b2114a4cd15e 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/AlterTopologyTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/AlterTopologyTest.java @@ -34,6 +34,7 @@ import org.apache.cassandra.exceptions.ExceptionCode; import org.apache.cassandra.harry.SchemaSpec; import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.Generator; @@ -54,6 +55,7 @@ import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; import static org.junit.Assert.assertEquals; public class AlterTopologyTest extends FuzzTestBase @@ -75,13 +77,13 @@ public void testTopologyChanges() throws Exception 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)); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(Generators.int64(0, Math.min(valueGenerators.pkGen().population(), 1000)))); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, (hb) -> InJvmDTestVisitExecutor.builder() .nodeSelector(i -> 1) - .build(schema, hb, cluster)); + .build(schema, valueGenerators, cluster)); history.custom(() -> { cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1' : 3 };"); @@ -92,7 +94,10 @@ public void testTopologyChanges() throws Exception Runnable writeAndValidate = () -> { for (int i = 0; i < 2000; i++) - history.insert(pkGen.generate(rng), ckGen.generate(rng)); + { + int pdIdx = pkGen.generate(rng); + history.insert(pdIdx, valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng)); + } for (int pk : pkGen.generated()) history.selectPartition(pk); 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 ed6faf26aa95..100dd856274f 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java @@ -40,7 +40,7 @@ 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.HistoryBuilderHelper; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; import org.apache.cassandra.harry.execution.CQLVisitExecutor; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.Generator; @@ -53,6 +53,7 @@ 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.HistoryBuilder.valueGenerators; import static org.junit.Assert.fail; import static org.psjava.util.AssertStatus.assertTrue; @@ -75,8 +76,9 @@ public void bounceTest() throws Exception 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)); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); + + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(valueGenerators.pkIdxGen())); cluster.schemaChange("CREATE KEYSPACE " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};"); @@ -84,12 +86,12 @@ public void bounceTest() throws Exception Future f = es.submit(new Runnable() { - final HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + final HistoryBuilder history = new HistoryBuilder(valueGenerators); final Iterator iterator = history.iterator(); final CQLVisitExecutor executor = InJvmDTestVisitExecutor.builder() .nodeSelector(lts -> 1) .retryPolicy(InJvmDTestVisitExecutor.RetryPolicy.RETRY_ON_TIMEOUT) - .build(schema, history, cluster); + .build(schema, history.valueGenerators(), cluster); @Override public void run() { @@ -97,7 +99,7 @@ public void run() { // Rate limit to ~10 per second LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); - HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history); + history.insert(); history.selectPartition(pkGen.generate(rng)); while (iterator.hasNext() && !stop.get()) 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 9246a7df000c..727848fca708 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java @@ -18,7 +18,6 @@ package org.apache.cassandra.distributed.test.log; - import java.nio.ByteBuffer; import java.util.List; import java.util.Random; @@ -38,11 +37,13 @@ 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.dsl.IndexedValueGenerators; 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.Kind; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.util.ByteUtils; import org.apache.cassandra.harry.util.TokenUtil; @@ -77,12 +78,12 @@ public void writeConsistencyTest() throws Throwable withRandom(rng -> { SchemaSpec schema = schemaGen.generate(rng); + IndexedValueGenerators valueGenerators = HistoryBuilder.valueGenerators(schema, rng.next(), 1000); cluster.schemaChange("CREATE KEYSPACE " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};"); cluster.schemaChange(schema.compile()); - HistoryBuilder.IndexedValueGenerators valueGenerators = (HistoryBuilder.IndexedValueGenerators) schema.valueGenerators; - for (int i = 0; i < valueGenerators.pkPopulation(); i++) + for (int i = 0; i < valueGenerators.pkGen().population(); i++) { long pd = valueGenerators.pkGen().descriptorAt(i); @@ -102,12 +103,12 @@ public void writeConsistencyTest() throws Throwable 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), + ValueGeneratorHelper.randomDescriptors(rng, valueGenerators.forPd(pd)::regularColumnGen, valueGenerators.forPd(pd).regularColumnCount()), + ValueGeneratorHelper.randomDescriptors(rng, valueGenerators.forPd(pd)::staticColumnGen, valueGenerators.forPd(pd).staticColumnCount()), + Kind.INSERT), schema, + valueGenerators, lts); cluster.coordinator(1).execute(s.cql(), ConsistencyLevel.QUORUM, s.bindings()); return null; 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 d64a1bdff968..d9a780254bf2 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java @@ -41,6 +41,7 @@ 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.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.Generator; @@ -60,6 +61,7 @@ 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; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; public class FailedLeaveTest extends FuzzTestBase { @@ -98,13 +100,13 @@ private void failedLeaveTest(BiFunction { 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)); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(Generators.int64(0, Math.min(valueGenerators.pkGen().population(), 1000)))); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, (hb) -> InJvmDTestVisitExecutor.builder() .nodeSelector(i -> 1) - .build(schema, hb, cluster)); + .build(schema, hb.valueGenerators(), cluster)); history.custom(() -> { cluster.schemaChange("CREATE KEYSPACE " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};"); @@ -113,7 +115,10 @@ private void failedLeaveTest(BiFunction { for (int i = 0; i < WRITES; i++) - history.insert(pkGen.generate(rng), ckGen.generate(rng)); + { + int pkIdx = pkGen.generate(rng); + history.insert(pkIdx, valueGenerators.forPdIdx(pkIdx).ckIdxGen().generate(rng)); + } for (int pk : pkGen.generated()) history.selectPartition(pk); 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 07ec054d54ae..b97c1fae6b43 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java @@ -38,6 +38,7 @@ 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.dsl.IndexedValueGenerators; import org.apache.cassandra.harry.execution.DataTracker; import org.apache.cassandra.harry.execution.RingAwareInJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.Generator; @@ -79,10 +80,10 @@ public void bootstrapWithDeferredJoinTest() throws Throwable 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)); + IndexedValueGenerators valueGenerators = HistoryBuilder.valueGenerators(schema, rng.next(), 1000); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(Generators.int64(0, Math.min(valueGenerators.pkGen().population(), 1000)))); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = new HistoryBuilder(valueGenerators); history.customThrowing(() -> { cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", schema.keyspace)); cluster.schemaChange(schema.compile()); @@ -91,7 +92,10 @@ public void bootstrapWithDeferredJoinTest() throws Throwable Runnable writeAndValidate = () -> { for (int i = 0; i < WRITES; i++) - history.insert(pkGen.generate(rng), ckGen.generate(rng)); + { + int pdIdx = pkGen.generate(rng); + history.insert(pdIdx, history.valueGenerators().forPdIdx(pdIdx).ckIdxGen().generate(rng)); + } for (int pk : pkGen.generated()) history.selectPartition(pk); @@ -100,15 +104,15 @@ public void bootstrapWithDeferredJoinTest() throws Throwable // 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); + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + QuiescentChecker checker = new QuiescentChecker(valueGenerators, tracker); 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); + .build(schema, valueGenerators, tracker, checker, cluster); Iterator iterator = history.iterator(); while (iterator.hasNext()) executor.execute(iterator.next()); @@ -130,7 +134,7 @@ public void bootstrapWithDeferredJoinTest() throws Throwable executor = RingAwareInJvmDTestVisitExecutor.builder() .replicationFactor(rf) .consistencyLevel(ConsistencyLevel.ONE) - .build(schema, tracker, checker, cluster); + .build(schema, valueGenerators, tracker, checker, cluster); while (iterator.hasNext()) executor.execute(iterator.next()); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java index 64810654d5cf..486adcf8000a 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java @@ -89,9 +89,7 @@ public void simpleUpgradeTest() .upgradesToCurrentFrom(v41) .withUpgradeListener(listener) .setup((cluster) -> { - SchemaSpec schema = new SchemaSpec(rng.next(), - 10_000, - "harry", "test_table", + SchemaSpec schema = new SchemaSpec("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)), @@ -99,7 +97,7 @@ public void simpleUpgradeTest() cluster.schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3)); cluster.schemaChange(schema.compile()); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(HistoryBuilder.valueGenerators(schema, rng.next()), hb -> InJvmDTestVisitExecutor.builder() .retryPolicy(retry -> true) .nodeSelector(lts -> { @@ -112,9 +110,9 @@ public void simpleUpgradeTest() } }) .consistencyLevel(ConsistencyLevel.QUORUM) - .build(schema, hb, cluster)); + .build(schema, hb.valueGenerators(), cluster)); - Generator pkIdxGen = Generators.int32(0, Math.min(10_000, schema.valueGenerators.ckPopulation())); + Generator pkIdxGen = Generators.adaptLongToInt(Generators.int64(0, Math.min(10_000, history.valueGenerators().pkGen().population()))); executor.set(executorFactory().infiniteLoop("R/W Worload", () -> { 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 8607c2551ddb..ad432fd2bd55 100644 --- a/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java @@ -33,10 +33,14 @@ import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; + public class RangeTombstoneBurnTest extends IntegrationTestBase { private final int ITERATIONS = 10; private final int STEPS_PER_ITERATION = 1000; + private final int POPULATION = 1000; @Test public void rangeTombstoneBurnTest() @@ -47,8 +51,9 @@ public void rangeTombstoneBurnTest() 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()); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), POPULATION); + int perIteration = 10; + int maxPartitions = Math.toIntExact(Math.max(perIteration, valueGenerators.pkGen().population())); for (int iteration = 0; iteration < ITERATIONS; iteration++) { @@ -61,13 +66,13 @@ public void rangeTombstoneBurnTest() 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()); + int maxPartitionSize = Math.min(rng.nextInt(1, 1 << rng.nextInt(5, 11)), POPULATION); 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)); + ReplayingHistoryBuilder historyBuilder = new ReplayingHistoryBuilder(valueGenerators, + (hb) -> InJvmDTestVisitExecutor.builder().build(schema, hb.valueGenerators(), cluster)); model.init(historyBuilder) .step((history, rng_) -> { 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 65b1bf444140..e37f13a49750 100644 --- a/test/distributed/org/apache/cassandra/fuzz/harry/examples/RepairBurnTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RepairBurnTest.java @@ -26,7 +26,7 @@ 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.dsl.IndexedValueGenerators; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.Generator; import org.apache.cassandra.harry.gen.Generators; @@ -47,19 +47,19 @@ public static void before() throws Throwable public void repairBurnTest() { int maxPartitionSize = 10; - int partitions = 1000; Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "repair_burn", 1000); withRandom(rng -> { SchemaSpec schema = schemaGen.generate(rng); + IndexedValueGenerators valueGenerators = HistoryBuilder.valueGenerators(schema, rng.next(), 1000); - 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)); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(valueGenerators.pkIdxGen())); + Generator ckGen = Generators.int32(0, maxPartitionSize); ModelChecker modelChecker = new ModelChecker<>(); - modelChecker.init(new HistoryBuilder(schema.valueGenerators)) - .step((history, rng_) -> HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history)) + modelChecker.init(new HistoryBuilder(valueGenerators)) + .step((history, rng_) -> history.insert(pkGen.generate(rng))) .step((history, rng_) -> history.deleteRow(pkGen.generate(rng), ckGen.generate(rng))) .exitCondition((history) -> { if (history.size() < 10_000) @@ -73,7 +73,7 @@ public void repairBurnTest() cluster.schemaChange(schema.compile()); - InJvmDTestVisitExecutor.replay(InJvmDTestVisitExecutor.builder().build(schema, history, cluster), + InJvmDTestVisitExecutor.replay(InJvmDTestVisitExecutor.builder().build(schema, history.valueGenerators(), cluster), history); return true; diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/sstable/LevelledSSTableGeneratorTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/sstable/LevelledSSTableGeneratorTest.java new file mode 100644 index 000000000000..ef8a6f93c592 --- /dev/null +++ b/test/distributed/org/apache/cassandra/fuzz/harry/sstable/LevelledSSTableGeneratorTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.sstable; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.op.ClusteringOrderBy; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.stress.ActivePartition; +import org.apache.cassandra.harry.stress.LevelledSStableGenerator; +import org.apache.cassandra.harry.stress.RotationStrategy; +import org.apache.cassandra.harry.stress.config.StressSchemaConfig; +import org.apache.cassandra.harry.stress.TokenIndex; +import org.apache.cassandra.harry.stress.TokenIndexGenerator; +import org.apache.cassandra.harry.stress.VisitGenerator; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.harry.stress.distribution.Distributions; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.tcm.ClusterMetadataService; + +public class LevelledSSTableGeneratorTest extends TestBaseImpl +{ + private static final Logger logger = LoggerFactory.getLogger(LevelledSSTableGeneratorTest.class); + + private static final String SCHEMA_CONFIG = "test/resources/harry/stress/levelled-sstable-schema.yaml"; + + private static final long INITIAL_LTS = 1; + private static final long VISITS = 100_000; // total writes; with visitSize=1, one op per LTS + private static final long END_LTS = INITIAL_LTS + VISITS; // first LTS past the written history; reads use it + private static final int SSTABLE_SIZE_MIB = 1; + private static final int[] LEVEL_WEIGHTS = { 1, 2, 4, 8, 16 }; // index == LCS level; weights spread writes across levels + + // Initialize static state for offline tool usage, since we are generating SSTables on the main class loader rather + // than in-jvm dtest nodes + public static void initForOfflineTool() + { + DatabaseDescriptor.toolInitialization(false); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ClusterMetadataService.initializeForClients(); + } + + @Test + public void generateAndValidateLevelledSSTables() throws Throwable + { + initForOfflineTool(); + + StressSchemaConfig config = StressSchemaConfig.load(Paths.get(SCHEMA_CONFIG)); + SchemaSpec schema = config.schema(); + + Distribution visitSize = Distributions.fixed(1); + VisitGenerator.OpKindGenFactory opKindGen = new VisitGenerator.RandomOpKindGenFactory(); + + try (Cluster cluster = init(Cluster.build(1).start(), 1)) + { + cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + cluster.schemaChange(schema.compile()); + cluster.get(1).nodetool("disableautocompaction", schema.keyspace, schema.table); + + + // Build TokenIndex + File tokenDir = new File(Files.createTempDirectory("harry-token-index")); + TokenIndexGenerator.generate(tokenDir.toJavaIOFile(), schema, config.rotationStrategy(), + config.rowPopulation(), Generators.constant(VisitGenerator.VisitType.MUTATE), + visitSize, INITIAL_LTS, VISITS); + TokenIndex tokenIndex = new TokenIndex(new File(tokenDir, "merged_tokens"), + new File(tokenDir, "merged_tokens.idx")); + + // Generate levelled SSTables + File sstableDir = new File(Files.createDirectories(Files.createTempDirectory("harry-levelled-sstables") + .resolve(schema.keyspace) + .resolve(schema.table + '-' + "0".repeat(32)))); + LevelledSStableGenerator generator = + new LevelledSStableGenerator(schema, config.rowPopulation(), config.columnPopulation(), visitSize, opKindGen, + false, SSTABLE_SIZE_MIB, + new LevelledSStableGenerator.SSTableLevelPicker(LEVEL_WEIGHTS), + tokenIndex, sstableDir); + generator.generate(Long.MIN_VALUE, Long.MAX_VALUE); + + // Import SSTables; -l to keeps generated levels + cluster.get(1).nodetoolResult("import", "-l", schema.keyspace, schema.table, sstableDir.absolutePath()) + .asserts().success(); + + TestOracle model = replay(schema, config, tokenIndex, visitSize, opKindGen); + tokenIndex.close(); + + // Validate + InJvmDTestVisitExecutor validator = new InJvmDTestVisitExecutor(schema, model.partitions, new DataTracker.NoOpDataTracker(), + new QuiescentChecker(model.partitions, model.tracker), cluster, + lts -> 1, + v -> InJvmDTestVisitExecutor.PageSizeSelector.NO_PAGING, + InJvmDTestVisitExecutor.RetryPolicy.NO_RETRY, + v -> ConsistencyLevel.NODE_LOCAL, + QueryBuildingVisitExecutor.WrapQueries.EMPTY); + for (long pd : model.generatedPds) + validator.execute(new Visit(END_LTS, new Operations.Operation[]{ new Operations.SelectPartition(END_LTS, pd, ClusteringOrderBy.ASC) })); + logger.info("Validated {} partitions", model.generatedPds.size()); + } + } + + private static TestOracle replay(SchemaSpec schema, StressSchemaConfig config, TokenIndex tokenIndex, + Distribution visitSize, VisitGenerator.OpKindGenFactory opKindGen) + { + RotationStrategy rotation = config.rotationStrategy(); + ActivePartition.Partitions partitions = new ActivePartition.Partitions(schema, config.rowPopulation(), config.columnPopulation(), + rotation, 0, rotation.targetSize(), INITIAL_LTS); + partitions.populate(); + DataTracker.SimpleDataTracker modelTracker = new DataTracker.SimpleDataTracker(); + Set generatedPds = new TreeSet<>(); + TokenIndex.EntryIterator iter = tokenIndex.range(Long.MIN_VALUE, Long.MAX_VALUE); + while (iter.hasNext()) + { + long pd = iter.pd(); + generatedPds.add(pd); + for (long lts : iter.readLts()) + { + Visit visit = VisitGenerator.mutatingVisit(lts, visitSize, opKindGen, rng -> partitions.forPd(pd)); + modelTracker.begin(visit); + modelTracker.end(visit); + } + iter.advance(); + } + return new TestOracle(partitions, modelTracker, generatedPds); + } + + private static final class TestOracle + { + final ActivePartition.Partitions partitions; + final DataTracker.SimpleDataTracker tracker; + final Set generatedPds; + + TestOracle(ActivePartition.Partitions partitions, DataTracker.SimpleDataTracker tracker, Set generatedPds) + { + this.partitions = partitions; + this.tracker = tracker; + this.generatedPds = generatedPds; + } + } +} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/sstable/RangeAwareSSTableLoadAndStressTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/sstable/RangeAwareSSTableLoadAndStressTest.java new file mode 100644 index 000000000000..e612a4920d8d --- /dev/null +++ b/test/distributed/org/apache/cassandra/fuzz/harry/sstable/RangeAwareSSTableLoadAndStressTest.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.sstable; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.test.TestBaseImpl; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.stress.HarryStress; +import org.apache.cassandra.harry.stress.LevelledSStableGenerator; +import org.apache.cassandra.harry.stress.RotationStrategy; +import org.apache.cassandra.harry.stress.TokenIndex; +import org.apache.cassandra.harry.stress.TokenIndexGenerator; +import org.apache.cassandra.harry.stress.VisitGenerator; +import org.apache.cassandra.harry.stress.config.StressSchemaConfig; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.harry.stress.distribution.Distributions; +import org.apache.cassandra.io.util.File; + +public class RangeAwareSSTableLoadAndStressTest extends TestBaseImpl +{ + private static final Logger logger = LoggerFactory.getLogger(RangeAwareSSTableLoadAndStressTest.class); + + private static final String SCHEMA_CONFIG = "test/resources/harry/stress/levelled-sstable-schema.yaml"; + + private static final int NUM_NODES = 4; + private static final int RF = 3; + + private static final long INITIAL_LTS = 1; + private static final long VISITS = 100_000; + private static final long END_LTS = INITIAL_LTS + VISITS; + private static final long STRESS_VISITS = 16_000; + private static final int CONCURRENCY = 2; + private static final int RATE_PER_SECOND = 20_000; + private static final int SSTABLE_SIZE_MIB = 1; + private static final int[] LEVEL_WEIGHTS = { 1, 2, 4, 8, 16 }; + + @Test + public void rangeAwareLoadAndStress() throws Throwable + { + LevelledSSTableGeneratorTest.initForOfflineTool(); + + StressSchemaConfig config = StressSchemaConfig.load(Paths.get(SCHEMA_CONFIG)); + SchemaSpec schema = config.schema(); + RotationStrategy rotation = config.rotationStrategy(); + + Distribution visitSize = Distributions.fixed(1); + VisitGenerator.OpKindGenFactory opKindGen = new VisitGenerator.RandomOpKindGenFactory(); + Map weights = new HashMap<>(); + weights.put(VisitGenerator.VisitType.MUTATE, 10); + weights.put(VisitGenerator.VisitType.VALIDATE, 10); + Generator visitTypeGen = Generators.weighted(weights); + + + try (Cluster cluster = init(Cluster.build(NUM_NODES) + .withConfig(c -> c.set("num_tokens", 1)) + .start(), RF)) + { + cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + + " WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter0': " + RF + "}"); + cluster.schemaChange(schema.compile()); + cluster.forEach(n -> n.nodetool("disableautocompaction", schema.keyspace, schema.table)); + + List ranges = getTopology(cluster, schema); + for (RangeReplicas r : ranges) + System.out.println(String.format(" range (%d, %d] -> nodes %s", r.startToken, r.endToken, java.util.Arrays.toString(r.replicas))); + logger.info("data_placements returned {} ranges", ranges.size()); + + File tokenDir = new File(Files.createTempDirectory("harry-token-index")); + TokenIndexGenerator.generate(tokenDir.toJavaIOFile(), schema, rotation, + config.rowPopulation(), visitTypeGen, + visitSize, INITIAL_LTS, VISITS); + TokenIndex tokenIndex = new TokenIndex(new File(tokenDir, "merged_tokens"), + new File(tokenDir, "merged_tokens.idx")); + + // Ror each range, generate levelled SSTables and import into replicas + for (RangeReplicas range : ranges) + { + File sstableDir = new File(Files.createDirectories(Files.createTempDirectory("range-aware-sstables") + .resolve(String.format("%d-%d", range.startToken, range.endToken)) + .resolve(schema.keyspace) + .resolve(schema.table + '-' + "0".repeat(32)))); + LevelledSStableGenerator generator = new LevelledSStableGenerator(schema, config.rowPopulation(), config.columnPopulation(), visitSize, opKindGen, + false, SSTABLE_SIZE_MIB, + new LevelledSStableGenerator.SSTableLevelPicker(LEVEL_WEIGHTS), + tokenIndex, sstableDir); + // Handle wraparound ranges + if (range.startToken <= range.endToken) + { + generator.generate(range.startToken, range.endToken); + } + else + { + generator.generate(Long.MIN_VALUE, range.endToken); + generator.generate(range.startToken, Long.MAX_VALUE); + } + + for (int node : range.replicas) + cluster.get(node).nodetoolResult("import", "-cd", "-t", schema.keyspace, schema.table, sstableDir.absolutePath()) + .asserts().success(); + } + logger.info("Generated and imported SSTables for all ranges"); + + // Continue HarryStress live from where SSTableGenerator has finished + HarryStress stress = new HarryStress(schema, + config.rowPopulation(), + config.columnPopulation(), + visitTypeGen, + visitSize, + opKindGen, + config.rotationStrategy(), + /*metricsOut=*/ null, + /*reportIntervalSeconds=*/ 30, + () -> (statement, run) -> { + Object[][] rs = cluster.coordinator(1).execute(statement.cql(), + ConsistencyLevel.QUORUM, + statement.bindings()); + if (run != null) + run.run(); + return rs; + }, + CONCURRENCY, + RATE_PER_SECOND, + /*minPartitionIdx=*/ 0, + /*maxPartitionIdx=*/ Long.MAX_VALUE, + /*initialLts=*/ INITIAL_LTS); + + stress.replay(INITIAL_LTS, END_LTS); + tokenIndex.close(); + + stress.start(END_LTS + STRESS_VISITS, Long.MAX_VALUE); + logger.info("HarryStress completed an additional {} visits past the loaded SSTable history", STRESS_VISITS); + } + } + + @SuppressWarnings("unchecked") + private static List getTopology(Cluster cluster, SchemaSpec schema) + { + Object[][] rows = cluster.coordinator(1).execute( + "SELECT range_start, range_end, write_replicas FROM system_views.data_placements " + + "WHERE keyspace_name = ? AND table_name = ?", + ConsistencyLevel.ONE, schema.keyspace, schema.table); + List result = new ArrayList<>(); + + for (Object[] row : rows) + { + long startToken = Long.parseLong((String) row[0]); + long endToken = Long.parseLong((String) row[1]); + int[] replicas = toIdx((Set) row[2]); + result.add(new RangeReplicas(startToken, endToken, replicas)); + } + return result; + } + + private static int[] toIdx(Set nodeIds) + { + // In dtest clusters, TCM node IDs are assigned 1..N matching the instance indices + int[] idxs = new int[nodeIds.size()]; + int i = 0; + for (int nodeId : nodeIds) + idxs[i++] = nodeId; + return idxs; + } + + // A token range and its write-replica set + private static final class RangeReplicas + { + final long startToken; + final long endToken; + final int[] replicas; + + RangeReplicas(long startToken, long endToken, int[] replicas) + { + this.startToken = startToken; + this.endToken = endToken; + this.replicas = replicas; + } + } +} diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/stress/HarryStressValidationSmokeTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/stress/HarryStressValidationSmokeTest.java new file mode 100644 index 000000000000..f44b142a540f --- /dev/null +++ b/test/distributed/org/apache/cassandra/fuzz/harry/stress/HarryStressValidationSmokeTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.fuzz.harry.stress; + +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.test.TestBaseImpl; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.stress.HarryStress; +import org.apache.cassandra.harry.stress.RotationStrategy; +import org.apache.cassandra.harry.stress.VisitGenerator; +import org.apache.cassandra.harry.stress.distribution.Distributions; + +import static java.util.Arrays.asList; +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; + +public class HarryStressValidationSmokeTest extends TestBaseImpl +{ + private static final Logger logger = LoggerFactory.getLogger(HarryStressValidationSmokeTest.class); + + private static final String TABLE = "stress_smoke"; + + private static final long ITERATIONS = 100_000; + private static final int NUM_PARTITIONS = 100; + private static final int REPLACE_WITH_NEW = 10; + private static final int ROW_POPULATION = 100; + private static final int COLUMN_POPULATION = 100; + private static final int CONCURRENCY = 2; + private static final int RATE_PER_SECOND = 20_000; + + @Test + public void validatingStressSmokeTest() throws Throwable + { + try (Cluster cluster = init(Cluster.build(1).start(), 1)) + { + SchemaSpec schema = schema(); + cluster.schemaChange(schema.compile()); + + HarryStress stress = + new HarryStress(schema, + Distributions.fixed(ROW_POPULATION), + column -> Distributions.fixed(COLUMN_POPULATION), + Generators.pick(VisitGenerator.VisitType.values()), + Distributions.fixed(1), + new VisitGenerator.RandomOpKindGenFactory(), + new RotationStrategy.FixedRotationStrategy(NUM_PARTITIONS, REPLACE_WITH_NEW, 0), + null, + 30, + () -> (statement, onComplete) -> { + Object[][] rows = cluster.coordinator(1).execute(statement.cql(), + ConsistencyLevel.QUORUM, + statement.bindings()); + if (onComplete != null) + onComplete.run(); + return rows; + }, + CONCURRENCY, + RATE_PER_SECOND, + 0, + Long.MAX_VALUE, + 0); + + stress.start(ITERATIONS, Long.MAX_VALUE); + logger.info("Validating stress smoke test ran ~{} visits across {} partitions with no validation failures", + ITERATIONS, NUM_PARTITIONS); + } + } + + private static SchemaSpec schema() + { + return new SchemaSpec(KEYSPACE, TABLE, + asList(pk("pk1", asciiType), pk("pk2", int64Type)), + asList(ck("ck1", asciiType, false), ck("ck2", int64Type, false)), + asList(regularColumn("v1", asciiType), regularColumn("v2", int64Type), regularColumn("v3", asciiType)), + asList(), + SchemaSpec.optionsBuilder().ifNotExists(true).addWriteTimestamps(true)); + } +} diff --git a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java index 08af4f4d4b9e..465a848c7fd6 100644 --- a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java @@ -39,7 +39,7 @@ import org.apache.cassandra.distributed.test.log.FuzzTestBase; 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.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.DataTracker; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; @@ -59,6 +59,7 @@ 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.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; public class ConsistentBootstrapTest extends FuzzTestBase { @@ -83,13 +84,16 @@ public void bootstrapFuzzTest() throws Throwable 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)); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(valueGenerators.pkIdxGen())); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = new HistoryBuilder(valueGenerators); Runnable writeAndValidate = () -> { for (int i = 0; i < WRITES; i++) - HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history); + { + int pkIdx = pkGen.generate(rng); + history.insert(pkIdx, valueGenerators.forPdIdx(pkIdx).ckIdxGen().generate(rng)); + } for (int pk : pkGen.generated()) history.selectPartition(pk); @@ -134,7 +138,7 @@ public void bootstrapFuzzTest() throws Throwable RingAwareInJvmDTestVisitExecutor.replay(RingAwareInJvmDTestVisitExecutor.builder() .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(2)) .consistencyLevel(ConsistencyLevel.ALL) - .build(schema, history, cluster), + .build(schema, history.valueGenerators(), cluster), history); }); } @@ -178,13 +182,13 @@ public void coordinatorIsBehindTest() throws Throwable withRandom(rng -> { SchemaSpec schema = schemaGen.generate(rng); - Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), 1000)); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, hb -> RingAwareInJvmDTestVisitExecutor.builder() .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(3)) .consistencyLevel(ConsistencyLevel.ALL) - .build(schema, hb, cluster)); + .build(schema, hb.valueGenerators(), cluster)); cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", KEYSPACE)); cluster.schemaChange(schema.compile()); @@ -198,15 +202,16 @@ public void coordinatorIsBehindTest() throws Throwable .drop() .on(); - DataTracker tracker = new DataTracker.SequentialDataTracker(); + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); RingAwareInJvmDTestVisitExecutor executor = RingAwareInJvmDTestVisitExecutor.builder() .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(2)) .nodeSelector(i -> 2) .consistencyLevel(ConsistencyLevel.ALL) .retryPolicy(InJvmDTestVisitExecutor.RetryPolicy.NO_RETRY) .build(schema, + history.valueGenerators(), tracker, - new QuiescentChecker(schema.valueGenerators, tracker, history), + new QuiescentChecker(valueGenerators, tracker), cluster); // Prime the CMS node to pause before the finish join event is committed @@ -231,7 +236,7 @@ public void coordinatorIsBehindTest() throws Throwable boolean triggered = false; outer: - for (int i = 0; i < history.valueGenerators().pkPopulation(); i++) + for (int i = 0; i < history.valueGenerators().pkGen().population(); i++) { long pd = history.valueGenerators().pkGen().descriptorAt(i); for (TokenPlacementModel.Replica replica : executor.getReplicasFor(pd)) @@ -240,7 +245,7 @@ public void coordinatorIsBehindTest() throws Throwable { try { - HistoryBuilderHelper.insertRandomData(schema, i, ckGen.generate(rng), rng, history); + history.insert(i, valueGenerators.forPdIdx(i).ckIdxGen().generate(rng)); } catch (Throwable t) { diff --git a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java index bb9fd4867c18..d6803e959909 100644 --- a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java @@ -57,6 +57,8 @@ 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 org.apache.cassandra.harry.dsl.IndexedValueGenerators; import static org.junit.Assert.assertFalse; public class ConsistentLeaveTest extends FuzzTestBase @@ -82,19 +84,22 @@ public void decommissionTest() throws Throwable 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)); + IndexedValueGenerators valueGenerators = HistoryBuilder.valueGenerators(schema, rng.next(), 1000); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(valueGenerators.pkIdxGen())); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(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)); + .build(schema, hb.valueGenerators(), cluster)); Runnable writeAndValidate = () -> { for (int i = 0; i < WRITES; i++) - history.insert(pkGen.generate(rng), ckGen.generate(rng)); + { + int pkIdx = pkGen.generate(rng); + history.insert(pkIdx, valueGenerators.forPdIdx(pkIdx).ckIdxGen().generate(rng)); + } for (int pk : pkGen.generated()) history.selectPartition(pk); diff --git a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java index 7f4d3ad5df4f..3e7e597258f1 100644 --- a/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java @@ -40,6 +40,7 @@ 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.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.RingAwareInJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.Generator; @@ -83,17 +84,20 @@ public void moveTest() throws Throwable 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)); + IndexedValueGenerators valueGenerators = HistoryBuilder.valueGenerators(schema, rng.next(), 1000); + Generators.TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(valueGenerators.pkIdxGen())); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, (hb) -> RingAwareInJvmDTestVisitExecutor.builder() .replicationFactor(new TokenPlacementModel.SimpleReplicationFactor(2)) .consistencyLevel(ConsistencyLevel.ALL) - .build(schema, hb, cluster)); + .build(schema, hb.valueGenerators(), cluster)); Runnable writeAndValidate = () -> { for (int i = 0; i < WRITES; i++) - history.insert(pkGen.generate(rng), ckGen.generate(rng)); + { + int pkIdx = pkGen.generate(rng); + history.insert(pkIdx, valueGenerators.forPdIdx(pkIdx).ckIdxGen().generate(rng)); + } for (int pk : pkGen.generated()) history.selectPartition(pk); diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITestBase.java b/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITestBase.java index bbe74c34a127..a441651f2bb6 100644 --- a/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITestBase.java +++ b/test/distributed/org/apache/cassandra/fuzz/sai/SingleNodeSAITestBase.java @@ -18,7 +18,6 @@ package org.apache.cassandra.fuzz.sai; - import java.util.HashSet; import java.util.List; import java.util.Set; @@ -43,6 +42,7 @@ 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.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.gen.EntropySource; @@ -55,6 +55,7 @@ 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.HistoryBuilder.valueGenerators; import static org.apache.cassandra.harry.dsl.SingleOperationBuilder.IdxRelation; // TODO: "WITH OPTIONS = {'case_sensitive': 'false', 'normalize': 'true', 'ascii': 'true'};", @@ -135,7 +136,7 @@ protected int rf() public void simplifiedSaiTest() { withRandom(rng -> saiTest(rng, - SchemaGenerators.trivialSchema(KEYSPACE, "simplified", 1000).generate(rng), + SchemaGenerators.trivialSchema(KEYSPACE, "simplified").generate(rng), () -> true, DEFAULT_REPAIR_SKIP)); } @@ -169,9 +170,8 @@ public void mixedFilteringSaiTest() private void saiTest(EntropySource rng, SchemaSpec schema, Supplier createIndex, int repairSkip) { logger.info(schema.compile()); - - Generator globalPkGen = Generators.int32(0, Math.min(NUM_PARTITIONS, schema.valueGenerators.pkPopulation())); - Generator ckGen = Generators.int32(0, schema.valueGenerators.ckPopulation()); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), MAX_PARTITION_SIZE); + Generator globalPkGen = Generators.adaptLongToInt(Generators.int64(0, Math.min(NUM_PARTITIONS, valueGenerators.pkGen().population()))); beforeEach(); cluster.forEach(i -> i.nodetool("disableautocompaction")); @@ -201,11 +201,11 @@ private void saiTest(EntropySource rng, SchemaSpec schema, Supplier cre CassandraRelevantProperties.SAI_INTERSECTION_CLAUSE_LIMIT.setInt(indexCount.get()); waitForIndexesQueryable(schema); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, (hb) -> InJvmDTestVisitExecutor.builder() .pageSizeSelector(pageSizeSelector(rng)) .consistencyLevel(consistencyLevelSelector()) - .doubleWriting(schema, hb, cluster, "debug_table")); + .doubleWriting(schema, hb.valueGenerators(), cluster, "debug_table")); Set partitions = new HashSet<>(); int attempts = 0; while (partitions.size() < NUM_VISITED_PARTITIONS && attempts < NUM_VISITED_PARTITIONS * 10) @@ -237,14 +237,15 @@ private void saiTest(EntropySource rng, SchemaSpec schema, Supplier cre 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); + int pdIdx = pkGen.generate(rng); + IndexedValueGenerators.IndexedPartitionValues partitionValues = valueGenerators.forPdIdx(pdIdx); + HistoryBuilderHelper.insertRandomData(schema, pdIdx, partitionValues.ckIdxGen().generate(rng), rng, 0.5d, history); if (rng.nextFloat() > 0.99f) { - int row1 = ckGen.generate(rng); - int row2 = ckGen.generate(rng); - history.deleteRowRange(partitionIndex, + int row1 = partitionValues.ckIdxGen().generate(rng); + int row2 = partitionValues.ckIdxGen().generate(rng); + history.deleteRowRange(pdIdx, Math.min(row1, row2), Math.max(row1, row2), rng.nextInt(schema.clusteringKeys.size()), @@ -253,10 +254,10 @@ private void saiTest(EntropySource rng, SchemaSpec schema, Supplier cre } if (rng.nextFloat() > 0.995f) - HistoryBuilderHelper.deleteRandomColumns(schema, partitionIndex, ckGen.generate(rng), rng, history); + HistoryBuilderHelper.deleteRandomColumns(schema, pdIdx, partitionValues.ckIdxGen().generate(rng), rng, history); if (rng.nextFloat() > 0.9995f) - history.deletePartition(partitionIndex); + history.deletePartition(pdIdx); if (i % FLUSH_SKIP == 0) history.custom(() -> flush(schema), "Flush"); @@ -272,13 +273,13 @@ else if (i % repairSkip == 0) List regularRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.regularColumns.size(), - column -> Math.min(schema.valueGenerators.regularPopulation(column), UNIQUE_CELL_VALUES), + column -> Math.min(partitionValues.regularPopulation(column), UNIQUE_CELL_VALUES), eqOnlyRegularColumns::contains); List staticRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.staticColumns.size(), - column -> Math.min(schema.valueGenerators.staticPopulation(column), UNIQUE_CELL_VALUES), + column -> Math.min(partitionValues.staticPopulation(column), UNIQUE_CELL_VALUES), eqOnlyStaticColumns::contains); Integer pk = pkGen.generate(rng); @@ -286,7 +287,7 @@ else if (i % repairSkip == 0) IdxRelation[] ckRelations = HistoryBuilderHelper.generateClusteringRelations(rng, schema.clusteringKeys.size(), - ckGen, + partitionValues.ckIdxGen(), eqOnlyClusteringColumns).toArray(new IdxRelation[0]); IdxRelation[] regularRelationsArray = regularRelations.toArray(new IdxRelation[regularRelations.size()]); @@ -353,7 +354,7 @@ public static Consumer defaultConfig() protected InJvmDTestVisitExecutor.ConsistencyLevelSelector consistencyLevelSelector() { return visit -> { - if (visit.selectOnly) + if (visit.validating) return ConsistencyLevel.ALL; // The goal here is to make replicas as out of date as possible, modulo the efforts of repair diff --git a/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java b/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java index 07844b045e31..d445fd5da40f 100644 --- a/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/sai/StaticsTortureTest.java @@ -36,6 +36,8 @@ import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; 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; @@ -45,7 +47,7 @@ public class StaticsTortureTest extends IntegrationTestBase private static final int MAX_PARTITION_SIZE = 10_000; private static final int NUM_PARTITIONS = 100; private static final int UNIQUE_CELL_VALUES = 5; - + private static final int POPULATION = 1000; @Test public void staticsTortureTest() { @@ -64,9 +66,7 @@ public void staticsTortureTest() public void staticsTortureTest(List> cks, int idx) { - SchemaSpec schema = new SchemaSpec(idx, - 10_000, - KEYSPACE, + SchemaSpec schema = new SchemaSpec(KEYSPACE, "tbl" + idx, Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.int64Type, Generators.int64()), ColumnSpec.pk("pk2", ColumnSpec.asciiType, Generators.ascii(4, 1000)), @@ -124,8 +124,9 @@ public void staticsTortureTest(List> cks, int idx) Generator staticColumnBitSet = Generators.bitSet(schema.staticColumns.size()); EntropySource rng = new JdkRandomEntropySource(1l); - ReplayingHistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, hb -> { - return InJvmDTestVisitExecutor.builder().pageSizeSelector(i -> rng.nextInt(1, 10)).build(schema, hb, cluster); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), POPULATION); + ReplayingHistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, hb -> { + return InJvmDTestVisitExecutor.builder().pageSizeSelector(i -> rng.nextInt(1, 10)).build(schema, hb.valueGenerators(), cluster); }); @@ -175,10 +176,8 @@ public void staticsTortureTest(List> cks, int idx) 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)); + List regularRelations = generateValueRelations(rng, schema.regularColumns.size(),column -> POPULATION); + List staticRelations = generateValueRelations(rng, schema.staticColumns.size(),column -> POPULATION); history.select(pdx, ckRelations.toArray(new IdxRelation[0]), regularRelations.toArray(new IdxRelation[0]), diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/AccordBootstrapTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/AccordBootstrapTest.java index 4ddcd03d45a7..4891cf31e354 100644 --- a/test/distributed/org/apache/cassandra/fuzz/topology/AccordBootstrapTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/topology/AccordBootstrapTest.java @@ -36,6 +36,7 @@ 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.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; @@ -48,6 +49,7 @@ 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.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; public class AccordBootstrapTest extends FuzzTestBase { @@ -77,16 +79,17 @@ public void bootstrapFuzzTest() throws Throwable HashSet downInstances = new HashSet<>(); withRandom(rng -> { - Generator schemaGen = SchemaGenerators.trivialSchema(KEYSPACE, () -> "bootstrap_fuzz", POPULATION, + Generator schemaGen = SchemaGenerators.trivialSchema(KEYSPACE, () -> "bootstrap_fuzz", SchemaSpec.optionsBuilder() .addWriteTimestamps(false) .withTransactionalMode(TransactionalMode.full) ); SchemaSpec schema = schemaGen.generate(rng); - TrackingGenerator pkGen = Generators.tracking(Generators.int32(0, Math.min(schema.valueGenerators.pkPopulation(), POPULATION))); - Generator ckGen = Generators.int32(0, Math.min(schema.valueGenerators.ckPopulation(), POPULATION)); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); + TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(Generators.int64(0, Math.min(valueGenerators.pkGen().population(), POPULATION)))); + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, hb -> InJvmDTestVisitExecutor.builder() .consistencyLevel(ConsistencyLevel.QUORUM) .wrapQueries(QueryBuildingVisitExecutor.WrapQueries.TRANSACTION) @@ -100,11 +103,15 @@ public void bootstrapFuzzTest() throws Throwable } }) - .build(schema, hb, cluster)); + .build(schema, valueGenerators, cluster)); Runnable writeAndValidate = () -> { for (int i = 0; i < WRITES; i++) - HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history); + { + int pdIdx = pkGen.generate(rng); + IndexedValueGenerators.IndexedPartitionValues partitionValues = valueGenerators.forPdIdx(pdIdx); + HistoryBuilderHelper.insertRandomData(schema, pdIdx, partitionValues.ckIdxGen.generate(rng), rng, 0.5d, history); + } for (int pk : pkGen.generated()) history.selectPartition(pk); diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/AccordBounceTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/AccordBounceTest.java index 69d12bf9ee31..5b5ab244ef71 100644 --- a/test/distributed/org/apache/cassandra/fuzz/topology/AccordBounceTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/topology/AccordBounceTest.java @@ -36,6 +36,7 @@ import org.apache.cassandra.distributed.test.log.FuzzTestBase; import org.apache.cassandra.harry.SchemaSpec; import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; @@ -45,6 +46,7 @@ import org.apache.cassandra.service.consensus.TransactionalMode; import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; public class AccordBounceTest extends FuzzTestBase { @@ -67,7 +69,7 @@ public String get() { return "bootstrap_fuzz" + (i++); } - }, POPULATION, + }, SchemaSpec.optionsBuilder() .addWriteTimestamps(false) .withTransactionalMode(TransactionalMode.full) @@ -78,12 +80,13 @@ public String get() { SchemaSpec schema = schemaGen.generate(rng); cluster.schemaChange(schema.compile()); - historyBuilders.add(new ReplayingHistoryBuilder(schema.valueGenerators, + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), POPULATION); + historyBuilders.add(new ReplayingHistoryBuilder(valueGenerators, hb -> InJvmDTestVisitExecutor.builder() .consistencyLevel(ConsistencyLevel.QUORUM) .wrapQueries(QueryBuildingVisitExecutor.WrapQueries.TRANSACTION) .pageSizeSelector(p -> InJvmDTestVisitExecutor.PageSizeSelector.NO_PAGING) - .build(schema, hb, cluster))); + .build(schema, valueGenerators, cluster))); } for (HistoryBuilder hb : historyBuilders) @@ -125,7 +128,7 @@ public String get() { return "bootstrap_fuzz" + (i++); } - }, POPULATION, + }, SchemaSpec.optionsBuilder() .addWriteTimestamps(false) .withTransactionalMode(TransactionalMode.full) @@ -136,12 +139,13 @@ public String get() { SchemaSpec schema = schemaGen.generate(rng); cluster.schemaChange(schema.compile()); - historyBuilders.add(new ReplayingHistoryBuilder(schema.valueGenerators, + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), POPULATION); + historyBuilders.add(new ReplayingHistoryBuilder(valueGenerators, hb -> InJvmDTestVisitExecutor.builder() .consistencyLevel(ConsistencyLevel.QUORUM) .wrapQueries(QueryBuildingVisitExecutor.WrapQueries.TRANSACTION) .pageSizeSelector(p -> InJvmDTestVisitExecutor.PageSizeSelector.NO_PAGING) - .build(schema, hb, cluster))); + .build(schema, valueGenerators, cluster))); } Runnable writeAndValidate = () -> { diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/AccordHardCatchupTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/AccordHardCatchupTest.java new file mode 100644 index 000000000000..623c0ed65369 --- /dev/null +++ b/test/distributed/org/apache/cassandra/fuzz/topology/AccordHardCatchupTest.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.fuzz.topology; + +import java.util.HashSet; + +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; +import org.junit.Test; + +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.Feature; +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.distributed.test.log.FuzzTestBase; +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.execution.QueryBuildingVisitExecutor; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.gen.Generators.TrackingGenerator; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.service.consensus.TransactionalMode; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; + +public class AccordHardCatchupTest extends FuzzTestBase +{ + private static final int WRITES = 10; + private static final int POPULATION = 1000; + + @Test + public void hardCatchupFuzzTest() throws Throwable + { + CassandraRelevantProperties.SYSTEM_TRACES_DEFAULT_RF.setInt(3); + Cluster.Builder builder = builder(); + try (Cluster cluster = builder.withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(100)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(100, "dc0", "rack0")) + .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP)) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + waitForCMSToQuiesce(cluster, cmsInstance); + + HashSet downInstances = new HashSet<>(); + withRandom(rng -> { + Generator schemaGen = SchemaGenerators.trivialSchema(KEYSPACE, () -> "bootstrap_fuzz", + SchemaSpec.optionsBuilder() + .addWriteTimestamps(false) + .withTransactionalMode(TransactionalMode.full) + ); + + SchemaSpec schema = schemaGen.generate(rng); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), POPULATION); + TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(Generators.int64(0, Math.min(valueGenerators.pkGen().population(), POPULATION)))); + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, + hb -> InJvmDTestVisitExecutor.builder() + .consistencyLevel(ConsistencyLevel.QUORUM) + .wrapQueries(QueryBuildingVisitExecutor.WrapQueries.TRANSACTION) + .pageSizeSelector(p -> InJvmDTestVisitExecutor.PageSizeSelector.NO_PAGING) + .nodeSelector(lts -> { + while (true) + { + int pick = rng.nextInt(1, cluster.size() + 1); + if (!downInstances.contains(pick)) + return pick; + + } + }) + .build(schema, valueGenerators, cluster)); + + Runnable writeAndValidate = () -> { + for (int i = 0; i < WRITES; i++) + { + int pdIdx = pkGen.generate(rng); + IndexedValueGenerators.IndexedPartitionValues partitionValues = valueGenerators.forPdIdx(pdIdx); + HistoryBuilderHelper.insertRandomData(schema, pdIdx, partitionValues.ckIdxGen.generate(rng), rng, 0.5d, history); + } + + for (int pk : pkGen.generated()) + history.selectPartition(pk); + }; + + 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"); + Thread.sleep(1000); + writeAndValidate.run(); + + history.customThrowing(() -> { + downInstances.add(2); + ClusterUtils.stopUnchecked(cluster.get(2)); + cluster.get(1).logs().watchFor("/127.0.0.2:.* is now DOWN"); + }, "Shut down node 2"); + + writeAndValidate.run(); + + history.customThrowing(() -> { + cluster.get(2).config().set("accord.catchup_on_start", "HARD"); + cluster.get(2).startup(); + cluster.get(2).logs().watchFor(".*Catchup.*"); + cluster.get(1).logs().watchFor("/127.0.0.2:.* is now UP"); + downInstances.remove(2); + }, "Start down node 2"); + + writeAndValidate.run(); + + history.customThrowing(() -> { + downInstances.add(1); + ClusterUtils.stopUnchecked(cluster.get(1)); + cluster.get(2).logs().watchFor("/127.0.0.1:.* is now DOWN"); + }, "Shut down node 1"); + + writeAndValidate.run(); + }); + } + } +} diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/AccordRebootstrapTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/AccordRebootstrapTest.java new file mode 100644 index 000000000000..cf40abbba51d --- /dev/null +++ b/test/distributed/org/apache/cassandra/fuzz/topology/AccordRebootstrapTest.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.topology; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntPredicate; + +import org.junit.Test; + +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.Feature; +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.distributed.test.log.FuzzTestBase; +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.IndexedValueGenerators; +import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; +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.Generators; +import org.apache.cassandra.harry.gen.Generators.TrackingGenerator; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.util.ThrowingRunnable; +import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.service.consensus.TransactionalMode; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; + +public class AccordRebootstrapTest extends FuzzTestBase +{ + private static final int WRITES = 10; + private static final int POPULATION = 1000; + + @Test + public void rebootstrapFuzzTest() throws Throwable + { + CassandraRelevantProperties.SYSTEM_TRACES_DEFAULT_RF.setInt(3); + Cluster.Builder builder = builder(); + try (Cluster cluster = builder.withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(100)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(100, "dc0", "rack0")) + .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP)) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + waitForCMSToQuiesce(cluster, cmsInstance); + + HashSet downInstances = new HashSet<>(); + AtomicInteger nextId = new AtomicInteger(); + withRandom(rng -> { + Generator schemaGen = SchemaGenerators.trivialSchema(KEYSPACE, () -> "bootstrap_fuzz" + (nextId.incrementAndGet()), + SchemaSpec.optionsBuilder() + .addWriteTimestamps(false) + .withTransactionalMode(TransactionalMode.full) + ); + + History history1 = createNewSchemaWithWriteAndValidate(schemaGen, rng, cluster, downInstances::contains); + history1.writeAndValidate(); + + history1.run(() -> { + downInstances.add(2); + ClusterUtils.stopUnchecked(cluster.get(2)); + cluster.get(1).logs().watchFor("/127.0.0.2:.* is now DOWN"); + }, "Shut down node 2"); + + history1.writeAndValidate(); + History history2 = createNewSchemaWithWriteAndValidate(schemaGen, rng, cluster, downInstances::contains); + history2.writeAndValidate(); + History history3 = createNewSchemaWithWriteAndValidate(schemaGen, rng, cluster, downInstances::contains); + history3.writeAndValidate(); + + history1.run(() -> { + cluster.get(2).config().set("accord.journal.stop_marker_failure_policy", "REBOOTSTRAP"); + Path journalDir = Path.of(cluster.get(2).config().get("accord.journal_directory").toString()); + Path stopMarker = journalDir.resolve("stopped"); + PathUtils.delete(stopMarker); + cluster.get(2).startup(); + cluster.get(2).logs().watchFor(".*Rebootstrapping.*"); + cluster.get(1).logs().watchFor("/127.0.0.2:.* is now UP"); + downInstances.remove(2); + }, "Start down node 2"); + + history1.writeAndValidate(); + history2.writeAndValidate(); + history3.writeAndValidate(); + }); + } + } + + interface History + { + void writeAndValidate(); + void run(ThrowingRunnable run, String tag); + } + + private History createNewSchemaWithWriteAndValidate(Generator schemaGen, EntropySource rng, Cluster cluster, IntPredicate downInstances) throws InterruptedException + { + IInvokableInstance cmsInstance = cluster.get(1); + SchemaSpec schema = schemaGen.generate(rng); + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), POPULATION); + TrackingGenerator pkGen = Generators.tracking(Generators.adaptLongToInt(Generators.int64(0, Math.min(valueGenerators.pkGen().population(), POPULATION)))); + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, + hb -> InJvmDTestVisitExecutor.builder() + .consistencyLevel(ConsistencyLevel.QUORUM) + .wrapQueries(QueryBuildingVisitExecutor.WrapQueries.TRANSACTION) + .pageSizeSelector(p -> InJvmDTestVisitExecutor.PageSizeSelector.NO_PAGING) + .nodeSelector(lts -> { + while (true) + { + int pick = rng.nextInt(1, cluster.size() + 1); + if (!downInstances.test(pick)) + return pick; + } + }) + .build(schema, valueGenerators, cluster)); + + history.customThrowing(() -> { + cluster.schemaChangeIgnoringStoppedInstances(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", KEYSPACE)); + cluster.schemaChangeIgnoringStoppedInstances(schema.compile()); + waitForCMSToQuiesce(cluster, cmsInstance); + }, "Setup"); + Thread.sleep(1000); + + return new History() + { + @Override + public void writeAndValidate() + { + for (int i = 0; i < WRITES; i++) + { + int pdIdx = pkGen.generate(rng); + IndexedValueGenerators.IndexedPartitionValues partitionValues = valueGenerators.forPdIdx(pdIdx); + HistoryBuilderHelper.insertRandomData(schema, pdIdx, partitionValues.ckIdxGen.generate(rng), rng, 0.5d, history); + } + + for (int pk : pkGen.generated()) + history.selectPartition(pk); + } + + @Override + public void run(ThrowingRunnable run, String tag) + { + history.customThrowing(run, tag); + } + }; + } +} diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java index d54ffc336769..8f00fdf4715d 100644 --- a/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/topology/HarryTopologyMixupTest.java @@ -55,7 +55,7 @@ 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.harry.op.Operations; +import org.apache.cassandra.harry.op.ClusteringOrderBy; import org.apache.cassandra.service.consensus.TransactionalMode; import org.apache.cassandra.utils.AssertionUtils; @@ -135,8 +135,7 @@ private static BiFunction createSchemaSpec(AccordMo schemaGen = SchemaGenerators.schemaSpecGen("harry", "table", 1000); schema = schemaGen.generate(rng); - - HistoryBuilder harry = new ReplayingHistoryBuilder(schema.valueGenerators, + HistoryBuilder harry = new ReplayingHistoryBuilder(HistoryBuilder.valueGenerators(schema, rng.next(), 1000), hb -> { InJvmDTestVisitExecutor.Builder builder = InJvmDTestVisitExecutor.builder(); if (mode.kind == AccordMode.Kind.Direct) @@ -182,7 +181,7 @@ public int select(long lts) } return false; }) - .build(schema, hb, cluster); + .build(schema, hb.valueGenerators(), cluster); }); cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", schema.keyspace)); cluster.schemaChange(schema.compile()); @@ -239,7 +238,7 @@ private static CommandGen cqlOperations(Spec spec) TransactionalMode transationalMode = spec.schema.options.transactionalMode(); if (TransactionalMode.full == transationalMode) - reads.add(new HarryCommand(s -> String.format("Harry Reverse Validate pd=%d%s", pd, state.commandNamePostfix()), s -> spec.harry.selectPartition(pkIdx, Operations.ClusteringOrderBy.DESC))); + reads.add(new HarryCommand(s -> String.format("Harry Reverse Validate pd=%d%s", pd, state.commandNamePostfix()), s -> spec.harry.selectPartition(pkIdx, ClusteringOrderBy.DESC))); } reads.add(new HarryCommand(s -> "Reset Harry Write State" + state.commandNamePostfix(), s -> ((HarryState) s).numInserts = 0)); return Property.multistep(reads); @@ -255,7 +254,7 @@ public Spec(HistoryBuilder harry, SchemaSpec schema) { this.harry = harry; this.schema = schema; - this.pkGen = Generators.tracking(Generators.int32(0, schema.valueGenerators.pkPopulation())); + this.pkGen = Generators.tracking(Generators.adaptLongToInt(harry.valueGenerators().pkIdxGen())); } @Override diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/IdenticalTopologyTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/IdenticalTopologyTest.java index 91b6527801c6..90aed519b692 100644 --- a/test/distributed/org/apache/cassandra/fuzz/topology/IdenticalTopologyTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/topology/IdenticalTopologyTest.java @@ -27,6 +27,7 @@ import org.apache.cassandra.distributed.test.log.FuzzTestBase; import org.apache.cassandra.harry.SchemaSpec; import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder; import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; @@ -38,6 +39,7 @@ import org.apache.cassandra.service.consensus.TransactionalMode; import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; public class IdenticalTopologyTest extends FuzzTestBase { @@ -64,12 +66,13 @@ public void identicalTopologyTest() throws Throwable SchemaSpec schema = schemaGen.generate(rng); cluster.schemaChange(schema.compile()); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), POPULATION); + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, hb -> InJvmDTestVisitExecutor.builder() .consistencyLevel(ConsistencyLevel.QUORUM) .wrapQueries(QueryBuildingVisitExecutor.WrapQueries.TRANSACTION) .pageSizeSelector(p -> InJvmDTestVisitExecutor.PageSizeSelector.NO_PAGING) - .build(schema, hb, cluster)); + .build(schema, valueGenerators, cluster)); for (int i = 0; i <= 100; i++) { diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/JournalGCTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/JournalGCTest.java index d70192f13677..1f208a334698 100644 --- a/test/distributed/org/apache/cassandra/fuzz/topology/JournalGCTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/topology/JournalGCTest.java @@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; import org.junit.Assert; import org.junit.Test; @@ -47,11 +48,10 @@ import static org.apache.cassandra.db.ColumnFamilyStore.FlushReason.UNIT_TESTS; import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; public class JournalGCTest extends FuzzTestBase { - private static final int POPULATION = 1000; - @Test public void journalGCTest() throws Throwable { @@ -70,19 +70,20 @@ public void journalGCTest() throws Throwable Keyspace.open(SchemaConstants.ACCORD_KEYSPACE_NAME).getColumnFamilyStore(AccordKeyspace.JOURNAL).disableAutoCompaction(); }); - Generator schemaGen = SchemaGenerators.trivialSchema(KEYSPACE, () -> "bootstrap_fuzz", POPULATION, + Generator schemaGen = SchemaGenerators.trivialSchema(KEYSPACE, () -> "bootstrap_fuzz", SchemaSpec.optionsBuilder() .addWriteTimestamps(false) .withTransactionalMode(TransactionalMode.full)); SchemaSpec schema = schemaGen.generate(rng); cluster.schemaChange(schema.compile()); - HistoryBuilder history = new ReplayingHistoryBuilder(schema.valueGenerators, + IndexedValueGenerators valueGenerators = valueGenerators(schema, rng.next(), 1000); + HistoryBuilder history = new ReplayingHistoryBuilder(valueGenerators, hb -> InJvmDTestVisitExecutor.builder() .consistencyLevel(ConsistencyLevel.QUORUM) .wrapQueries(QueryBuildingVisitExecutor.WrapQueries.TRANSACTION) .pageSizeSelector(p -> InJvmDTestVisitExecutor.PageSizeSelector.NO_PAGING) - .build(schema, hb, cluster)); + .build(schema, valueGenerators, cluster)); for (int pk = 0; pk <= 500; pk++) { for (int i = 0; i < 100; i++) diff --git a/test/harry/main/org/apache/cassandra/harry/MagicConstants.java b/test/harry/main/org/apache/cassandra/harry/MagicConstants.java index 9e7137fccc37..2d5a6245d728 100644 --- a/test/harry/main/org/apache/cassandra/harry/MagicConstants.java +++ b/test/harry/main/org/apache/cassandra/harry/MagicConstants.java @@ -55,6 +55,11 @@ public String toString() 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); + + public static boolean isMagicDescriptor(long val) + { + return val <= UNSET_DESCR; + } /** * For LTS */ diff --git a/test/harry/main/org/apache/cassandra/harry/SchemaSpec.java b/test/harry/main/org/apache/cassandra/harry/SchemaSpec.java index f6f5b8bf224b..8884b1ccebc2 100644 --- a/test/harry/main/org/apache/cassandra/harry/SchemaSpec.java +++ b/test/harry/main/org/apache/cassandra/harry/SchemaSpec.java @@ -25,10 +25,8 @@ 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.service.consensus.TransactionalMode; import org.apache.cassandra.utils.ByteArrayUtil; @@ -46,25 +44,20 @@ public class SchemaSpec public final List> staticColumns; public final List> allColumnInSelectOrder; - public final ValueGenerators valueGenerators; public final Options options; - public SchemaSpec(long seed, - int populationPerColumn, - String keyspace, + public SchemaSpec(String keyspace, String table, List> partitionKeys, List> clusteringKeys, List> regularColumns, List> staticColumns) { - this(seed, populationPerColumn, keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, optionsBuilder()); + this(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, optionsBuilder()); } @SuppressWarnings({ "unchecked" }) - public SchemaSpec(long seed, - int populationPerColumn, - String keyspace, + public SchemaSpec(String keyspace, String table, List> partitionKeys, List> clusteringKeys, @@ -93,11 +86,11 @@ public SchemaSpec(long seed, regularSelectOrder)) selectOrder.add(column); this.allColumnInSelectOrder = Collections.unmodifiableList(selectOrder); - - // TODO: empty gen - this.valueGenerators = HistoryBuilder.valueGenerators(this, seed, populationPerColumn); } + /** + * A number of unique values that can be generated for a given combination of columns. + */ public static /* unsigned */ long cumulativeEntropy(List> columns) { if (columns.isEmpty()) @@ -383,7 +376,6 @@ public static class OptionsBuilder implements Options private boolean trackLts = false; private boolean compactStorage = false; private String speculativeRetry = null; - private OptionsBuilder() { } diff --git a/test/harry/main/org/apache/cassandra/harry/cql/DeleteHelper.java b/test/harry/main/org/apache/cassandra/harry/cql/DeleteHelper.java index 8b0e1ccea21d..0c7a8b8ce4bb 100644 --- a/test/harry/main/org/apache/cassandra/harry/cql/DeleteHelper.java +++ b/test/harry/main/org/apache/cassandra/harry/cql/DeleteHelper.java @@ -18,16 +18,19 @@ package org.apache.cassandra.harry.cql; +import java.nio.ByteBuffer; 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.db.marshal.AbstractType; import org.apache.cassandra.harry.ColumnSpec; import org.apache.cassandra.harry.Relations; import org.apache.cassandra.harry.SchemaSpec; import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.gen.ValueGenerators; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.util.BitSet; @@ -35,6 +38,7 @@ public class DeleteHelper { public static CompiledStatement inflateDelete(Operations.DeletePartition delete, SchemaSpec schema, + ValueGenerators valueGenerators, long timestamp) { StringBuilder b = new StringBuilder(); @@ -50,7 +54,7 @@ public static CompiledStatement inflateDelete(Operations.DeletePartition delete, List bindings = new ArrayList<>(); - Object[] pk = schema.valueGenerators.pkGen().inflate(delete.pd()); + Object[] pk = valueGenerators.pkGen().inflate(delete.pd()); RelationWriter writer = new RelationWriter(b, bindings::add) ; @@ -61,11 +65,19 @@ public static CompiledStatement inflateDelete(Operations.DeletePartition delete, Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); - return new CompiledStatement(b.toString(), bindingsArr); + CompiledStatement compiled = new CompiledStatement(false, b.toString(), bindingsArr); + { + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < pk.length; i++) + pkBuffers[i] = ((AbstractType)schema.partitionKeys.get(i).type.asServerType()).decompose(pk[i]); + compiled.setPk(pkBuffers); + } + return compiled; } public static CompiledStatement inflateDelete(Operations.DeleteRow delete, SchemaSpec schema, + ValueGenerators generators, long timestamp) { StringBuilder b = new StringBuilder(); @@ -81,8 +93,9 @@ public static CompiledStatement inflateDelete(Operations.DeleteRow delete, List bindings = new ArrayList<>(); - Object[] pk = schema.valueGenerators.pkGen().inflate(delete.pd()); - Object[] ck = schema.valueGenerators.ckGen().inflate(delete.cd()); + Object[] pk = generators.pkGen().inflate(delete.pd()); + ValueGenerators.PartitionValues valueGenerators = generators.forPd(delete.pd); + Object[] ck = valueGenerators.ckGen().inflate(delete.cd()); RelationWriter writer = new RelationWriter(b, bindings::add); @@ -95,11 +108,19 @@ public static CompiledStatement inflateDelete(Operations.DeleteRow delete, Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); - return new CompiledStatement(b.toString(), bindingsArr); + CompiledStatement compiled = new CompiledStatement(false, b.toString(), bindingsArr); + { + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < pk.length; i++) + pkBuffers[i] = ((AbstractType)schema.partitionKeys.get(i).type.asServerType()).decompose(pk[i]); + compiled.setPk(pkBuffers); + } + return compiled; } public static CompiledStatement inflateDelete(Operations.DeleteColumns delete, SchemaSpec schema, + ValueGenerators generators, long timestamp) { StringBuilder b = new StringBuilder(); @@ -139,8 +160,9 @@ public static CompiledStatement inflateDelete(Operations.DeleteColumns delete, List bindings = new ArrayList<>(); - Object[] pk = schema.valueGenerators.pkGen().inflate(delete.pd()); - Object[] ck = schema.valueGenerators.ckGen().inflate(delete.cd()); + Object[] pk = generators.pkGen().inflate(delete.pd()); + ValueGenerators.PartitionValues valueGenerators = generators.forPd(delete.pd); + Object[] ck = valueGenerators.ckGen().inflate(delete.cd()); RelationWriter writer = new RelationWriter(b, bindings::add); @@ -153,11 +175,19 @@ public static CompiledStatement inflateDelete(Operations.DeleteColumns delete, Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); - return new CompiledStatement(b.toString(), bindingsArr); + CompiledStatement compiled = new CompiledStatement(false, b.toString(), bindingsArr); + { + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < pk.length; i++) + pkBuffers[i] = ((AbstractType)schema.partitionKeys.get(i).type.asServerType()).decompose(pk[i]); + compiled.setPk(pkBuffers); + } + return compiled; } public static CompiledStatement inflateDelete(Operations.DeleteRange delete, SchemaSpec schema, + ValueGenerators generators, long timestamp) { StringBuilder b = new StringBuilder(); @@ -173,9 +203,10 @@ public static CompiledStatement inflateDelete(Operations.DeleteRange delete, 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()); + Object[] pk = generators.pkGen().inflate(delete.pd()); + ValueGenerators.PartitionValues valueGenerators = generators.forPd(delete.pd); + Object[] lowBound = valueGenerators.ckGen().inflate(delete.lowerBound()); + Object[] highBound = valueGenerators.ckGen().inflate(delete.upperBound()); RelationWriter writer = new RelationWriter(b, bindings::add); @@ -196,7 +227,14 @@ public static CompiledStatement inflateDelete(Operations.DeleteRange delete, Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); - return new CompiledStatement(b.toString(), bindingsArr); + CompiledStatement compiled = new CompiledStatement(false, b.toString(), bindingsArr); + { + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < pk.length; i++) + pkBuffers[i] = ((AbstractType)schema.partitionKeys.get(i).type.asServerType()).decompose(pk[i]); + compiled.setPk(pkBuffers); + } + return compiled; } private static final class RelationWriter diff --git a/test/harry/main/org/apache/cassandra/harry/cql/SelectHelper.java b/test/harry/main/org/apache/cassandra/harry/cql/SelectHelper.java index 8e71ae114e77..3f24effa8faf 100644 --- a/test/harry/main/org/apache/cassandra/harry/cql/SelectHelper.java +++ b/test/harry/main/org/apache/cassandra/harry/cql/SelectHelper.java @@ -18,6 +18,7 @@ package org.apache.cassandra.harry.cql; +import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; @@ -27,19 +28,26 @@ 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.db.marshal.AbstractType; 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.execution.CompiledStatement; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.op.ClusteringOrderBy; import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Selection; public class SelectHelper { - public static CompiledStatement select(Operations.SelectPartition select, SchemaSpec schema) + public static CompiledStatement select(Operations.SelectPartition select, + SchemaSpec schema, + ValueGenerators generators) { - Select.Builder builder = commmonPart(select, schema); + Select.Builder builder = commmonPart(select, schema, generators); - if (select.orderBy() == Operations.ClusteringOrderBy.DESC) + if (select.orderBy() == ClusteringOrderBy.DESC) { for (int i = 0; i < schema.clusteringKeys.size(); i++) { @@ -48,14 +56,28 @@ public static CompiledStatement select(Operations.SelectPartition select, Schema } } - return toCompiled(builder.build()); + CompiledStatement compiled = toCompiled(builder.build()); + { + Object[] pk = generators.pkGen().inflate(select.pd()); + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < schema.partitionKeys.size(); i++) + { + ColumnSpec column = schema.partitionKeys.get(i); + Object value = pk[i]; + pkBuffers[i] = ((AbstractType) column.type.asServerType()).decompose(value); + } + compiled.setPk(pkBuffers); + } + return compiled; } - public static CompiledStatement select(Operations.SelectRow select, SchemaSpec schema) + public static CompiledStatement select(Operations.SelectRow select, + SchemaSpec schema, + ValueGenerators generators) { - Select.Builder builder = commmonPart(select, schema); - - Object[] ck = schema.valueGenerators.ckGen().inflate(select.cd()); + Select.Builder builder = commmonPart(select, schema, generators); + ValueGenerators.PartitionValues valueGenerators = generators.forPd(select.pd); + Object[] ck = valueGenerators.ckGen().inflate(select.cd()); for (int i = 0; i < schema.clusteringKeys.size(); i++) { @@ -65,27 +87,42 @@ public static CompiledStatement select(Operations.SelectRow select, SchemaSpec s new Bind(ck[i], column.type.asServerType())); } - return toCompiled(builder.build()); + CompiledStatement compiled = toCompiled(builder.build()); + { + Object[] pk = generators.pkGen().inflate(select.pd()); + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < schema.partitionKeys.size(); i++) + { + ColumnSpec column = schema.partitionKeys.get(i); + Object value = pk[i]; + pkBuffers[i] = ((AbstractType) column.type.asServerType()).decompose(value); + } + compiled.setPk(pkBuffers); + } + return compiled; } - public static CompiledStatement select(Operations.SelectRange select, SchemaSpec schema) + public static CompiledStatement select(Operations.SelectRange select, + SchemaSpec schema, + ValueGenerators generators) { - Select.Builder builder = commmonPart(select, schema); + Select.Builder builder = commmonPart(select, schema, generators); - Object[] lowBound = schema.valueGenerators.ckGen().inflate(select.lowerBound()); - Object[] highBound = schema.valueGenerators.ckGen().inflate(select.upperBound()); + ValueGenerators.PartitionValues valueGenerators = generators.forPd(select.pd); + Object[] lowBound = select.lowerBound() == MagicConstants.UNSET_DESCR ? null : valueGenerators.ckGen().inflate(select.lowerBound()); + Object[] highBound = select.upperBound() == MagicConstants.UNSET_DESCR ? null : 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) + if (lowBound != null && 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) + if (highBound != null && select.upperBoundRelation()[i] != null) { builder.where(new Symbol(column.name, column.type.asServerType()), toInequality(select.upperBoundRelation()[i]), @@ -93,7 +130,7 @@ public static CompiledStatement select(Operations.SelectRange select, SchemaSpec } } - if (select.orderBy() == Operations.ClusteringOrderBy.DESC) + if (select.orderBy() == ClusteringOrderBy.DESC) { for (int i = 0; i < schema.clusteringKeys.size(); i++) { @@ -102,17 +139,32 @@ public static CompiledStatement select(Operations.SelectRange select, SchemaSpec } } - return toCompiled(builder.build()); + CompiledStatement compiled = toCompiled(builder.build()); + { + Object[] pk = generators.pkGen().inflate(select.pd()); + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < schema.partitionKeys.size(); i++) + { + ColumnSpec column = schema.partitionKeys.get(i); + Object value = pk[i]; + pkBuffers[i] = ((AbstractType) column.type.asServerType()).decompose(value); + } + compiled.setPk(pkBuffers); + } + return compiled; } - public static CompiledStatement select(Operations.SelectCustom select, SchemaSpec schema) + public static CompiledStatement select(Operations.SelectCustom select, + SchemaSpec schema, + ValueGenerators generators) { - Select.Builder builder = commmonPart(select, schema); + Select.Builder builder = commmonPart(select, schema, generators); + ValueGenerators.PartitionValues valueGenerators = generators.forPd(select.pd); Map cache = new HashMap<>(); for (Relations.Relation relation : select.ckRelations()) { - Object[] query = cache.computeIfAbsent(relation.descriptor, schema.valueGenerators.ckGen()::inflate); + Object[] query = cache.computeIfAbsent(relation.descriptor, valueGenerators.ckGen()::inflate); ColumnSpec column = schema.clusteringKeys.get(relation.column); builder.where(new Symbol(column.name, column.type.asServerType()), toInequality(relation.kind), @@ -122,7 +174,7 @@ public static CompiledStatement select(Operations.SelectCustom select, SchemaSpe for (Relations.Relation relation : select.regularRelations()) { ColumnSpec column = schema.regularColumns.get(relation.column); - Object query = schema.valueGenerators.regularColumnGen(relation.column).inflate(relation.descriptor); + Object query = 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())); @@ -130,14 +182,14 @@ public static CompiledStatement select(Operations.SelectCustom select, SchemaSpe for (Relations.Relation relation : select.staticRelations()) { - Object query = schema.valueGenerators.staticColumnGen(relation.column).inflate(relation.descriptor); + Object query = 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) + if (select.orderBy() == ClusteringOrderBy.DESC) { for (int i = 0; i < schema.clusteringKeys.size(); i++) { @@ -148,14 +200,28 @@ public static CompiledStatement select(Operations.SelectCustom select, SchemaSpe builder.allowFiltering(); - return toCompiled(builder.build()); + CompiledStatement compiled = toCompiled(builder.build()); + { + Object[] pk = generators.pkGen().inflate(select.pd()); + ByteBuffer[] pkBuffers = new ByteBuffer[pk.length]; + for (int i = 0; i < schema.partitionKeys.size(); i++) + { + ColumnSpec column = schema.partitionKeys.get(i); + Object value = pk[i]; + pkBuffers[i] = ((AbstractType) column.type.asServerType()).decompose(value); + } + compiled.setPk(pkBuffers); + } + return compiled; } - public static Select.Builder commmonPart(Operations.SelectStatement select, SchemaSpec schema) + public static Select.Builder commmonPart(Operations.SelectStatement select, + SchemaSpec schema, + ValueGenerators valueGenerators) { Select.Builder builder = new Select.Builder(); - Operations.Selection selection = Operations.Selection.fromBitSet(select.selection(), schema); + Selection selection = Selection.fromBitSet(select.selection(), schema); if (selection.isWildcard()) { builder.wildcard(); @@ -191,7 +257,7 @@ public static Select.Builder commmonPart(Operations.SelectStatement select, Sche builder.table(schema.keyspace, schema.table); - Object[] pk = schema.valueGenerators.pkGen().inflate(select.pd()); + Object[] pk = valueGenerators.pkGen().inflate(select.pd()); for (int i = 0; i < schema.partitionKeys.size(); i++) { ColumnSpec column = schema.partitionKeys.get(i); @@ -235,7 +301,7 @@ 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); + return new CompiledStatement(true, 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 index 48e9f10ed65f..82d6679b3958 100644 --- a/test/harry/main/org/apache/cassandra/harry/cql/WriteHelper.java +++ b/test/harry/main/org/apache/cassandra/harry/cql/WriteHelper.java @@ -18,30 +18,34 @@ package org.apache.cassandra.harry.cql; +import java.nio.ByteBuffer; import java.util.List; import accord.utils.Invariants; - +import org.apache.cassandra.db.marshal.AbstractType; 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.gen.ValueGenerators; import org.apache.cassandra.harry.op.Operations; public class WriteHelper { public static CompiledStatement inflateInsert(Operations.WriteOp op, SchemaSpec schema, + ValueGenerators gens, 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(); + ValueGenerators.PartitionValues valueGenerators = gens.forPd(op.pd); + assert op.vds().length == valueGenerators.regularColumnCount(); + assert op.sds().length == valueGenerators.staticColumnCount(); - Object[] partitionKey = schema.valueGenerators.pkGen().inflate(op.pd()); + Object[] partitionKey = gens.pkGen().inflate(op.pd()); assert partitionKey.length == schema.partitionKeys.size(); - Object[] clusteringKey = schema.valueGenerators.ckGen().inflate(op.cd()); + Object[] clusteringKey = 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]; @@ -52,7 +56,7 @@ public static CompiledStatement inflateInsert(Operations.WriteOp op, if (descriptor == MagicConstants.UNSET_DESCR) regularColumns[i] = MagicConstants.UNSET_VALUE; else - regularColumns[i] = schema.valueGenerators.regularColumnGen(i).inflate(descriptor); + regularColumns[i] = valueGenerators.regularColumnGen(i).inflate(descriptor); } for (int i = 0; i < op.sds().length; i++) @@ -61,7 +65,7 @@ public static CompiledStatement inflateInsert(Operations.WriteOp op, if (descriptor == MagicConstants.UNSET_DESCR) staticColumns[i] = MagicConstants.UNSET_VALUE; else - staticColumns[i] = schema.valueGenerators.staticColumnGen(i).inflate(descriptor); + staticColumns[i] = valueGenerators.staticColumnGen(i).inflate(descriptor); } Object[] bindings = new Object[schema.allColumnInSelectOrder.size()]; @@ -96,7 +100,15 @@ public static CompiledStatement inflateInsert(Operations.WriteOp op, } b.append(";"); - return new CompiledStatement(b.toString(), adjustArraySize(bindings, bindingsCount)); + + CompiledStatement compiled = new CompiledStatement(false, b.toString(), adjustArraySize(bindings, bindingsCount)); + { + ByteBuffer[] pkBuffers = new ByteBuffer[partitionKey.length]; + for (int i = 0; i < partitionKey.length; i++) + pkBuffers[i] = ((AbstractType)schema.partitionKeys.get(i).type.asServerType()).decompose(partitionKey[i]); + compiled.setPk(pkBuffers); + } + return compiled; } public static Object[] adjustArraySize(Object[] bindings, int bindingsCount) @@ -112,25 +124,28 @@ public static Object[] adjustArraySize(Object[] bindings, int bindingsCount) public static CompiledStatement inflateUpdate(Operations.WriteOp op, SchemaSpec schema, + ValueGenerators gens, 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); + ValueGenerators.PartitionValues valueGenerators = gens.forPd(op.pd); + assert op.vds().length == valueGenerators.regularColumnCount(); + assert op.sds().length == valueGenerators.staticColumnCount(); + + Object[] partitionKey = gens.pkGen().inflate(op.pd); assert partitionKey.length == schema.partitionKeys.size(); - Object[] clusteringKey = schema.valueGenerators.ckGen().inflate(op.cd()); + Object[] clusteringKey = 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]); + regularColumns[i] = 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]); + staticColumns[i] = valueGenerators.staticColumnGen(i).inflate(op.sds()[i]); Object[] bindings = new Object[schema.allColumnInSelectOrder.size()]; @@ -143,10 +158,11 @@ public static CompiledStatement inflateUpdate(Operations.WriteOp op, if (timestamp != -1 && schema.options.addWriteTimestamps()) { b.append(" USING TIMESTAMP ") - .append(timestamp) - .append(" SET "); + .append(timestamp); } + b.append(" SET "); + int bindingsCount = 0; bindingsCount += addSetStatements(b, bindings, schema.regularColumns, regularColumns, bindingsCount); if (staticColumns.length != 0) @@ -158,7 +174,15 @@ public static CompiledStatement inflateUpdate(Operations.WriteOp op, 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)); + + CompiledStatement compiled = new CompiledStatement(false, b.toString(), adjustArraySize(bindings, bindingsCount)); + { + ByteBuffer[] pkBuffers = new ByteBuffer[partitionKey.length]; + for (int i = 0; i < partitionKey.length; i++) + pkBuffers[i] = ((AbstractType)schema.partitionKeys.get(i).type.asServerType()).decompose(partitionKey[i]); + compiled.setPk(pkBuffers); + } + return compiled; } private static int addSetStatements(StringBuilder b, 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 36514296cc72..f2d3eed1a734 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java @@ -27,16 +27,16 @@ import java.util.Map; import java.util.stream.Collectors; +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.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.model.Model; +import org.apache.cassandra.harry.op.ClusteringOrderBy; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.op.Visit; import org.apache.cassandra.harry.util.BitSet; @@ -47,33 +47,25 @@ 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 +public class HistoryBuilder implements SingleOperationBuilder, Model.FullReplay { protected final IndexedValueGenerators valueGenerators; - protected final IndexGenerators indexGenerators; protected int nextOpIdx = 0; - // TODO: would be great to have a very simple B-Tree here - protected final Map log; + // TODO: would be great to have a very simple B-Tree here, potentially append-only + protected final ArrayList log; public static HistoryBuilder fromSchema(SchemaSpec schemaSpec, long seed, int population) { IndexedValueGenerators generators = valueGenerators(schemaSpec, seed, population); - return new HistoryBuilder(generators, IndexGenerators.withDefaults(generators)); + return new HistoryBuilder(generators); } - public HistoryBuilder(ValueGenerators generators) + public HistoryBuilder(IndexedValueGenerators valueGenerators) { - this((IndexedValueGenerators) generators, IndexGenerators.withDefaults(generators)); - } - - public HistoryBuilder(IndexedValueGenerators valueGenerators, - IndexGenerators indexGenerators) - { - this.log = new HashMap<>(); + this.log = new ArrayList<>(1024); this.valueGenerators = valueGenerators; - this.indexGenerators = indexGenerators; } public IndexedValueGenerators valueGenerators() @@ -86,12 +78,11 @@ public int size() return log.size(); } - @Override public Iterator iterator() { return new Iterator<>() { - long replayed = 0; + int replayed = 0; public boolean hasNext() { @@ -105,25 +96,13 @@ public Visit next() }; } - @Override - public Visit replay(long lts) - { - return log.get(lts); - } - - @Override - public Operations.Operation replay(long lts, int opId) - { - return replay(lts).operations[opId]; - } - SingleOperationVisitBuilder singleOpVisitBuilder() { long visitLts = nextOpIdx++; + Invariants.require(visitLts == log.size()); return new SingleOperationVisitBuilder(visitLts, valueGenerators, - indexGenerators, - (visit) -> log.put(visit.lts, visit)); + log::add); } ; @@ -133,8 +112,7 @@ public MultiOperationVisitBuilder multistep() long visitLts = nextOpIdx++; return new MultiOperationVisitBuilder(visitLts, valueGenerators, - indexGenerators, - visit -> log.put(visit.lts, visit)); + log::add); } @Override @@ -240,7 +218,7 @@ public SingleOperationBuilder selectPartition(int pdIdx) } @Override - public SingleOperationBuilder selectPartition(int pdIdx, Operations.ClusteringOrderBy orderBy) + public SingleOperationBuilder selectPartition(int pdIdx, ClusteringOrderBy orderBy) { singleOpVisitBuilder().selectPartition(pdIdx, orderBy); return this; @@ -313,21 +291,23 @@ public SingleOperationBuilder deleteRowSliceByUpperBound(int pdIdx, int upperBou */ public interface IndexedBijection extends Bijections.Bijection { - int idxFor(long descriptor); + long idxFor(long descriptor); - long descriptorAt(int idx); + long descriptorAt(long idx); @Override default String toString(long pd) { if (pd == MagicConstants.UNSET_DESCR) - return Integer.toString(MagicConstants.UNSET_IDX); + return Long.toString(MagicConstants.UNSET_IDX); if (pd == MagicConstants.NIL_DESCR) - return Integer.toString(MagicConstants.NIL_IDX); + return Long.toString(MagicConstants.NIL_IDX); - return Integer.toString(idxFor(pd)); + return Long.toString(idxFor(pd)); } + + default void discard(){} } public static IndexedValueGenerators valueGenerators(SchemaSpec schema, long seed) @@ -358,64 +338,20 @@ public static IndexedValueGenerators valueGenerators(SchemaSpec schema, long see 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) - { - super(pkGen, ckGen, ArrayAccessor.instance, - (List>) (List) regularColumnGens, - (List>) (List) staticColumnGens, - pkComparators, ckComparators, regularComparators, staticComparators); - } - - @Override - public IndexedBijection pkGen() - { - return (IndexedBijection) super.pkGen(); - } - - @Override - public IndexedBijection ckGen() - { - 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 new IndexedValueGenerators.Shared(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()), + ckComparators, + regularComparators, + staticComparators); + } + + public static Comparator keyComparator(List> columns) { return (o1, o2) -> compareKeys(columns, o1, o2); } diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilderHelper.java b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilderHelper.java index 21893a035243..51710880ffa4 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilderHelper.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilderHelper.java @@ -49,41 +49,32 @@ 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) + // TODO: bring back +// 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 pdIdx, 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)); + vIdxs[i] = rng.nextDouble() <= chanceOfUnset ? MagicConstants.UNSET_IDX : rng.nextInt(history.valueGenerators().forPdIdx(pdIdx).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); + sIdxs[i] = rng.nextDouble() <= chanceOfUnset ? MagicConstants.UNSET_IDX : rng.nextInt(history.valueGenerators().forPdIdx(pdIdx).staticPopulation(i)); + history.insert(pdIdx, rowIdx, vIdxs, sIdxs); } public static void deleteRandomColumns(SchemaSpec schema, int partitionIdx, int rowIdx, EntropySource rng, SingleOperationBuilder history) diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/IndexedValueGenerators.java b/test/harry/main/org/apache/cassandra/harry/dsl/IndexedValueGenerators.java new file mode 100644 index 000000000000..9826e9da0351 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/IndexedValueGenerators.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.util.Comparator; +import java.util.List; + +import org.apache.cassandra.harry.gen.IndexGenerators; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.gen.Bijections; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; + +public abstract class IndexedValueGenerators extends ValueGenerators +{ + private final Generator pkIdxGen; + + public IndexedValueGenerators(HistoryBuilder.IndexedBijection pkGen) + { + super(pkGen); + this.pkIdxGen = Generators.int64(0, pkGen().population()); + } + + public IndexedPartitionValues forPdIdx(long pdIdx) + { + return forPd(pkGen().descriptorAt(pdIdx)); + } + + @Override + public HistoryBuilder.IndexedBijection pkGen() + { + return (HistoryBuilder.IndexedBijection) super.pkGen(); + } + + @Override + public abstract IndexedPartitionValues forPd(long pd); + + public Generator pkIdxGen() + { + return pkIdxGen; + } + + /** + * Indexed value generators, for which values that can be generated inside each partition are shared + */ + public static class Shared extends IndexedValueGenerators + { + private final IndexedPartitionValues partitionValues; + + public Shared(HistoryBuilder.IndexedBijection pkGen, + HistoryBuilder.IndexedBijection ckGen, + List> regularColumnGens, + List> staticColumnGens, + List> ckComparators, + List> regularComparators, + List> staticComparators) + { + super(pkGen); + this.partitionValues = IndexedPartitionValues.uniform(ckGen, + regularColumnGens, + staticColumnGens, + ckComparators, + regularComparators, + staticComparators); + } + + @Override + public IndexedPartitionValues forPd(long pd) + { + return partitionValues; + } + } + + public static class IndexedPartitionValues extends PartitionValues + { + public final Generator ckIdxGen; + public final Generator[] regularIdxGens; + public final Generator[] staticIdxGens; + + /** + * Index partition value wrapper with random value pickers + */ + public static IndexedPartitionValues uniform(Bijections.Bijection ckGen, + List> regularColumnGens, + List> staticColumnGens, + List> ckComparators, + List> regularComparators, + List> staticComparators) + { + return new IndexedPartitionValues(ckGen, + + regularColumnGens, + staticColumnGens, + ckComparators, + regularComparators, + staticComparators, + + IndexGenerators.uniform(ckGen), + IndexGenerators.uniform(regularColumnGens), + IndexGenerators.uniform(regularColumnGens)); + } + + public IndexedPartitionValues(Bijections.Bijection ckGen, + + List> regularColumnGens, + List> staticColumnGens, + List> ckComparators, + List> regularComparators, + List> staticComparators, + + Generator ckIdxGen, + Generator[] regularIdxGens, + Generator[] staticIdxGens) + { + super(ckGen, ArrayAccessor.instance, regularColumnGens, staticColumnGens, ckComparators, regularComparators, staticComparators); + + this.ckIdxGen = ckIdxGen; + this.regularIdxGens = regularIdxGens; + this.staticIdxGens = staticIdxGens; + } + + @Override + public HistoryBuilder.IndexedBijection ckGen() + { + return (HistoryBuilder.IndexedBijection) super.ckGen(); + } + + @Override + public HistoryBuilder.IndexedBijection regularColumnGen(int idx) + { + return (HistoryBuilder.IndexedBijection) super.regularColumnGen(idx); + } + + @Override + public HistoryBuilder.IndexedBijection staticColumnGen(int idx) + { + return (HistoryBuilder.IndexedBijection) super.staticColumnGen(idx); + } + + public Generator ckIdxGen() + { + return ckIdxGen; + } + public Generator[] regularIdxGens() + { + return regularIdxGens; + } + + public Generator[] staticIdxGens() + { + return staticIdxGens; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/MultiOperationVisitBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/MultiOperationVisitBuilder.java index 7dbff3db2e14..96434f52073d 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/MultiOperationVisitBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/MultiOperationVisitBuilder.java @@ -21,17 +21,14 @@ 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) + MultiOperationVisitBuilder(long lts, IndexedValueGenerators valueGenerators, Consumer appendToLog) { - super(lts, valueGenerators, indexGenerators, appendToLog); + super(lts, valueGenerators, appendToLog); } @Override 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 830c3ad4efcd..ef6fcfd400b4 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java @@ -20,28 +20,28 @@ import java.util.function.Function; +import accord.utils.Invariants; 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 CQLVisitExecutor executor; - public ReplayingHistoryBuilder(ValueGenerators generators, Function executorFactory) + public ReplayingHistoryBuilder(IndexedValueGenerators generators, Function executorFactory) { - super((IndexedValueGenerators) generators); + super(generators); this.executor = executorFactory.apply(this); - } + } SingleOperationVisitBuilder singleOpVisitBuilder() { long visitLts = nextOpIdx++; HistoryBuilder this_ = this; + Invariants.require(visitLts == log.size()); return new SingleOperationVisitBuilder(visitLts, valueGenerators, - indexGenerators, - (visit) -> log.put(visit.lts, visit)) { + log::add) { @Override Visit build() { @@ -59,8 +59,7 @@ public MultiOperationVisitBuilder multistep() HistoryBuilder this_ = this; return new MultiOperationVisitBuilder(visitLts, valueGenerators, - indexGenerators, - visit -> log.put(visit.lts, visit)) { + log::add) { @Override Visit buildInternal() { 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 49a73268a302..034837a3860c 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java @@ -19,6 +19,7 @@ package org.apache.cassandra.harry.dsl; import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.op.ClusteringOrderBy; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.util.BitSet; import org.apache.cassandra.harry.util.ThrowingRunnable; @@ -46,7 +47,7 @@ default SingleOperationBuilder customThrowing(ThrowingRunnable runnable, String 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 selectPartition(int pdIdx, 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); 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 bd8579fef798..5aec3cad2237 100644 --- a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java @@ -27,23 +27,23 @@ import org.slf4j.LoggerFactory; import accord.utils.Invariants; - import org.apache.cassandra.harry.MagicConstants; import org.apache.cassandra.harry.Relations; -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.op.ClusteringOrderBy; +import org.apache.cassandra.harry.op.Kind; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.op.Visit; import org.apache.cassandra.harry.util.BitSet; -import static org.apache.cassandra.harry.dsl.HistoryBuilder.IndexedValueGenerators; -import static org.apache.cassandra.harry.op.Operations.Kind; +import static org.apache.cassandra.harry.dsl.IndexedValueGenerators.IndexedPartitionValues; import static org.apache.cassandra.harry.op.Operations.Operation; import static org.apache.cassandra.harry.op.Operations.WriteOp; -class SingleOperationVisitBuilder implements SingleOperationBuilder +// TODO: make package-private again +public class SingleOperationVisitBuilder implements SingleOperationBuilder { private static final Logger logger = LoggerFactory.getLogger(SingleOperationVisitBuilder.class); @@ -59,11 +59,9 @@ class SingleOperationVisitBuilder implements SingleOperationBuilder protected int opIdCounter; protected final IndexedValueGenerators valueGenerators; - protected final IndexGenerators indexGenerators; SingleOperationVisitBuilder(long lts, IndexedValueGenerators valueGenerators, - IndexGenerators indexGenerators, Consumer appendToLog) { this.operations = new ArrayList<>(); @@ -73,7 +71,6 @@ class SingleOperationVisitBuilder implements SingleOperationBuilder this.opIdCounter = 0; this.valueGenerators = valueGenerators; - this.indexGenerators = indexGenerators; this.seedSelector = new PureRng.PCGFast(lts); } @@ -84,7 +81,8 @@ public SingleOperationBuilder insert() int opId = opIdCounter++; long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); return rngSupplier.computeWithSeed(seed, rng -> { - int pdIdx = indexGenerators.pkIdxGen.generate(rng); + long pdIdx = valueGenerators.pkIdxGen().generate(rng); + IndexedPartitionValues indexGenerators = valueGenerators.forPdIdx(pdIdx); int cdIdx = indexGenerators.ckIdxGen.generate(rng); int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; @@ -93,7 +91,7 @@ public SingleOperationBuilder insert() 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); + return write(pdIdx, cdIdx, valueIdxs, sValueIdxs, Kind.INSERT); }); } @@ -102,6 +100,7 @@ public SingleOperationBuilder insert(int pdIdx) { int opId = opIdCounter++; long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + IndexedPartitionValues indexGenerators = valueGenerators.forPdIdx(pdIdx); return rngSupplier.computeWithSeed(seed, rng -> { int cdIdx = indexGenerators.ckIdxGen.generate(rng); @@ -120,6 +119,7 @@ public SingleOperationBuilder insert(int pdIdx, int cdIdx) { int opId = opIdCounter++; long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + IndexedPartitionValues indexGenerators = valueGenerators.forPdIdx(pdIdx); return rngSupplier.computeWithSeed(seed, rng -> { int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; for (int i = 0; i < valueIdxs.length; i++) @@ -166,7 +166,8 @@ 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); + long pdIdx = valueGenerators.pkIdxGen().generate(rng); + IndexedPartitionValues indexGenerators = valueGenerators.forPdIdx(pdIdx); int cdIdx = indexGenerators.ckIdxGen.generate(rng); int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; @@ -175,7 +176,7 @@ public SingleOperationBuilder update() 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); + return write(pdIdx, cdIdx, valueIdxs, sValueIdxs, Kind.UPDATE); }); } @@ -184,6 +185,7 @@ public SingleOperationBuilder update(int pdIdx) { int opId = opIdCounter++; long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + IndexedPartitionValues indexGenerators = valueGenerators.forPdIdx(pdIdx); return rngSupplier.computeWithSeed(seed, rng -> { int cdIdx = indexGenerators.ckIdxGen.generate(rng); @@ -202,6 +204,7 @@ public SingleOperationBuilder update(int pdIdx, int cdIdx) { int opId = opIdCounter++; long seed = PCGFastPure.shuffle(PCGFastPure.advanceState(lts, opId, lts)); + IndexedPartitionValues indexGenerators = valueGenerators.forPdIdx(pdIdx); return rngSupplier.computeWithSeed(seed, rng -> { int[] valueIdxs = new int[indexGenerators.regularIdxGens.length]; for (int i = 0; i < valueIdxs.length; i++) @@ -219,15 +222,25 @@ public SingleOperationBuilder update(int pdIdx, int cdIdx, int[] valueIdxs, int[ return write(pdIdx, cdIdx, valueIdxs, sValueIdxs, Kind.UPDATE); } - private SingleOperationBuilder write(int pdIdx, int cdIdx, int[] valueIdxs, int[] sValueIdxs, Kind kind) + public SingleOperationBuilder write(long pdIdx, int cdIdx, int[] valueIdxs, int[] sValueIdxs, Kind kind) + { + WriteOp op = write(valueGenerators.pkGen(), valueGenerators.forPdIdx(pdIdx), lts, pdIdx, cdIdx, valueIdxs, sValueIdxs, kind); + opIdCounter++; + operations.add(op); + build(); + return this; + } + + // TODO: extract + public static WriteOp write(HistoryBuilder.IndexedBijection pkGen, IndexedPartitionValues valueGenerators, + long lts, long 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 pd = pkGen.descriptorAt(pdIdx); long cd = valueGenerators.ckGen().descriptorAt(cdIdx); - opIdCounter++; long[] vds = new long[valueIdxs.length]; for (int i = 0; i < valueGenerators.regularColumnCount(); i++) { @@ -248,24 +261,23 @@ private SingleOperationBuilder write(int pdIdx, int cdIdx, int[] valueIdxs, int[ sds[i] = valueGenerators.staticColumnGen(i).descriptorAt(valueIdx); } - operations.add(new WriteOp(lts, pd, cd, vds, sds, kind) { + return new WriteOp(lts, pd, cd, vds, sds, kind) { @Override public String toString() { return String.format("%s (%d, %d, %s, %s)", kind, pdIdx, cdIdx, Arrays.toString(valueIdxs), Arrays.toString(sValueIdxs)); } - }); - build(); - return this; + }; } @Override public SingleOperationVisitBuilder deleteRowRange(int pdIdx, int lowerBoundRowIdx, int upperBoundRowIdx, int nonEqFrom, boolean includeLowerBound, boolean includeUpperBound) { - long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + long pd = this.valueGenerators.pkGen().descriptorAt(pdIdx); + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); @@ -325,6 +337,7 @@ public SingleOperationBuilder deleteRow(int pdIdx, int rowIdx) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); opIdCounter++; + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long cd = valueGenerators.ckGen().descriptorAt(rowIdx); operations.add(new Operations.DeleteRow(lts, pd, cd) { @Override @@ -342,6 +355,7 @@ public SingleOperationBuilder deleteColumns(int pdIdx, int rowIdx, BitSet regula { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); opIdCounter++; + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long cd = valueGenerators.ckGen().descriptorAt(rowIdx); operations.add(new Operations.DeleteColumns(lts, pd, cd, regularSelection, staticSelection) { @Override @@ -358,6 +372,7 @@ public String toString() public SingleOperationBuilder deleteRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean includeBound) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); Relations.RelationKind[] lowerBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; @@ -391,6 +406,7 @@ public String toString() public SingleOperationBuilder deleteRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean includeBound) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); Relations.RelationKind[] upperBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; @@ -426,6 +442,7 @@ public SingleOperationVisitBuilder selectRowRange(int pdIdx, int lowerBoundRowId int nonEqFrom, boolean includeLowerBound, boolean includeUpperBound) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); @@ -459,6 +476,7 @@ public SingleOperationBuilder select(int pdIdx, IdxRelation[] ckIdxRelations, Id long pd = valueGenerators.pkGen().descriptorAt(pdIdx); opIdCounter++; + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); Relations.Relation[] ckRelations = new Relations.Relation[ckIdxRelations.length]; for (int i = 0; i < ckRelations.length; i++) { @@ -503,7 +521,7 @@ public SingleOperationBuilder selectPartition(int pdIdx) } @Override - public SingleOperationBuilder selectPartition(int pdIdx, Operations.ClusteringOrderBy orderBy) + public SingleOperationBuilder selectPartition(int pdIdx, ClusteringOrderBy orderBy) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); opIdCounter++; @@ -518,6 +536,7 @@ public SingleOperationBuilder selectRow(int pdIdx, int rowIdx) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); opIdCounter++; + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long cd = valueGenerators.ckGen().descriptorAt(rowIdx); operations.add(new Operations.SelectRow(lts, pd, cd)); build(); @@ -528,6 +547,7 @@ public SingleOperationBuilder selectRow(int pdIdx, int rowIdx) public SingleOperationBuilder selectRowSliceByLowerBound(int pdIdx, int lowerBoundRowIdx, int nonEqFrom, boolean includeBound) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long lowerBoundCd = valueGenerators.ckGen().descriptorAt(lowerBoundRowIdx); Relations.RelationKind[] lowerBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; @@ -554,6 +574,7 @@ public SingleOperationBuilder selectRowSliceByLowerBound(int pdIdx, int lowerBou public SingleOperationBuilder selectRowSliceByUpperBound(int pdIdx, int upperBoundRowIdx, int nonEqFrom, boolean includeBound) { long pd = valueGenerators.pkGen().descriptorAt(pdIdx); + IndexedPartitionValues valueGenerators = this.valueGenerators.forPd(pd); long upperBoundCd = valueGenerators.ckGen().descriptorAt(upperBoundRowIdx); Relations.RelationKind[] upperBoundRelations = new Relations.RelationKind[valueGenerators.ckColumnCount()]; diff --git a/test/harry/main/org/apache/cassandra/harry/execution/CQLTesterVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/CQLTesterVisitExecutor.java index 4a514169d035..660a4d541772 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/CQLTesterVisitExecutor.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/CQLTesterVisitExecutor.java @@ -30,8 +30,10 @@ import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.harry.ColumnSpec; import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.ValueGenerators; import org.apache.cassandra.harry.model.Model; import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Selection; import org.apache.cassandra.harry.op.Visit; import static org.apache.cassandra.harry.MagicConstants.LTS_UNKNOWN; @@ -43,15 +45,20 @@ public class CQLTesterVisitExecutor extends CQLVisitExecutor { private static final Logger logger = LoggerFactory.getLogger(CQLTesterVisitExecutor.class); + private final Function execute; + private final ValueGenerators valueGenerators; public CQLTesterVisitExecutor(SchemaSpec schema, + ValueGenerators valueGenerators, DataTracker dataTracker, Model model, Function execute) { - super(schema, dataTracker, model, new QueryBuildingVisitExecutor(schema, QueryBuildingVisitExecutor.WrapQueries.UNLOGGED_BATCH)); + super(schema, dataTracker, model, + new QueryBuildingVisitExecutor(schema, QueryBuildingVisitExecutor.WrapQueries.UNLOGGED_BATCH, valueGenerators)); this.execute = execute; + this.valueGenerators = valueGenerators; } @Override @@ -60,8 +67,9 @@ public List executeWithResult(Visit visit, CompiledStatement state List actual = new ArrayList<>(); // TODO: Have never tested with multiple Invariants.require(visit.operations.length == 1); + // TODO: for now, cql tester only supports "global" value gens for (UntypedResultSet.Row row : execute.apply(statement)) - actual.add(resultSetToRow(schema, (Operations.SelectStatement) visit.operations[0], row)); + actual.add(resultSetToRow(schema, valueGenerators, (Operations.SelectStatement) visit.operations[0], row)); return actual; } @@ -70,10 +78,12 @@ protected void executeWithoutResult(Visit visit, CompiledStatement statement) execute.apply(statement); } - public static ResultSetRow resultSetToRow(SchemaSpec schema, Operations.SelectStatement select, UntypedResultSet.Row row) + public static ResultSetRow resultSetToRow(SchemaSpec schema, + ValueGenerators generators, + Operations.SelectStatement select, UntypedResultSet.Row row) { // TODO: do we want to use selection? - Operations.Selection selection = Operations.Selection.fromBitSet(select.selection(), schema); + Selection selection = Selection.fromBitSet(select.selection(), schema); long pd = UNKNOWN_DESCR; if (selection.selectsAllOf(schema.partitionKeys)) @@ -85,10 +95,10 @@ public static ResultSetRow resultSetToRow(SchemaSpec schema, Operations.SelectSt partitionKey[i] = column.type.asServerType().compose(row.getBytes(column.name)); } - pd = schema.valueGenerators.pkGen().deflate(partitionKey); + pd = generators.pkGen().deflate(partitionKey); } - + ValueGenerators.PartitionValues valueGenerators = Invariants.nonNull(generators.forPd(pd)); long cd = UNKNOWN_DESCR; if (selection.selectsAllOf(schema.clusteringKeys)) { @@ -116,7 +126,7 @@ public static ResultSetRow resultSetToRow(SchemaSpec schema, Operations.SelectSt if (clusteringKey == NIL_KEY) cd = UNSET_DESCR; else - cd = schema.valueGenerators.ckGen().deflate(clusteringKey); + cd = valueGenerators.ckGen().deflate(clusteringKey); } long[] regularColumns = new long[schema.regularColumns.size()]; @@ -128,7 +138,7 @@ public static ResultSetRow resultSetToRow(SchemaSpec schema, Operations.SelectSt if (row.has(column.name)) { Object value = column.type.asServerType().compose(row.getBytes(column.name)); - regularColumns[i] = schema.valueGenerators.regularColumnGen(i).deflate(value); + regularColumns[i] = valueGenerators.regularColumnGen(i).deflate(value); } else { @@ -150,7 +160,7 @@ public static ResultSetRow resultSetToRow(SchemaSpec schema, Operations.SelectSt if (row.has(column.name)) { Object value = column.type.asServerType().compose(row.getBytes(column.name)); - staticColumns[i] = schema.valueGenerators.staticColumnGen(i).deflate(value); + staticColumns[i] = valueGenerators.staticColumnGen(i).deflate(value); } else { diff --git a/test/harry/main/org/apache/cassandra/harry/execution/CQLVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/CQLVisitExecutor.java index c482842b6a3c..760ef7e2faab 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/CQLVisitExecutor.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/CQLVisitExecutor.java @@ -19,6 +19,7 @@ package org.apache.cassandra.harry.execution; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -30,13 +31,10 @@ 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.Kind; 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); @@ -54,7 +52,7 @@ public CQLVisitExecutor(SchemaSpec schema, DataTracker dataTracker, Model model, this.queryBuilder = queryBuilder; } - public static void replay(CQLVisitExecutor executor, Model.Replay replay) + public static void replay(CQLVisitExecutor executor, Model.FullReplay replay) { for (Visit visit : replay) { @@ -64,7 +62,7 @@ public static void replay(CQLVisitExecutor executor, Model.Replay replay) } } - public static void executeVisit(Visit visit, CQLVisitExecutor executor, Model.Replay replay) + public static void executeVisit(Visit visit, CQLVisitExecutor executor, Model.FullReplay replay) { try { @@ -88,7 +86,7 @@ public enum ResultDumpMode LAST_50 } - public static void replayAfterFailure(Visit visit, CQLVisitExecutor executor, Model.Replay replay) + public static void replayAfterFailure(Visit visit, CQLVisitExecutor executor, Model.FullReplay replay) { QueryBuildingVisitExecutor queryBuilder = executor.queryBuilder; if (!visit.hasCustom) @@ -123,9 +121,12 @@ public static void replayAfterFailure(Visit visit, CQLVisitExecutor executor, Mo } } - private static boolean intersects(Set set1, Set set2) + private static boolean intersects(long[] v1, long[] v2) { - for (Object o : set1) + Set set2 = new HashSet<>(); + for (Long l : v2) + set2.add(l); + for (long o : v1) { if (set2.contains(o)) return true; @@ -136,25 +137,31 @@ private static boolean intersects(Set set1, Set set2) 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.requireArgument(Arrays.stream(visit.operations).allMatch(op -> op.kind() == Operations.Kind.CUSTOM)); - return; - } - - List selects = compiledStatement.selects; - if (selects.isEmpty()) + try { - executeMutatingVisit(visit, compiledStatement); + QueryBuildingVisitExecutor.BuiltQuery compiledStatement = queryBuilder.compile(visit); + // All operations are not touching any data + if (compiledStatement == null) + { + Invariants.requireArgument(Arrays.stream(visit.operations).allMatch(op -> op.kind() == Kind.CUSTOM)); + return; + } + + List selects = compiledStatement.selects; + if (selects.isEmpty()) + { + executeMutatingVisit(visit, compiledStatement); + } + else + { + Invariants.require(selects.size() == 1); + executeValidatingVisit(visit, selects, compiledStatement); + } } - else + finally { - Invariants.require(selects.size() == 1); - executeValidatingVisit(visit, selects, compiledStatement); + dataTracker.end(visit); } - dataTracker.end(visit); } // Lives in a separate method so that it is easier to override it diff --git a/test/harry/main/org/apache/cassandra/harry/execution/CompiledStatement.java b/test/harry/main/org/apache/cassandra/harry/execution/CompiledStatement.java index 8e84a64b6188..100ba1f6e34a 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/CompiledStatement.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/CompiledStatement.java @@ -18,18 +18,22 @@ package org.apache.cassandra.harry.execution; +import org.apache.cassandra.distributed.api.ConsistencyLevel; + import java.net.InetAddress; +import java.nio.ByteBuffer; import java.util.UUID; -import org.apache.cassandra.distributed.api.ConsistencyLevel; - public class CompiledStatement { + public final boolean validating; private final String cql; private final Object[] bindings; + private ByteBuffer[] pk; - public CompiledStatement(String cql, Object... bindings) + public CompiledStatement(boolean validating, String cql, Object... bindings) { + this.validating = validating; this.cql = cql; this.bindings = bindings; } @@ -41,26 +45,33 @@ public String cql() public CompiledStatement withSchema(String oldKs, String oldTable, String newKs, String newTable) { - return new CompiledStatement(cql.replace(oldKs + "." + oldTable, - newKs + "." + newTable), - bindings); + CompiledStatement statement = new CompiledStatement(validating, cql.replace(oldKs + "." + oldTable, + newKs + "." + newTable), + bindings); + statement.setPk(pk); + return statement; } public CompiledStatement withFiltering() { - return new CompiledStatement(cql.replace(";", + return new CompiledStatement(validating, cql.replace(";", " ALLOW FILTERING;"), bindings); } - public Object[] bindings() + public void setPk(ByteBuffer[] pk) { - return bindings; + this.pk = pk; + } + + public ByteBuffer[] pk() + { + return pk; } - public static CompiledStatement create(String cql, Object... bindings) + public Object[] bindings() { - return new CompiledStatement(cql, bindings); + return bindings; } public String 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 index ec2b1d95c9c0..2cb5098567fa 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/DataTracker.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/DataTracker.java @@ -20,24 +20,26 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import accord.utils.Invariants; - import org.apache.cassandra.harry.model.Model; -import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Kind; import org.apache.cassandra.harry.op.Visit; -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; +import static org.apache.cassandra.harry.op.Kind.CUSTOM; +import static org.apache.cassandra.harry.op.Kind.SELECT_CUSTOM; +import static org.apache.cassandra.harry.op.Kind.SELECT_PARTITION; +import static org.apache.cassandra.harry.op.Kind.SELECT_RANGE; +import static org.apache.cassandra.harry.op.Kind.SELECT_ROW; +import static org.apache.cassandra.harry.op.Operations.Operation; +import static org.apache.cassandra.harry.op.Operations.PartitionOperation; /** * Data tracker tracks every operation that was started and finished. @@ -50,22 +52,21 @@ public interface DataTracker { void begin(Visit visit); void end(Visit visit); + default void gc(long pd) {} - 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); + 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_ + * Data tracker that only allows partition visits to be done _in sequence_. + * + * This data tracker is _not_ thread safe. */ - class SequentialDataTracker implements DataTracker + class SequentialDataTracker implements DataTracker, Model.PartialReplay { private final AtomicLong started = new AtomicLong(); private final AtomicLong finished = new AtomicLong(); - private Map> partitionVisits = new HashMap<>(); + private Map> partitionVisits = new HashMap<>(); public void begin(Visit visit) { @@ -74,15 +75,15 @@ public void begin(Visit visit) started.set(visit.lts); for (int i = 0; i < visit.operations.length; i++) { - Operations.Operation operation = visit.operations[i]; + 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; + PartitionOperation partitionOp = (PartitionOperation) operation; partitionVisits.computeIfAbsent(partitionOp.pd, pd_ -> new ArrayList<>()) - .add(new Model.LtsOperationPair(visit.lts, i)); + .add(operation); } } @@ -93,79 +94,91 @@ public void end(Visit visit) finished.set(visit.lts); } - public Iterable potentialVisits(long pd) + @Override + public Iterable potentialVisits(long pd) { - Iterable res = partitionVisits.get(pd); + Iterable res = partitionVisits.get(pd); if (res != null) return res; return Collections.emptyList(); } + } - public boolean isFinished(long lts) + public static interface ReplayingDataTracker extends DataTracker, Model.PartialReplay {} + + public static class NoOpDataTracker implements ReplayingDataTracker + { + + @Override + public void begin(Visit visit) { - return finished.get() >= lts; } @Override - public boolean allFinished() + public void end(Visit visit) { - return started.get() == finished.get(); } - } - - // TODO: optimize for sequential accesses + @Override + public Iterable potentialVisits(long pd) + { + return Collections.emptyList(); + } + } /** - * Data tracker able to track LTS out of order + * Data tracker able to track LTS out of order. + * + * Intended to be used either in a single-threaded environment, or in conjuction with a locking/concurrent tracker */ - class SimpleDataTracker implements DataTracker + // TODO: optimize for sequential accesses + class SimpleDataTracker implements ReplayingDataTracker { - private final Set started = new HashSet<>(); - private final Set finished = new HashSet<>(); - - private Map> partitionVisits = new HashMap<>(); + // WARNING: you can access partitions concurrently, but make sure to use locking tracker to guard the op list + private Map> partitionVisits = new ConcurrentHashMap<>(); public void begin(Visit visit) { - started.add(visit.lts); for (int i = 0; i < visit.operations.length; i++) { - Operations.Operation operation = visit.operations[i]; + 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; + PartitionOperation partitionOp = (PartitionOperation) operation; partitionVisits.computeIfAbsent(partitionOp.pd, pd_ -> new ArrayList<>()) - .add(new Model.LtsOperationPair(visit.lts, i)); + .add(operation); } } - public void end(Visit visit) - { - finished.add(visit.lts); - } - - public Iterable potentialVisits(long pd) + @Override + public void gc(long pd) { - Iterable res = partitionVisits.get(pd); - if (res != null) - return res; - - return Collections.emptyList(); + partitionVisits.remove(pd); } - public boolean isFinished(long lts) + public void end(Visit visit) { - return finished.contains(lts); } @Override - public boolean allFinished() + public Iterable potentialVisits(long pd) { - return started.size() == finished.size(); + List res = partitionVisits.get(pd); + if (res == null) + return Collections.emptyList(); + + // TODO: this won't hold for Accord or Paxos, so we will need to also have a separate wall clock + // tracker for operations. + // Operations are appended in begin() order, which under concurrent execution is not logical-timestamp + // order. The quiescent checker requires them grouped and applied in increasing lts (matching the DB's + // last-write-wins by USING TIMESTAMP lts), so return an lts-sorted copy. The caller holds the partition + // read lock, so no writer is appending to the underlying list while we copy it. + List sorted = new ArrayList<>(res); + sorted.sort(Comparator.comparingLong(Operation::lts)); + return sorted; } } } diff --git a/test/harry/main/org/apache/cassandra/harry/execution/InJvmDTestVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/InJvmDTestVisitExecutor.java index ee00dfaaa8e0..7acb75dbea09 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/InJvmDTestVisitExecutor.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/InJvmDTestVisitExecutor.java @@ -37,9 +37,12 @@ import org.apache.cassandra.harry.ColumnSpec; import org.apache.cassandra.harry.MagicConstants; import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; +import org.apache.cassandra.harry.gen.ValueGenerators; 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.Selection; import org.apache.cassandra.harry.op.Visit; import org.apache.cassandra.utils.AssertionUtils; @@ -61,18 +64,24 @@ public class InJvmDTestVisitExecutor extends CQLVisitExecutor protected final PageSizeSelector pageSizeSelector; protected final RetryPolicy retryPolicy; - protected InJvmDTestVisitExecutor(SchemaSpec schema, - DataTracker dataTracker, - Model model, - ICluster cluster, + protected final ValueGenerators valueGenerators; - NodeSelector nodeSelector, - PageSizeSelector pageSizeSelector, - RetryPolicy retryPolicy, - ConsistencyLevelSelector consistencyLevel, - WrapQueries wrapQueries) + public InJvmDTestVisitExecutor(SchemaSpec schema, + + ValueGenerators valueGenerators, + DataTracker dataTracker, + Model model, + ICluster cluster, + + NodeSelector nodeSelector, + PageSizeSelector pageSizeSelector, + RetryPolicy retryPolicy, + ConsistencyLevelSelector consistencyLevel, + WrapQueries wrapQueries) { - super(schema, dataTracker, model, new QueryBuildingVisitExecutor(schema, wrapQueries)); + super(schema, dataTracker, model, new QueryBuildingVisitExecutor(schema, wrapQueries, valueGenerators)); + + this.valueGenerators = valueGenerators; this.cluster = cluster; this.consistencyLevel = consistencyLevel; @@ -121,7 +130,7 @@ protected List executeWithResult(Visit visit, int node, int pageSi if (logger.isTraceEnabled()) logger.trace("{} returned {} results", statement, rows.length); - return rowsToResultSet(schema, (Operations.SelectStatement) visit.operations[0], rows); + return rowsToResultSet(schema, valueGenerators, (Operations.SelectStatement) visit.operations[0], rows); } protected static Object[][] iterToArr(Iterator iter) @@ -160,15 +169,20 @@ protected void executeWithoutResult(Visit visit, int node, CompiledStatement sta cluster.coordinator(node).execute(statement.cql(), consistencyLevel, statement.bindings()); } - public static List rowsToResultSet(SchemaSpec schema, Operations.SelectStatement select, Object[][] result) + public static List rowsToResultSet(SchemaSpec schema, + ValueGenerators generators, + Operations.SelectStatement select, Object[][] result) { List rs = new ArrayList<>(); for (Object[] res : result) - rs.add(rowToResultSet(schema, select, res)); + rs.add(rowToResultSet(schema, generators, select, res)); return rs; } - public static ResultSetRow rowToResultSet(SchemaSpec schema, Operations.SelectStatement select, Object[] result) + public static ResultSetRow rowToResultSet(SchemaSpec schema, + ValueGenerators generators, + Operations.SelectStatement select, + Object[] result) { long[] staticColumns = new long[schema.staticColumns.size()]; long[] regularColumns = new long[schema.regularColumns.size()]; @@ -179,16 +193,18 @@ public static ResultSetRow rowToResultSet(SchemaSpec schema, Operations.SelectSt long[] regularLts = LTS_UNKNOWN; long pd = UNKNOWN_DESCR; - Operations.Selection selection = Operations.Selection.fromBitSet(select.selection(), schema); + Selection selection = Selection.fromBitSet(select.selection(), schema); + Invariants.require(selection.selectsAllOf(schema.partitionKeys)); 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); + pd = generators.pkGen().deflate(partitionKey); } + ValueGenerators.PartitionValues valueGenerators = generators.forPd(pd); // 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)) @@ -214,7 +230,7 @@ public static ResultSetRow rowToResultSet(SchemaSpec schema, Operations.SelectSt if (clusteringKey == NIL_KEY) cd = UNSET_DESCR; else - cd = schema.valueGenerators.ckGen().deflate(clusteringKey); + cd = valueGenerators.ckGen().deflate(clusteringKey); } for (int i = 0; i < schema.regularColumns.size(); i++) @@ -226,7 +242,7 @@ public static ResultSetRow rowToResultSet(SchemaSpec schema, Operations.SelectSt if (v == null) regularColumns[i] = NIL_DESCR; else - regularColumns[i] = schema.valueGenerators.regularColumnGen(i).deflate(v); + regularColumns[i] = valueGenerators.regularColumnGen(i).deflate(v); } else { @@ -243,7 +259,7 @@ public static ResultSetRow rowToResultSet(SchemaSpec schema, Operations.SelectSt if (v == null) staticColumns[i] = NIL_DESCR; else - staticColumns[i] = schema.valueGenerators.staticColumnGen(i).deflate(v); + staticColumns[i] = valueGenerators.staticColumnGen(i).deflate(v); } else { @@ -371,12 +387,12 @@ public int select(long lts) } } - public InJvmDTestVisitExecutor build(SchemaSpec schema, Model.Replay replay, ICluster cluster) + public InJvmDTestVisitExecutor build(SchemaSpec schema, IndexedValueGenerators valueGenerators, 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, + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + Model model = new QuiescentChecker(valueGenerators, tracker); + return new InJvmDTestVisitExecutor(schema, valueGenerators, tracker, model, cluster, nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, wrapQueries); } @@ -389,12 +405,12 @@ public InJvmDTestVisitExecutor build(SchemaSpec schema, ICluster cluster, Fun /** * WARNING: highly experimental */ - public InJvmDTestVisitExecutor doubleWriting(SchemaSpec schema, Model.Replay replay, ICluster cluster, String secondTable) + public InJvmDTestVisitExecutor doubleWriting(SchemaSpec schema, IndexedValueGenerators valueGens, 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, + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + Model model = new QuiescentChecker(valueGens, tracker); + return new InJvmDTestVisitExecutor(schema, valueGens, tracker, model, cluster, nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, wrapQueries) { @Override @@ -408,7 +424,7 @@ protected List executeWithResult(Visit visit, int node, int pageSi { logger.debug("Second opinion: "); for (ResultSetRow resultSetRow : secondOpinion) - logger.debug(resultSetRow.toString(schema.valueGenerators)); + logger.debug(resultSetRow.toString(valueGens)); } return rows; } diff --git a/test/harry/main/org/apache/cassandra/harry/execution/LockingDataTracker.java b/test/harry/main/org/apache/cassandra/harry/execution/LockingDataTracker.java new file mode 100644 index 000000000000..ead4f314ba58 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/execution/LockingDataTracker.java @@ -0,0 +1,325 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.op.Visit; +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 implements DataTracker +{ + private final Map locked = new ConcurrentHashMap<>(); + + private final WaitQueue readersQueue = WaitQueue.newWaitQueue(); + private final WaitQueue writersQueue = WaitQueue.newWaitQueue(); + + // TODO: primitive concurrent lock! + private Set readingFrom = new ConcurrentSkipListSet<>(); + private Set writingTo = new ConcurrentSkipListSet<>(); + + private final DataTracker delegate; + + private final int readConcurrency; + private final int writeConcurrency; + + public LockingDataTracker(DataTracker delegate) + { + // By default, until we have a better/stronger model, we allow however many readers, but only 1 writer at a time + this(delegate, Integer.MAX_VALUE, 1); + } + + public LockingDataTracker(DataTracker delegate, int readConcurrency, int writeConcurrency) + { + this.delegate = delegate; + this.readConcurrency = readConcurrency; + this.writeConcurrency = writeConcurrency; + } + + @Override + public void begin(Visit visit) + { + while (true) + { + int lockedUpTo = -1; + // Grab all locks, or release all locks, to avoid deadlocking with other visitors + for (int i = 0; i < visit.visitedPartitions.length; i++) + { + Long pd = visit.visitedPartitions[i]; + ReadersWritersLock partitionLock = getLock(pd); + if (visit.validating()) + { + if (!partitionLock.tryLockForRead()) + { +// System.out.println("Could not lock for read"); + break; + } + assert !writingTo.contains(pd) : String.format("Writing to should not have contained %d", pd); + readingFrom.add(pd); + } + else + { + if (!partitionLock.tryLockForWrite()) + { +// System.out.println("Could not lock for write"); + break; + } +// assert !readingFrom.contains(partitionLock.descriptor) : String.format("Reading from should not have contained %d", partitionLock.descriptor); + writingTo.add(partitionLock.descriptor); + } + + lockedUpTo = i; + } + + if (lockedUpTo != visit.visitedPartitions.length - 1) + { + for (int i = 0; i < lockedUpTo; i++) + { + Long pd = visit.visitedPartitions[i]; + ReadersWritersLock partitionLock = getLock(pd); + if (visit.validating()) + partitionLock.unlockAfterRead(); + else + partitionLock.unlockAfterWrite(); + } + continue; + } + + break; + } + delegate.begin(visit); + } + + @Override + public void end(Visit visit) + { + for (Long pd : visit.visitedPartitions) + { + ReadersWritersLock partitionLock = getLock(pd); + if (visit.validating()) + { + readingFrom.remove(pd); + partitionLock.unlockAfterRead(); + } + else + { + writingTo.remove(pd); + partitionLock.unlockAfterWrite(); + } + } + delegate.end(visit); + } + + private ReadersWritersLock getLock(long pd) + { + return locked.computeIfAbsent(pd, (pd_) -> new ReadersWritersLock(readersQueue, writersQueue, pd, readConcurrency, writeConcurrency)); + } + + /** + * 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; + // TODO: we do not need to use queues here, just using a signal will suffice + final WaitQueue readersQueue; + final WaitQueue writersQueue; + private final int readConcurrency; + private final int writeConcurrency; + + public ReadersWritersLock(WaitQueue readersQueue, WaitQueue writersQueue, long descriptor, int readConcurrency, int writeConcurrency) + { + this.readersQueue = readersQueue; + this.writersQueue = writersQueue; + this.lock = 0L; + this.descriptor = descriptor; + Invariants.require(readConcurrency > 0); + this.readConcurrency = readConcurrency; + Invariants.require(writeConcurrency > 0); + this.writeConcurrency = writeConcurrency; + } + + @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 && getWriters(v) < writeConcurrency) + { + if (fieldUpdater.compareAndSet(this, v, incWriters(v))) + { + signal.cancel(); + return; + } + } + signal.awaitUninterruptibly(); + } + } + + public boolean tryLockForWrite() + { + long v = lock; + if (getReaders(v) == 0 && getWriters(v) < writeConcurrency && fieldUpdater.compareAndSet(this, v, incWriters(v))) + return true; + + return false; + } + + 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 && getReaders(v) < readConcurrency) + { + if (fieldUpdater.compareAndSet(this, v, incReaders(v))) + { + signal.cancel(); + return; + } + } + signal.awaitUninterruptibly(); + } + } + + public boolean tryLockForRead() + { + long v = lock; + if (getWriters(v) == 0 && getReaders(v) < readConcurrency && 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 String toString() + { + return "Locking" + super.toString(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/execution/QueryBuildingVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/execution/QueryBuildingVisitExecutor.java index a22e39314126..9ea463376bfe 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/QueryBuildingVisitExecutor.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/QueryBuildingVisitExecutor.java @@ -18,6 +18,7 @@ package org.apache.cassandra.harry.execution; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -33,6 +34,7 @@ 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.gen.ValueGenerators; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.op.Visit; @@ -42,11 +44,13 @@ public class QueryBuildingVisitExecutor extends VisitExecutor private static final Logger logger = LoggerFactory.getLogger(QueryBuildingVisitExecutor.class); protected final SchemaSpec schema; protected final WrapQueries wrapQueries; + protected final ValueGenerators valueGenerators; - public QueryBuildingVisitExecutor(SchemaSpec schema, WrapQueries wrapQueries) + public QueryBuildingVisitExecutor(SchemaSpec schema, WrapQueries wrapQueries, ValueGenerators valueGenerators) { this.schema = schema; this.wrapQueries = wrapQueries; + this.valueGenerators = valueGenerators; } public BuiltQuery compile(Visit visit) @@ -66,9 +70,11 @@ public BuiltQuery compile(Visit visit) { Object[] bindingsArray = new Object[bindings.size()]; bindings.toArray(bindingsArray); - BuiltQuery query = new BuiltQuery(selects, + BuiltQuery query = new BuiltQuery(selects, visit.validating, wrapQueries.wrap(visit, String.join("\n ", statements)), bindingsArray); + if (pks.size() == 1) + query.setPk(pks.get(0)); clear(); return query; } @@ -80,9 +86,9 @@ public BuiltQuery compile(Visit visit) public static class BuiltQuery extends CompiledStatement { protected final List selects; - public BuiltQuery(List selects, String cql, Object... bindings) + public BuiltQuery(List selects, boolean mutates, String cql, Object... bindings) { - super(cql, bindings); + super(mutates, cql, bindings); this.selects = selects; } } @@ -93,14 +99,13 @@ public BuiltQuery(List selects, String cql, Object.. private final List statements = new ArrayList<>(); private final List bindings = new ArrayList<>(); private final Set visitedPds = new HashSet<>(); + private final List pks = new ArrayList<>(); private List selects = null; protected void beginLts(long lts) { - statements.clear(); - bindings.clear(); - visitedPds.clear(); + clear(); selects = new ArrayList<>(); } @@ -120,9 +125,11 @@ private void clear() statements.clear(); bindings.clear(); visitedPds.clear(); + pks.clear(); selects = null; } + @SuppressWarnings("unchecked") protected void operation(Operations.Operation operation) { if (operation instanceof Operations.PartitionOperation) @@ -131,37 +138,47 @@ protected void operation(Operations.Operation operation) switch (operation.kind()) { case UPDATE: - statement = WriteHelper.inflateUpdate((Operations.WriteOp) operation, schema, operation.lts()); + { + Operations.WriteOp write = (Operations.WriteOp) operation; + statement = WriteHelper.inflateUpdate(write, schema, valueGenerators, operation.lts()); break; + } case INSERT: - statement = WriteHelper.inflateInsert((Operations.WriteOp) operation, schema, operation.lts()); + { + Operations.WriteOp write = (Operations.WriteOp) operation; + statement = WriteHelper.inflateInsert(write, schema, valueGenerators, operation.lts()); break; + } case DELETE_RANGE: - statement = DeleteHelper.inflateDelete((Operations.DeleteRange) operation, schema, operation.lts()); + // TODO: unroll + statement = DeleteHelper.inflateDelete((Operations.DeleteRange) operation, schema, valueGenerators, operation.lts()); break; case DELETE_PARTITION: - statement = DeleteHelper.inflateDelete((Operations.DeletePartition) operation, schema, operation.lts()); + statement = DeleteHelper.inflateDelete((Operations.DeletePartition) operation, schema, valueGenerators, operation.lts()); break; case DELETE_ROW: - statement = DeleteHelper.inflateDelete((Operations.DeleteRow) operation, schema, operation.lts()); + statement = DeleteHelper.inflateDelete((Operations.DeleteRow) operation, schema, valueGenerators, operation.lts()); break; case DELETE_COLUMNS: - statement = DeleteHelper.inflateDelete((Operations.DeleteColumns) operation, schema, operation.lts()); + statement = DeleteHelper.inflateDelete((Operations.DeleteColumns) operation, schema, valueGenerators, operation.lts()); break; case SELECT_PARTITION: - statement = SelectHelper.select((Operations.SelectPartition) operation, schema); + { + Operations.SelectPartition select = (Operations.SelectPartition) operation; + statement = SelectHelper.select(select, schema, valueGenerators); selects.add((Operations.SelectStatement) operation); break; + } case SELECT_ROW: - statement = SelectHelper.select((Operations.SelectRow) operation, schema); + statement = SelectHelper.select((Operations.SelectRow) operation, schema, valueGenerators); selects.add((Operations.SelectStatement) operation); break; case SELECT_RANGE: - statement = SelectHelper.select((Operations.SelectRange) operation, schema); + statement = SelectHelper.select((Operations.SelectRange) operation, schema, valueGenerators); selects.add((Operations.SelectStatement) operation); break; case SELECT_CUSTOM: - statement = SelectHelper.select((Operations.SelectCustom) operation, schema); + statement = SelectHelper.select((Operations.SelectCustom) operation, schema, valueGenerators); selects.add((Operations.SelectStatement) operation); break; @@ -172,6 +189,7 @@ protected void operation(Operations.Operation operation) throw new IllegalArgumentException(); } statements.add(statement.cql()); + pks.add(statement.pk()); Collections.addAll(bindings, statement.bindings()); } @@ -193,6 +211,8 @@ public interface WrapQueries WrapQueries TRANSACTION = (visit, compiled) -> String.format(wrapInTxnFormat, compiled); + WrapQueries EMPTY = (visit, compiled) -> 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 index bc06f479aeca..dd60a070a8c3 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/ResultSetRow.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/ResultSetRow.java @@ -118,10 +118,10 @@ public String toString(ValueGenerators valueGenerators) { return "resultSetRow(" + valueGenerators.pkGen().toString(pd) - + ", " + descrToIdx(valueGenerators.ckGen(), cd) + - (sds == null ? "" : ", statics(" + toString(sds, valueGenerators::staticColumnGen) + ")") + + + ", " + descrToIdx(valueGenerators.forPd(pd).ckGen(), cd) + + (sds == null ? "" : ", statics(" + toString(sds, valueGenerators.forPd(pd)::staticColumnGen) + ")") + (slts == null ? "" : ", slts(" + StringUtils.toString(slts) + ")") + - ", values(" + toString(vds, valueGenerators::regularColumnGen) + ")" + + ", values(" + toString(vds, valueGenerators.forPd(pd)::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 index 9905e86b6ff0..cbc7cb6ef99d 100644 --- a/test/harry/main/org/apache/cassandra/harry/execution/RingAwareInJvmDTestVisitExecutor.java +++ b/test/harry/main/org/apache/cassandra/harry/execution/RingAwareInJvmDTestVisitExecutor.java @@ -33,6 +33,7 @@ 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.gen.ValueGenerators; import org.apache.cassandra.harry.model.Model; import org.apache.cassandra.harry.model.QuiescentChecker; import org.apache.cassandra.harry.model.TokenPlacementModel; @@ -52,6 +53,7 @@ public class RingAwareInJvmDTestVisitExecutor extends InJvmDTestVisitExecutor private final TokenPlacementModel.ReplicationFactor rf; private RingAwareInJvmDTestVisitExecutor(SchemaSpec schema, + ValueGenerators valueGenerators, DataTracker dataTracker, Model model, ICluster cluster, @@ -62,7 +64,7 @@ private RingAwareInJvmDTestVisitExecutor(SchemaSpec schema, TokenPlacementModel.ReplicationFactor rf, QueryBuildingVisitExecutor.WrapQueries wrapQueries) { - super(schema, dataTracker, model, cluster, nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, QueryBuildingVisitExecutor.WrapQueries.UNLOGGED_BATCH); + super(schema, valueGenerators, dataTracker, model, cluster, nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, wrapQueries); this.rf = rf; } @@ -86,7 +88,7 @@ public List getReplicasFor(long pd) protected long token(long pd) { - return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(schema.valueGenerators.pkGen().inflate(pd)))); + return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(valueGenerators.pkGen().inflate(pd)))); } @Override @@ -120,9 +122,10 @@ protected void executeWithoutResult(Visit visit, CompiledStatement statement) { try { - Invariants.require(visit.visitedPartitions.size() == 1, + Invariants.require(visit.visitedPartitions.length == 1, "Ring aware executor can only read and write one partition at a time"); - for (TokenPlacementModel.Replica replica : getReplicasFor(visit.visitedPartitions.iterator().next().longValue())) + + for (TokenPlacementModel.Replica replica : getReplicasFor(visit.visitedPartitions[0])) { IInstance instance = cluster .stream() @@ -198,17 +201,17 @@ protected void setDefaults(SchemaSpec schema, ICluster cluster) } } - public RingAwareInJvmDTestVisitExecutor build(SchemaSpec schema, Model.Replay replay, ICluster cluster) + public RingAwareInJvmDTestVisitExecutor build(SchemaSpec schema, ValueGenerators valueGenerators, ICluster cluster) { - DataTracker tracker = new DataTracker.SequentialDataTracker(); - Model model = new QuiescentChecker(schema.valueGenerators, tracker, replay); - return build(schema, tracker, model, cluster); + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + Model model = new QuiescentChecker(valueGenerators, tracker); + return build(schema, valueGenerators, tracker, model, cluster); } - public RingAwareInJvmDTestVisitExecutor build(SchemaSpec schema, DataTracker tracker, Model model, ICluster cluster) + public RingAwareInJvmDTestVisitExecutor build(SchemaSpec schema, ValueGenerators valueGenerators, DataTracker tracker, Model model, ICluster cluster) { setDefaults(schema, cluster); - return new RingAwareInJvmDTestVisitExecutor(schema, tracker, model, cluster, + return new RingAwareInJvmDTestVisitExecutor(schema, valueGenerators, tracker, model, cluster, nodeSelector, pageSizeSelector, retryPolicy, consistencyLevel, rf, wrapQueries); } } diff --git a/test/harry/main/org/apache/cassandra/harry/gen/BijectionCache.java b/test/harry/main/org/apache/cassandra/harry/gen/BijectionCache.java index d1f9e0db7b52..b3c115f35eb3 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/BijectionCache.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/BijectionCache.java @@ -76,7 +76,7 @@ public Set values() } @Override - public int population() + public long population() { throw new UnsupportedOperationException(); } 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 b41487303b9a..eed096305467 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java @@ -53,7 +53,7 @@ public interface Bijection // 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() + default long population() { return byteSize() * Byte.SIZE; } diff --git a/test/harry/main/org/apache/cassandra/harry/gen/BytesBijection.java b/test/harry/main/org/apache/cassandra/harry/gen/BytesBijection.java new file mode 100644 index 000000000000..0ebb3e9aff80 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/BytesBijection.java @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.nio.ByteBuffer; +import java.util.Arrays; +import java.util.TreeMap; + +import org.apache.cassandra.harry.gen.rng.RngUtils; + +/** + * A nibble-based bijection for ByteBuffers, similar to {@link StringBijection}. + *

+ * Each byte of the descriptor is used as an index into a nibble table of 256 pre-generated + * byte[] fragments. The 8 nibbles form a unique prefix that guarantees bijectivity; + * additional random bytes are appended for variable-length output. + */ +public class BytesBijection implements Bijections.Bijection +{ + public static final int NIBBLES_SIZE = 256; + private final byte[][] nibbles; + private final int nibbleSize; + private final int maxRandomBytes; + + public BytesBijection() + { + this(8, 10); + } + + public BytesBijection(int nibbleSize, int maxRandomBytes) + { + this(generateNibbles(nibbleSize), nibbleSize, maxRandomBytes); + } + + public BytesBijection(byte[][] nibbles, int nibbleSize, int maxRandomBytes) + { + assert nibbles.length == NIBBLES_SIZE; + this.nibbles = nibbles; + this.nibbleSize = nibbleSize; + this.maxRandomBytes = maxRandomBytes; + + for (int i = 0; i < nibbles.length; i++) + assert nibbles[i].length == nibbleSize; + } + + public ByteBuffer inflate(long descriptor) + { + // Prefix: 8 nibbles selected by each byte of the descriptor + int prefixLen = Long.BYTES * nibbleSize; + + // Determine suffix length + int suffixLen = suffixLength(descriptor); + + // Determine extra length (subclasses may override) + int extraLen = extraLength(descriptor); + + byte[] result = new byte[prefixLen + suffixLen + extraLen]; + + // Write prefix nibbles + for (int i = 0; i < Long.BYTES; i++) + { + int idx = getByte(descriptor, i); + System.arraycopy(nibbles[idx], 0, result, i * nibbleSize, nibbleSize); + } + + // Append suffix bytes + appendSuffix(result, prefixLen, suffixLen, descriptor); + + // Append extra bytes (subclasses may override) + appendExtra(result, prefixLen + suffixLen, extraLen, descriptor); + + return ByteBuffer.wrap(result); + } + + protected int suffixLength(long descriptor) + { + long rnd = RngUtils.next(descriptor); + return RngUtils.asInt(rnd, 0, maxRandomBytes); + } + + protected int extraLength(long descriptor) + { + return 0; + } + + protected void appendSuffix(byte[] result, int offset, int length, long descriptor) + { + long rnd = RngUtils.next(descriptor); + // skip past the rnd used for length + rnd = RngUtils.next(rnd); + + int pos = offset; + int remaining = length; + while (remaining > 0) + { + rnd = RngUtils.next(rnd); + for (int i = 0; i < remaining && i < Long.BYTES; i++) + { + result[pos++] = (byte) ((rnd >> (i * 8)) & 0xff); + remaining--; + } + } + } + + protected void appendExtra(byte[] result, int offset, int length, long descriptor) + { + } + + public long deflate(ByteBuffer value) + { + long res = 0; + for (int i = 0; i < Long.BYTES; i++) + { + int idx = findNibble(value, value.position() + i * nibbleSize); + long v = idx; + if (i == 0) + v ^= 0x80; + res |= v << (Long.BYTES - i - 1) * Byte.SIZE; + } + return res; + } + + private int findNibble(ByteBuffer value, int offset) + { + for (int n = 0; n < NIBBLES_SIZE; n++) + { + boolean match = true; + for (int j = 0; j < nibbleSize; j++) + { + if (value.get(offset + j) != nibbles[n][j]) + { + match = false; + break; + } + } + if (match) + return n; + } + throw new IllegalArgumentException("No matching nibble found at offset " + offset); + } + + public static int getByte(long l, int idx) + { + int b = (int) ((l >> (Long.BYTES - idx - 1) * Byte.SIZE) & 0xff); + if (idx == 0) + b ^= 0x80; + return b; + } + + public int compare(long l, long r) + { + for (int i = 0; i < Long.BYTES; i++) + { + int cmp = Integer.compare(getByte(l, i), getByte(r, i)); + if (cmp != 0) + return cmp; + } + return 0; + } + + public int byteSize() + { + return Long.BYTES; + } + + @Override + public String toString() + { + return "bytes(" + + "nibbleSize=" + nibbleSize + + ", maxRandomBytes=" + maxRandomBytes + + ')'; + } + + /** + * Generate 256 unique byte[] nibbles of the given size, sorted lexicographically. + * Uses a deterministic RNG so nibbles are stable across runs. + */ + public static byte[][] generateNibbles(int nibbleSize) + { + TreeMap sorted = new TreeMap<>(); + long seed = 0xdeadbeefcafeL; + + while (sorted.size() < NIBBLES_SIZE) + { + seed = RngUtils.next(seed); + byte[] nibble = new byte[nibbleSize]; + long s = seed; + for (int j = 0; j < nibbleSize; j++) + { + nibble[j] = (byte) (s & 0xff); + s = RngUtils.next(s); + } + sorted.put(new ByteArrayWrapper(nibble), nibble); + } + + byte[][] nibbles = new byte[NIBBLES_SIZE][]; + int i = 0; + for (byte[] nibble : sorted.values()) + nibbles[i++] = nibble; + + return nibbles; + } + + /** + * Wrapper for byte[] that provides equals/hashCode/compareTo for use in sorted collections. + */ + private static class ByteArrayWrapper implements Comparable + { + private final byte[] data; + + ByteArrayWrapper(byte[] data) + { + this.data = data; + } + + @Override + public int compareTo(ByteArrayWrapper other) + { + int len = Math.min(data.length, other.data.length); + for (int i = 0; i < len; i++) + { + int cmp = Integer.compare(data[i] & 0xff, other.data[i] & 0xff); + if (cmp != 0) + return cmp; + } + return Integer.compare(data.length, other.data.length); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof ByteArrayWrapper)) return false; + return Arrays.equals(data, ((ByteArrayWrapper) o).data); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(data); + } + } +} 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 c8e166956be4..9d8ea0d2af9e 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/Generators.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/Generators.java @@ -25,14 +25,18 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.NavigableMap; import java.util.Set; +import java.util.TreeMap; import java.util.UUID; import java.util.function.Supplier; import accord.utils.Invariants; - +import org.apache.cassandra.harry.stress.distribution.Distribution; import org.apache.cassandra.harry.util.BitSet; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.utils.TimeUUID; @@ -229,6 +233,19 @@ public static TrackingGenerator tracking(Generator delegate) return new TrackingGenerator<>(delegate); } + public static Generator adaptLongToInt(Generator orig) + { + return new Generator() + { + final Generator longGen = orig; + @Override + public Integer generate(EntropySource rng) + { + return Math.toIntExact(longGen.generate(rng)); + } + }; + } + public static class TrackingGenerator implements Generator { private final Set generated; @@ -325,6 +342,60 @@ public String generate(EntropySource rng) } } + public static final class VariableSizeStringGenerator implements Generator + { + final char[] chars; + final Distribution sizeDistribution; + + public VariableSizeStringGenerator(char[] chars, Distribution sizeDistribution) + { + this.chars = chars; + this.sizeDistribution = sizeDistribution; + } + + public VariableSizeStringGenerator(int minChar, int maxChar, Distribution sizeDistribution) + { + int range = maxChar - minChar + 1; + this.chars = new char[range]; + for (int i = 0; i < range; i++) + chars[i] = (char) (minChar + i); + this.sizeDistribution = sizeDistribution; + } + + @Override + public String generate(EntropySource rng) + { + int length = Math.toIntExact(sizeDistribution.next(rng.next())); + char[] buf = new char[length]; + for (int i = 0; i < length; i++) + buf[i] = chars[rng.nextInt(chars.length)]; + return new String(buf); + } + } + + public static final class VariableSizeByteBufferGenerator implements Generator + { + final Distribution sizeDistribution; + + public VariableSizeByteBufferGenerator(Distribution sizeDistribution) + { + this.sizeDistribution = sizeDistribution; + } + + @Override + public ByteBuffer generate(EntropySource rng) + { + int size = Math.toIntExact(sizeDistribution.next(rng.next())); + 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 final class LongGenerator implements Generator { @Override @@ -391,7 +462,7 @@ public static Generator pick(List ts) { if (ts.isEmpty()) throw new IllegalStateException("Can't pick from an empty list"); - return (rng) -> ts.get(rng.nextInt(0, ts.size())); + return (rng) -> ts.get(rng.nextInt(ts.size())); } public static Generator pick(T... ts) @@ -466,4 +537,38 @@ public static Generator constant(Supplier constant) { return (random) -> constant.get(); } + + + public static Map normalize(Map weights) + { + Map normalized = new HashMap<>(); + int sum = 0; + for (Integer value : weights.values()) + sum += value; + + for (T kind : weights.keySet()) + { + double dbl = (sum * ((double) weights.get(kind)) / sum); + normalized.put(kind, (int) Math.round(dbl)); + } + + return normalized; + } + public static Generator weighted(Map weights) + { + NavigableMap weightMap = weights instanceof NavigableMap ? (TreeMap) weights : new TreeMap(); + int sum = 0; + for (Map.Entry entry : weights.entrySet()) + { + sum += entry.getValue(); + weightMap.put(sum, entry.getKey()); + } + + int max = sum; + return (rng) -> { + int weight = rng.nextInt(max); + return weightMap.ceilingEntry(weight).getValue(); + }; + } + } diff --git a/test/harry/main/org/apache/cassandra/harry/gen/IndexGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/IndexGenerators.java index 9aeadf357392..998a144c398d 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/IndexGenerators.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/IndexGenerators.java @@ -1,4 +1,4 @@ -/* +package org.apache.cassandra.harry.gen;/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -16,115 +16,23 @@ * limitations under the License. */ -package org.apache.cassandra.harry.gen; - -import org.apache.cassandra.harry.MagicConstants; +import java.util.List; 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() + public static Generator uniform(Bijections.Bijection gen) { - if (pkIdxGen instanceof Generators.TrackingGenerator) - return this; - - return new IndexGenerators(valueGenerators, - Generators.tracking(pkIdxGen), - ckIdxGen, - regularIdxGens, - staticIdxGens); + return (rng) -> rng.nextInt((int) gen.population()); } - public IndexGenerators withChanceOfUnset(double chanceOfUnset) + public static Generator[] uniform(List> gens) { - 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++) + Generator[] res = new Generator[gens.size()]; + for (int i = 0; i < res.length; i++) { int column = i; - staticIdxGens[i] = (rng) -> rng.nextDouble() <= chanceOfUnset ? MagicConstants.UNSET_IDX : rng.nextInt(valueGenerators.staticPopulation(column)); + res[i] = (rng) -> rng.nextInt((int) gens.get(column).population()); } - - return new IndexGenerators(valueGenerators, - // TODO: distribution for visits - Generators.int32(0, valueGenerators.pkPopulation()), - Generators.int32(0, valueGenerators.ckPopulation()), - regularIdxGens, - staticIdxGens); + return res; } } diff --git a/test/harry/main/org/apache/cassandra/harry/gen/InvertibleGenerator.java b/test/harry/main/org/apache/cassandra/harry/gen/InvertibleGenerator.java index 295ffdff8013..01518600601a 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/InvertibleGenerator.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/InvertibleGenerator.java @@ -20,14 +20,15 @@ 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.concurrent.ConcurrentSkipListMap; +import java.util.function.LongFunction; import java.util.stream.Collectors; -import org.agrona.collections.IntHashSet; +import org.agrona.collections.Long2ObjectHashMap; import accord.utils.Invariants; @@ -51,25 +52,41 @@ * * 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. + * TODO (expected): use smaller-entropy values for descriptors, as we only need to generate _distinct_ values. In principe, we can just use counters. */ public class InvertibleGenerator implements HistoryBuilder.IndexedBijection { public static long MAX_ENTROPY = 1L << 63; - private static final boolean PARANOIA = true; + private static final boolean PARANOIA = false; + private static final int INFLATE_CACHE_SIZE = 10_000; + // Number of top levels of the deflate binary-search tree to pin permanently (up to ~2^depth entries), so + // the hottest comparisons are never evicted by the FIFO backstop. Tunable. + private static final int BINARY_SEARCH_CACHE_DEPTH = 10; - // TODO (required): switch to use a primitive array; will need to implement a sort comparator for primitive types - private final List allocatedDescriptors; + private static final ConcurrentSkipListMap REUSE_DESCRIPTOR_ARRAYS = new ConcurrentSkipListMap<>(); + + private long[] descriptors; + private final long descriptorCount; - private final Generator gen; private final Comparator comparator; + private final Cache inflateCache; - // To avoid erased types - public static InvertibleGenerator fromType(EntropySource rng, int population, ColumnSpec spec) + public static HistoryBuilder.IndexedBijection fromType(EntropySource rng, int population, ColumnSpec spec) { + if (spec.gen instanceof HistoryBuilder.IndexedBijection) + return (HistoryBuilder.IndexedBijection) spec.gen; return new InvertibleGenerator<>(rng, spec.type.typeEntropy(), population, spec.gen, spec.type.comparator()); } + @Override + public void discard() + { + if (descriptors.length > 100) + REUSE_DESCRIPTOR_ARRAYS.putIfAbsent(descriptors.length, descriptors); + descriptors = null; + } + public InvertibleGenerator(EntropySource rng, /* unsigned */ long typeEntropy, int population, @@ -87,62 +104,80 @@ public InvertibleGenerator(EntropySource rng, 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) + LongFunction compute = descriptor -> SeedableEntropySource.computeWithSeed(descriptor, gen::generate); + + Map.Entry e = REUSE_DESCRIPTOR_ARRAYS.ceilingEntry(population); + if (population > 100 && e != null && e.getValue().length < population * 2 && REUSE_DESCRIPTOR_ARRAYS.remove(e.getKey(), e.getValue())) + this.descriptors = e.getValue(); + else + this.descriptors = new long[population]; + + // Generate a population of values into a throwaway boxed list, sort it by value, and copy the distinct + // values (now adjacent) into the primitive descriptor array. The list and the value cache exist only + // for the duration of construction; the long-lived state is descriptors[]/descriptorCount. + List candidates = new ArrayList<>(population); + for (int i = 0 ; i < population ; ++i) { long candidate = rng.next(); // Should never allocate these, however improbable that is - if (MagicConstants.MAGIC_DESCRIPTOR_VALS.contains(candidate)) + if (MagicConstants.isMagicDescriptor(candidate)) continue; - Object inflated = inflate(candidate); - int hash = ArrayUtils.hashCode(inflated); - Invariants.require(hash != System.identityHashCode(inflated), "hashCode was not overridden for type %s", inflated.getClass()); + candidates.add(candidate); + } - if (hashes.add(hash)) - allocatedDescriptors.add(candidate); + Cache tmpCache = new UnboundedCache<>(population, compute); + candidates.sort((d1, d2) -> comparator.compare(tmpCache.get(d1), tmpCache.get(d2))); + int count = 0; + for (int i = 0; i < candidates.size(); i++) + { + long candidate = candidates.get(i); + if (count > 0 && comparator.compare(tmpCache.get(descriptors[count - 1]), tmpCache.get(candidate)) == 0) + continue; + descriptors[count++] = candidate; } - hashes.clear(); + descriptorCount = count; - allocatedDescriptors.sort(this::compare); + // Inflate cache: pin the hottest binary-search entries (the top few levels, visited by every deflate) + // so they are never evicted, and fall back to a bounded FIFO for everything else. + this.inflateCache = new HierarchicalCache<>(new BinarySearchPathCache<>(descriptors, count, BINARY_SEARCH_CACHE_DEPTH, compute), + new FifoCache<>(INFLATE_CACHE_SIZE, compute)); // 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 prev = inflate(descriptors[0]); + for (int i = 1; i < descriptorCount; i++) { - T current = inflate(allocatedDescriptors.get(i)); + T current = inflate(descriptors[i]); Invariants.require( comparator.compare(current, prev) > 0, - () -> String.format("%s should be strictly after %s", prev, current)); + "%s should be strictly after %s", prev, current); + prev = current; } } } @Override - public int idxFor(long descriptor) + public long idxFor(long descriptor) { - return Collections.binarySearch(allocatedDescriptors, descriptor, this.descriptorsComparator()); + // descriptors[] is value-sorted, so the index of a descriptor is where its value sorts. + return binarySearch(inflate(descriptor)); } @Override - public long descriptorAt(int idx) + public long descriptorAt(long idx) { - return allocatedDescriptors.get(idx); + return descriptors[(int) idx]; } @Override public T inflate(long descriptor) { - Invariants.require(!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); + Invariants.require(!MagicConstants.isMagicDescriptor(descriptor), + "Should not be able to inflate %d, as it's magic value", descriptor); + return inflateCache.get(descriptor); } @Override @@ -153,28 +188,26 @@ public long deflate(T value) { if (idx < 0) { - for (long descriptor : allocatedDescriptors) + for (int i = 0; i < descriptorCount; i++) { - Object expected = inflate(descriptor); + Object expected = inflate(descriptors[i]); if (value.getClass().isArray()) { Object[] valueArr = (Object[]) value; Object[] expectedArr = (Object[]) expected; Invariants.require(comparator.compare((T) expected, value) != 0, - "%s was found: %s", Arrays.toString(expectedArr), Arrays.toString(valueArr)); - + "%s was found: %s", Arrays.toString(expectedArr), Arrays.toString(valueArr)); } else { Invariants.require(comparator.compare((T) expected, value) != 0, - "%s was found: %s", expected, value); + "%s was found: %s", expected, value); } - } } else { - long res = allocatedDescriptors.get(idx); + long res = descriptors[idx]; Object expected = inflate(res); if (value.getClass().isArray()) { @@ -182,13 +215,12 @@ public long deflate(T value) Object[] expectedArr = (Object[]) expected; Invariants.require(comparator.compare((T) expected, value) == 0, - "%s != %s", Arrays.toString(expectedArr), Arrays.toString(valueArr)); - + "%s != %s", Arrays.toString(expectedArr), Arrays.toString(valueArr)); } else { Invariants.require(comparator.compare((T) expected, value) == 0, - "%s != %s", expected, value); + "%s != %s", expected, value); } return res; @@ -200,12 +232,12 @@ public long deflate(T value) 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))); + nearby.add(inflate(descriptors[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); + return descriptors[idx]; } @@ -217,11 +249,11 @@ public int byteSize() private int binarySearch(T key) { - int low = 0, mid = allocatedDescriptors.size(), high = mid - 1, result = -1; + int low = 0, mid = (int) descriptorCount, high = mid - 1, result = -1; while (low <= high) { mid = (low + high) >>> 1; - result = comparator.compare(key, inflate(allocatedDescriptors.get(mid))); + result = comparator.compare(key, inflate(descriptors[mid])); if (result > 0) low = mid + 1; else if (result == 0) @@ -232,7 +264,6 @@ else if (result == 0) return -mid - (result < 0 ? 1 : 2); } - @Override public int compare(long d1, long d2) { @@ -247,17 +278,179 @@ public int compare(long d1, long d2) * Returns a number of allocated descriptors */ @Override - public int population() + public long population() { - return allocatedDescriptors.size(); + return descriptorCount; } 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); + for (int i = 0; i < descriptorCount; i++) + descriptorToIdx.put(descriptors[i], i); return Comparator.comparingInt(descriptorToIdx::get); } + + /** + * A value cache keyed by descriptor. + */ + public interface Cache + { + /** Returns the cached value for {@code descriptor}, or {@code null} if it is not cached; never computes. */ + T lookup(long descriptor); + + /** Returns the value for {@code descriptor}, computing it (and possibly caching it) on a miss. */ + T get(long descriptor); + } + + /** + * Fixed-size FIFO cache: a ring buffer tracks insertion order for eviction while a map provides O(1) + * lookup by descriptor. Misses are filled with the supplied compute function. + */ + public static final class FifoCache implements Cache + { + private final int capacity; + private final LongFunction compute; + private final long[] ring; + private final Long2ObjectHashMap map; + private int pos = 0; + private int count = 0; + + public FifoCache(int capacity, LongFunction compute) + { + this.capacity = capacity; + this.compute = compute; + this.ring = new long[capacity]; + this.map = new Long2ObjectHashMap<>(capacity, 0.75f); + } + + @Override + public T lookup(long descriptor) + { + return map.get(descriptor); + } + + @Override + public T get(long descriptor) + { + T cached = map.get(descriptor); + if (cached != null) + return cached; + + T value = compute.apply(descriptor); + if (count == capacity) + map.remove(ring[pos]); // evict the oldest entry to make room + else + count++; + + ring[pos] = descriptor; + map.put(descriptor, value); + pos = (pos + 1) % capacity; + return value; + } + } + + /** + * Unbounded cache that retains every entry it computes; intended for short-lived, build-time use where the + * whole population is touched and recomputation must be avoided. Not suitable as a long-lived cache. + */ + public static final class UnboundedCache implements Cache + { + private final LongFunction compute; + private final Long2ObjectHashMap map; + + public UnboundedCache(int initialCapacity, LongFunction compute) + { + this.compute = compute; + this.map = new Long2ObjectHashMap<>(Math.max(1, initialCapacity), 0.75f); + } + + @Override + public T lookup(long descriptor) + { + return map.get(descriptor); + } + + @Override + public T get(long descriptor) + { + T cached = map.get(descriptor); + if (cached != null) + return cached; + + T value = compute.apply(descriptor); + map.put(descriptor, value); + return value; + } + } + + /** + * Caches exactly the entries that a binary search over a sorted descriptor array would touch in its first {@code depth} steps. + */ + public static final class BinarySearchPathCache implements Cache + { + private final LongFunction compute; + private final Long2ObjectHashMap map; + + public BinarySearchPathCache(long[] sortedDescriptors, int size, int depth, LongFunction compute) + { + this.compute = compute; + this.map = new Long2ObjectHashMap<>(Math.max(1, 1 << Math.min(depth, 16)), 0.75f); + cachePath(sortedDescriptors, 0, size - 1, depth); + } + + private void cachePath(long[] sortedDescriptors, int lo, int hi, int depth) + { + if (depth <= 0 || lo > hi) + return; + int mid = (lo + hi) >>> 1; + long descriptor = sortedDescriptors[mid]; + map.put(descriptor, compute.apply(descriptor)); + cachePath(sortedDescriptors, lo, mid - 1, depth - 1); + cachePath(sortedDescriptors, mid + 1, hi, depth - 1); + } + + @Override + public T lookup(long descriptor) + { + return map.get(descriptor); + } + + @Override + public T get(long descriptor) + { + T cached = map.get(descriptor); + return cached != null ? cached : compute.apply(descriptor); + } + } + + /** + * Composes two caches into a hierarchy: lookups try {@code primary} first and fall back to {@code secondry} + * on a miss. + */ + public static final class HierarchicalCache implements Cache + { + private final Cache primary; + private final Cache secondary; + + public HierarchicalCache(Cache primary, Cache secondary) + { + this.primary = primary; + this.secondary = secondary; + } + + @Override + public T lookup(long descriptor) + { + T value = primary.lookup(descriptor); + return value != null ? value : secondary.lookup(descriptor); + } + + @Override + public T get(long descriptor) + { + T value = primary.lookup(descriptor); + return value != null ? value : secondary.get(descriptor); + } + } } diff --git a/test/harry/main/org/apache/cassandra/harry/gen/OperationsGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/OperationsGenerators.java index 33b831bfb6f5..f509c197a86a 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/OperationsGenerators.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/OperationsGenerators.java @@ -19,7 +19,8 @@ package org.apache.cassandra.harry.gen; import org.apache.cassandra.harry.SchemaSpec; -import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; +import org.apache.cassandra.harry.op.Kind; import org.apache.cassandra.harry.op.Operations; public class OperationsGenerators @@ -38,16 +39,14 @@ public Long generate(EntropySource rng) }; } - // TODO: distributions - public static Generator sequentialPd(SchemaSpec schema) + // TODO: can we use this, it is not functionally pure + public static Generator sequentialPd(IndexedValueGenerators valueGenerators) { - // TODO: switch away from Indexed generators here - HistoryBuilder.IndexedValueGenerators valueGenerators = (HistoryBuilder.IndexedValueGenerators) schema.valueGenerators; - int population = valueGenerators.pkPopulation(); + long population = valueGenerators.pkGen.population(); return new Generator<>() { - int counter = 0; + long counter = 0; @Override public Long generate(EntropySource rng) @@ -57,53 +56,43 @@ public Long generate(EntropySource rng) }; } - 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: useful? +// public static Generator sequentialCd(IndexedValueGenerators valueGenerators) +// { +// int population = valueGenerators.ckPopulation(); +// +// return new Generator<>() +// { +// int counter = 0; +// +// @Override +// public Long generate(EntropySource rng) +// { +// return valueGenerators.ckGen().descriptorAt(counter++ % population); +// } +// }; +// } // TODO: chance of unset public static Generator writeOp(SchemaSpec schema, - Generator pdGen, - Generator cdGen) + IndexedValueGenerators valueGenerators) { - // 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 pd = valueGenerators.pkIdxGen().generate(rng); + long cd = valueGenerators.forPd(pd).ckIdxGen().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 idx = rng.nextInt(valueGenerators.forPd(pd).regularPopulation(i)); + vds[i] = valueGenerators.forPd(pd).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); + long idx = rng.nextInt(valueGenerators.forPd(pd).staticPopulation(i)); + sds[i] = valueGenerators.forPd(pd).staticColumnGen(i).descriptorAt(idx); } - return lts -> new Operations.WriteOp(lts, pd, cd, vds, sds, Operations.Kind.INSERT); + return lts -> new Operations.WriteOp(lts, pd, cd, vds, sds, Kind.INSERT); }; } diff --git a/test/harry/main/org/apache/cassandra/harry/gen/SchemaGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/SchemaGenerators.java index 4feff7cf268c..b457d7b7f03d 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/SchemaGenerators.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/SchemaGenerators.java @@ -46,9 +46,7 @@ public static Generator schemaSpecGen(String ks, String prefix, int public SchemaSpec generate(EntropySource rng) { int idx = counter++; - return new SchemaSpec(rng.next(), - expectedValues, - ks, + return new SchemaSpec(ks, prefix + idx, pkGen.generate(rng), ckGen.generate(rng), @@ -97,17 +95,25 @@ public ColumnSpec generate(EntropySource rng) }; } - public static Generator trivialSchema(String ks, String table, int population) + public static Generator trivialSchema(String ks, String table) { - return trivialSchema(ks, () -> table, population, SchemaSpec.optionsBuilder().build()); + return trivialSchema(ks, table, SchemaSpec.optionsBuilder().build()); + } + + public static Generator trivialSchema(String ks, String table, SchemaSpec.Options options) + { + return trivialSchema(ks, () -> table, options); + } + + public static Generator trivialSchema(String ks, Supplier table, SchemaSpec.Options options) + { + return trivialSchema(ks, table, 1000, options); } public static Generator trivialSchema(String ks, Supplier table, int population, SchemaSpec.Options options) { return (rng) -> { - return new SchemaSpec(rng.next(), - population, - ks, table.get(), + return new SchemaSpec(ks, table.get(), 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)), diff --git a/test/harry/main/org/apache/cassandra/harry/gen/SharedValueGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/SharedValueGenerators.java new file mode 100644 index 000000000000..67b6760c75b4 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/SharedValueGenerators.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.gen; + +import java.util.Comparator; +import java.util.List; + +import org.apache.cassandra.harry.gen.Bijections.Bijection; + +public class SharedValueGenerators extends ValueGenerators +{ + protected final PartitionValues partitionValues; + + public SharedValueGenerators(Bijection pkGen, + + Bijection ckGen, + Accessor ckAccessor, + + List> regularColumnGens, + List> staticColumnGens, + + List> ckComparators, + List> regularComparators, + List> staticComparators) + { + super(pkGen); + this.partitionValues = new PartitionValues<>(ckGen, ckAccessor, regularColumnGens, staticColumnGens, ckComparators, regularComparators, staticComparators); + } + + public Bijection pkGen() + { + return pkGen; + } + + @Override + public PartitionValues forPd(long pd) + { + return partitionValues; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java b/test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java index 91b2aa4f2970..b8878625336d 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java @@ -70,10 +70,12 @@ public String inflate(long descriptor) builder.append(nibbles[idx]); } - appendRandomBytes(builder, descriptor); + appendSuffix(builder, descriptor); + + // everything after the nibble prefix is just random padding, since strings are guaranteed + // to have unique prefixes. Subclasses may append additional content after this. + appendExtra(builder, descriptor); - // everything after this point can be just random, since strings are guaranteed - // to have unique prefixes return builder.toString(); } @@ -85,7 +87,7 @@ public static int getByte(long l, int idx) return b; } - private void appendRandomBytes(StringBuilder builder, long descriptor) + protected void appendSuffix(StringBuilder builder, long descriptor) { long rnd = RngUtils.next(descriptor); int remaining = RngUtils.asInt(rnd, 0, maxRandomBytes); @@ -101,6 +103,14 @@ private void appendRandomBytes(StringBuilder builder, long descriptor) } } + /** + * Hook for subclasses to append additional content after the nibble prefix and suffix. + * By default, does nothing. + */ + protected void appendExtra(StringBuilder builder, long descriptor) + { + } + public long deflate(String descriptor) { long res = 0; diff --git a/test/harry/main/org/apache/cassandra/harry/gen/TypeAdapters.java b/test/harry/main/org/apache/cassandra/harry/gen/TypeAdapters.java index 808ff5e45ef3..b68c4ee37cc6 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/TypeAdapters.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/TypeAdapters.java @@ -44,6 +44,7 @@ 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.stress.distribution.Distribution; import org.apache.cassandra.schema.ColumnMetadata; /** @@ -111,6 +112,41 @@ public static Generator forValues(AbstractType type) return forValues(type, defaults); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static Generator forValues(AbstractType type, Distribution distribution) + { + return forValues(type, distribution, 0xdeadbeefcafeL); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static Generator forValues(AbstractType type, Distribution distribution, long seed) + { + if (distribution == null) + return forValues(type, defaults); + + AbstractType baseType = type instanceof ReversedType ? ((ReversedType) type).baseType : type; + + if (baseType == AsciiType.instance) + { + char[] asciiChars = new char[128]; + for (int i = 0; i < 128; i++) + asciiChars[i] = (char) i; + return (Generator) (Generator) new UnorderedBijections.UnorderedStringBijection(0xaabbccddeeffL, seed, distribution, asciiChars); + } + else if (baseType == UTF8Type.instance) + { + int range = 0xD7FF + 1; + char[] utf8Chars = new char[range]; + for (int i = 0; i < range; i++) + utf8Chars[i] = (char) i; + return (Generator) (Generator) new UnorderedBijections.UnorderedStringBijection(0xaabbccddeeffL, seed, distribution, utf8Chars); + } + else if (baseType == BytesType.instance) + return (Generator) (Generator) new UnorderedBijections.UnorderedBytesBijection(0xaabbccddeeffL, seed, distribution); + else + throw new IllegalArgumentException(String.format("Distribution-based generator is not supported for type %s", type)); + } + private static Generator forValues(AbstractType type, Map, Generator> typeToGen) { Generator gen = (Generator) typeToGen.get(type); diff --git a/test/harry/main/org/apache/cassandra/harry/gen/UnorderedBijections.java b/test/harry/main/org/apache/cassandra/harry/gen/UnorderedBijections.java new file mode 100644 index 000000000000..fbd863a5db92 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/UnorderedBijections.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.nio.ByteBuffer; + +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.gen.rng.PureRng; +import org.apache.cassandra.harry.gen.rng.RngUtils; +import org.apache.cassandra.harry.stress.distribution.Distribution; + +/** + * Bijections where values are NOT sorted by index. Indices are assigned via a pseudo-random + * permutation (PCG), so the mapping between index and descriptor is invertible but unordered: + * iterating indices 0, 1, 2, ... produces values in an effectively random order. + */ +public class UnorderedBijections +{ + public static class UnorderedStringBijection extends StringBijection implements HistoryBuilder.IndexedBijection, Generator + { + private final long stream; + private final PureRng rng; + private final Distribution extraSizeDistribution; + private final char[] allowedChars; + + public UnorderedStringBijection(long seed) + { + this(0xaabbccddeeffL, seed, null, null); + } + + public UnorderedStringBijection(long stream, long seed) + { + this(stream, seed, null, null); + } + + public UnorderedStringBijection(long stream, long seed, Distribution extraSizeDistribution, char[] allowedChars) + { + this.stream = stream; + this.rng = new PureRng.PCGFast(seed); + this.extraSizeDistribution = extraSizeDistribution; + this.allowedChars = allowedChars; + } + + public UnorderedStringBijection(int nibbleSize, int maxRandomBytes, long stream, long seed, + Distribution extraSizeDistribution, char[] allowedChars) + { + super(nibbleSize, maxRandomBytes); + this.stream = stream; + this.rng = new PureRng.PCGFast(seed); + this.extraSizeDistribution = extraSizeDistribution; + this.allowedChars = allowedChars; + } + + public UnorderedStringBijection(String[] nibbles, int nibbleSize, int maxRandomBytes, long stream, long seed, + Distribution extraSizeDistribution, char[] allowedChars) + { + super(nibbles, nibbleSize, maxRandomBytes); + this.stream = stream; + this.rng = new PureRng.PCGFast(seed); + this.extraSizeDistribution = extraSizeDistribution; + this.allowedChars = allowedChars; + } + + @Override + public long idxFor(long descriptor) + { + return rng.sequenceNumber(descriptor, stream); + } + + @Override + public long descriptorAt(long idx) + { + return rng.randomNumber(idx, stream); + } + + @Override + public String generate(EntropySource rng) + { + return inflate(rng.next()); + } + + @Override + protected void appendExtra(StringBuilder builder, long descriptor) + { + if (extraSizeDistribution == null || allowedChars == null) + return; + + // Use a different seed derivation to avoid correlation with the suffix + long rnd = RngUtils.next(RngUtils.next(RngUtils.next(descriptor))); + int remaining = Math.toIntExact(extraSizeDistribution.next(rnd)); + + while (remaining > 0) + { + rnd = RngUtils.next(rnd); + builder.append(allowedChars[RngUtils.asInt(rnd, 0, allowedChars.length - 1)]); + remaining--; + } + } + } + + public static class UnorderedBytesBijection extends BytesBijection implements HistoryBuilder.IndexedBijection, Generator + { + private final long stream; + private final PureRng rng; + private final Distribution extraSizeDistribution; + + public UnorderedBytesBijection(long seed) + { + this(0xaabbccddeeffL, seed, null); + } + + public UnorderedBytesBijection(long stream, long seed) + { + this(stream, seed, null); + } + + public UnorderedBytesBijection(long stream, long seed, Distribution extraSizeDistribution) + { + this.stream = stream; + this.rng = new PureRng.PCGFast(seed); + this.extraSizeDistribution = extraSizeDistribution; + } + + public UnorderedBytesBijection(int nibbleSize, int maxRandomBytes, long stream, long seed, + Distribution extraSizeDistribution) + { + super(nibbleSize, maxRandomBytes); + this.stream = stream; + this.rng = new PureRng.PCGFast(seed); + this.extraSizeDistribution = extraSizeDistribution; + } + + public UnorderedBytesBijection(byte[][] nibbles, int nibbleSize, int maxRandomBytes, long stream, long seed, + Distribution extraSizeDistribution) + { + super(nibbles, nibbleSize, maxRandomBytes); + this.stream = stream; + this.rng = new PureRng.PCGFast(seed); + this.extraSizeDistribution = extraSizeDistribution; + } + + @Override + public long idxFor(long descriptor) + { + return rng.sequenceNumber(descriptor, stream); + } + + @Override + public long descriptorAt(long idx) + { + return rng.randomNumber(idx, stream); + } + + @Override + public ByteBuffer generate(EntropySource rng) + { + return inflate(rng.next()); + } + + @Override + protected int extraLength(long descriptor) + { + if (extraSizeDistribution == null) + return 0; + + // Use a different seed derivation to avoid correlation with the suffix + long rnd = RngUtils.next(RngUtils.next(RngUtils.next(descriptor))); + return Math.toIntExact(extraSizeDistribution.next(rnd)); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/ValueGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/ValueGenerators.java index 9c8d62ba1d5f..03f9bead57b8 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/ValueGenerators.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/ValueGenerators.java @@ -23,42 +23,13 @@ import org.apache.cassandra.harry.gen.Bijections.Bijection; -public class ValueGenerators +public abstract class ValueGenerators { protected final Bijection pkGen; - protected final Bijection ckGen; - protected final Accessor ckAccessor; - - 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(Bijection pkGen, - Bijection ckGen, - Accessor ckAccessor, - - List> regularColumnGens, - List> staticColumnGens, - - List> pkComparators, - List> ckComparators, - List> regularComparators, - List> staticComparators) + public ValueGenerators(Bijection pkGen) { this.pkGen = pkGen; - this.ckGen = ckGen; - this.ckAccessor = ckAccessor; - this.regularColumnGens = regularColumnGens; - this.staticColumnGens = staticColumnGens; - this.pkComparators = pkComparators; - this.ckComparators = ckComparators; - this.regularComparators = regularComparators; - this.staticComparators = staticComparators; } public Bijection pkGen() @@ -66,79 +37,105 @@ public Bijection pkGen() return pkGen; } - public Bijection ckGen() - { - return ckGen; - } + public abstract PartitionValues forPd(long pd); - public Bijection regularColumnGen(int idx) + public static class PartitionValues { - return regularColumnGens.get(idx); - } + protected final Bijection ckGen; - public Bijection staticColumnGen(int idx) - { - return staticColumnGens.get(idx); - } + protected final Accessor ckAccessor; - public int ckColumnCount() - { - return ckComparators.size(); - } + protected final List> regularColumnGens; + protected final List> staticColumnGens; - public int regularColumnCount() - { - return regularColumnGens.size(); - } + protected final List> ckComparators; + protected final List> regularComparators; + protected final List> staticComparators; - public int staticColumnCount() - { - return staticColumnGens.size(); - } + public PartitionValues(Bijection ckGen, + Accessor ckAccessor, - public Comparator pkComparator(int idx) - { - return pkComparators.get(idx); - } + List> regularColumnGens, + List> staticColumnGens, - public Comparator ckComparator(int idx) - { - return ckComparators.get(idx); - } + List> ckComparators, + List> regularComparators, + List> staticComparators) + { - public Comparator regularComparator(int idx) - { - return regularComparators.get(idx); - } + this.ckGen = ckGen; + this.ckAccessor = ckAccessor; + this.regularColumnGens = regularColumnGens; + this.staticColumnGens = staticColumnGens; + this.ckComparators = ckComparators; + this.regularComparators = regularComparators; + this.staticComparators = staticComparators; + } - public Comparator staticComparator(int idx) - { - return staticComparators.get(idx); - } + public Bijection ckGen() + { + return ckGen; + } - public Accessor ckAccessor() - { - return ckAccessor; - } + public Bijection regularColumnGen(int idx) + { + return regularColumnGens.get(idx); + } - public int pkPopulation() - { - return pkGen.population(); - } + public Bijection staticColumnGen(int idx) + { + return staticColumnGens.get(idx); + } - public int ckPopulation() - { - return ckGen.population(); - } + public int ckColumnCount() + { + return ckComparators.size(); + } - public int regularPopulation(int i) - { - return regularColumnGens.get(i).population(); - } + public int regularColumnCount() + { + return regularColumnGens.size(); + } - public int staticPopulation(int i) - { - return staticColumnGens.get(i).population(); + public int staticColumnCount() + { + return staticColumnGens.size(); + } + + 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 Accessor ckAccessor() + { + return ckAccessor; + } + + public int ckPopulation() + { + return Math.toIntExact(ckGen.population()); + } + + public int regularPopulation(int i) + { + return Math.toIntExact(regularColumnGens.get(i).population()); + } + + public int staticPopulation(int i) + { + return Math.toIntExact(staticColumnGens.get(i).population()); + } } public interface Accessor 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 index 2306966aeb3a..1ebe0b77a0c3 100644 --- a/test/harry/main/org/apache/cassandra/harry/gen/rng/SeedableEntropySource.java +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/SeedableEntropySource.java @@ -27,19 +27,30 @@ public class SeedableEntropySource { + // TODO (required): forbid reentrancy private static final FastThreadLocal THREAD_LOCAL = new FastThreadLocal<>(); + public static T computeWithSeed(long seed, long stream, Function fn) + { + return fn.apply(entropySource(seed, stream)); + } + public static T computeWithSeed(long seed, Function fn) { - return fn.apply(entropySource(seed)); + return fn.apply(entropySource(seed, seed)); + } + + public static long computeLongWithSeed(long seed, ToLong fn) + { + return fn.apply(entropySource(seed, seed)); } public static void doWithSeed(long seed, Consumer fn) { - fn.accept(entropySource(seed)); + fn.accept(entropySource(seed, seed)); } - private static EntropySource entropySource(long seed) + public static EntropySource entropySource(long seed, long stream) { EntropySource entropySource = THREAD_LOCAL.get(); if (entropySource == null) @@ -47,7 +58,15 @@ private static EntropySource entropySource(long seed) entropySource = new JdkRandomEntropySource(0); THREAD_LOCAL.set(entropySource); } + // scramble seed even more, since it seems to be not enough entropy for small cardinality values, such as rng.nextInt(2) + // in default JDK scrambler + seed = PCGFastPure.shuffle(PCGFastPure.advanceState(seed, stream, 100)); entropySource.seed(seed); return entropySource; } + + public static interface ToLong + { + long apply(T value); + } } \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java b/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java index b64d9613bb18..479a9f83bc04 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java +++ b/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java @@ -43,6 +43,7 @@ import org.apache.cassandra.harry.MagicConstants; import org.apache.cassandra.harry.gen.BijectionCache; import org.apache.cassandra.harry.gen.Bijections; +import org.apache.cassandra.harry.gen.SharedValueGenerators; import org.apache.cassandra.harry.gen.ValueGenerators; import org.apache.cassandra.harry.util.BitSet; import org.apache.cassandra.schema.ColumnMetadata; @@ -598,10 +599,9 @@ public Factory(TableMetadata metadata) clusteringCache = new BijectionCache<>(clusteringComparator); ValueGenerators.Accessor> clusteringAccessor = (offset, clustering) -> clustering.bufferAt(offset); - valueGenerators = new ValueGenerators<>(partitionCache, clusteringCache, clusteringAccessor, - regularColumnGens, staticColumnGens, - pkComparators, ckComparators, - regularComparators, staticComparators); + valueGenerators = new SharedValueGenerators<>(partitionCache, clusteringCache, clusteringAccessor, + regularColumnGens, staticColumnGens, ckComparators, + regularComparators, staticComparators); } private Comparator compareValue(AbstractType type) 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 08b72c25d157..63cafd4c6d99 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/Model.java +++ b/test/harry/main/org/apache/cassandra/harry/model/Model.java @@ -28,22 +28,21 @@ public interface Model { void validate(Operations.SelectStatement select, List actual); - class LtsOperationPair + interface FullReplay extends Iterable { - final long lts; - final int opId; - - public LtsOperationPair(long lts, int opId) - { - this.lts = lts; - this.opId = opId; - } } - interface Replay extends Iterable + interface PartialReplay { - Visit replay(long lts); - Operations.Operation replay(long lts, int opId); + Iterable potentialVisits(long pd); } + public Model NO_OP = new Model() + { + @Override + public void validate(Operations.SelectStatement select, List actual) + { + + } + }; } diff --git a/test/harry/main/org/apache/cassandra/harry/model/PartitionState.java b/test/harry/main/org/apache/cassandra/harry/model/PartitionState.java index 8701c452ce03..f59f58c3dbb5 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/PartitionState.java +++ b/test/harry/main/org/apache/cassandra/harry/model/PartitionState.java @@ -56,16 +56,22 @@ public class PartitionState implements Iterable NavigableMap rows; final ValueGenerators valueGenerators; - + final ValueGenerators.PartitionValues partitionValues; public PartitionState(long pd, ValueGenerators valueGenerators) { this.pd = pd; - this.rows = new TreeMap<>(valueGenerators.ckGen().descriptorsComparator()); this.valueGenerators = valueGenerators; + this.partitionValues = valueGenerators.forPd(pd); + // TODO: ideally, we want to switch to ck _indexes_ here, since they are still in the value order. + // this is just an unfortunate consequence of going from index to value t descriptor. + // Order rows by clustering value rather than by the raw clustering descriptor: for indexed bijections + // the descriptor is only a seed and is not monotonic with value, so a natural-ordered map would not + // match the clustering order the database returns. + this.rows = new TreeMap(partitionValues.ckGen()::compare); this.staticRow = new RowState(this, STATIC_CLUSTERING, - arr(valueGenerators.staticColumnCount(), MagicConstants.NIL_DESCR), - arr(valueGenerators.staticColumnCount(), MagicConstants.NO_TIMESTAMP)); + arr(partitionValues.staticColumnCount(), MagicConstants.NIL_DESCR), + arr(partitionValues.staticColumnCount(), MagicConstants.NO_TIMESTAMP)); } /** @@ -78,20 +84,21 @@ public NavigableMap rows() public void writeStatic(long[] sds, long lts) { - staticRow = updateRowState(staticRow, valueGenerators::staticColumnGen, STATIC_CLUSTERING, sds, lts, false); + staticRow = updateRowState(staticRow, partitionValues::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)); + rows.compute(cd, (cd_, current) -> updateRowState(current, partitionValues::regularColumnGen, cd, vds, lts, writePrimaryKeyLiveness)); } + // TODO: Make sure to use LTS, as writes can be propagated out of order! 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(), + rows.entrySet().removeIf(e -> Relations.matchRange(partitionValues.ckGen(), + partitionValues::ckComparator, + partitionValues.ckColumnCount(), delete.lowerBound(), delete.upperBound(), delete.lowerBoundRelation(), @@ -134,9 +141,9 @@ private void filterInternal(Operations.SelectRow select) 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(), + rows.entrySet().removeIf(e -> !Relations.matchRange(partitionValues.ckGen(), + partitionValues::ckComparator, + partitionValues.ckColumnCount(), select.lowerBound(), select.upperBound(), select.lowerBoundRelation(), @@ -151,10 +158,10 @@ private void filterInternal(Operations.SelectCustom select) 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); - var accessor = valueGenerators.ckAccessor(); - if (!relation.kind.match(valueGenerators.ckComparator(relation.column), + Object query = cache.computeIfAbsent(relation.descriptor, partitionValues.ckGen()::inflate); + Object match = cache.computeIfAbsent(e.getValue().cd, partitionValues.ckGen()::inflate); + var accessor = partitionValues.ckAccessor(); + if (!relation.kind.match(partitionValues.ckComparator(relation.column), accessor.access(relation.column, match), accessor.access(relation.column, query))) return true; // true means "no match", so remove from resultset @@ -162,23 +169,23 @@ private void filterInternal(Operations.SelectCustom select) for (Relations.Relation relation : select.regularRelations()) { - Object query = valueGenerators.regularColumnGen(relation.column).inflate(relation.descriptor); + Object query = partitionValues.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)) + Object match = partitionValues.regularColumnGen(relation.column).inflate(e.getValue().vds[relation.column]); + if (!relation.kind.match(partitionValues.regularComparator(relation.column), match, query)) return true; } for (Relations.Relation relation : select.staticRelations()) { - Object query = valueGenerators.staticColumnGen(relation.column).inflate(relation.descriptor); + Object query = partitionValues.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)) + Object match = partitionValues.staticColumnGen(relation.column).inflate(e.getValue().partitionState.staticRow.vds[relation.column]); + if (!relation.kind.match(partitionValues.staticComparator(relation.column), match, query)) return true; } @@ -239,7 +246,8 @@ private RowState updateRowState(RowState currentState, IntFunction= 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 + // TODO: lts could be out of order with accord, but these asserts could still be useful + //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 (lts != MagicConstants.NO_TIMESTAMP && currentState.lts[i] == lts) { @@ -254,7 +262,7 @@ private RowState updateRowState(RowState currentState, IntFunction currentState.lts[i]; + // assert lts == MagicConstants.NO_TIMESTAMP || lts > currentState.lts[i]; currentState.vds[i] = vds[i]; currentState.lts[i] = lts; } @@ -345,12 +353,12 @@ public String toString() if (staticRow != null) { - sb.append("Static row:\n" + staticRow.toString(valueGenerators)).append("\n"); + sb.append("Static row:\n" + staticRow).append("\n"); sb.append("\n"); } for (RowState row : rows.values()) - sb.append(row.toString(valueGenerators)).append("\n"); + sb.append(row.toString()).append("\n"); return sb.toString(); } @@ -400,31 +408,26 @@ public static String descrToIdxForToString(Bijections.Bijection gen, long des return gen.toString(descr); } - public String toString(ValueGenerators valueGenerators) + @Override + public String toString() { if (cd == STATIC_CLUSTERING) { return " rowStateRow(" - + valueGenerators.pkGen().toString(partitionState.pd) + + + partitionState.valueGenerators.pkGen().toString(partitionState.pd) + ", STATIC" + - ", statics(" + toString(partitionState.staticRow.vds, valueGenerators::staticColumnGen) + ")" + + ", statics(" + toString(partitionState.staticRow.vds, partitionState.partitionValues::staticColumnGen) + ")" + ", lts(" + StringUtils.toString(partitionState.staticRow.lts) + ")"; } else { return " rowStateRow(" - + valueGenerators.pkGen().toString(partitionState.pd) + - ", " + descrToIdxForToString(valueGenerators.ckGen(), cd) + - ", vds(" + toString(vds, valueGenerators::regularColumnGen) + ")" + + + partitionState.valueGenerators.pkGen().toString(partitionState.pd) + + ", " + descrToIdxForToString(partitionState.partitionValues.ckGen(), cd) + + ", vds(" + toString(vds, partitionState.partitionValues::regularColumnGen) + ")" + ", lts(" + StringUtils.toString(lts) + ")"; } } - - @Override - public String toString() - { - return toString(partitionState.valueGenerators); - } } public static long[] arr(int length, long fill) diff --git a/test/harry/main/org/apache/cassandra/harry/model/PartitionStateBuilder.java b/test/harry/main/org/apache/cassandra/harry/model/PartitionStateBuilder.java index d70ecad1b4db..ed3324fb9fcc 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/PartitionStateBuilder.java +++ b/test/harry/main/org/apache/cassandra/harry/model/PartitionStateBuilder.java @@ -32,7 +32,7 @@ import static org.apache.cassandra.harry.op.Operations.DeleteColumnsOp; import static org.apache.cassandra.harry.op.Operations.DeleteRange; import static org.apache.cassandra.harry.op.Operations.DeleteRow; -import static org.apache.cassandra.harry.op.Operations.Kind.INSERT; +import static org.apache.cassandra.harry.op.Kind.INSERT; import static org.apache.cassandra.harry.op.Operations.Operation; import static org.apache.cassandra.harry.op.Operations.WriteOp; @@ -46,9 +46,9 @@ class PartitionStateBuilder extends VisitExecutor private final List rangeDeletes; private final List writes; private final List columnDeletes; - private final ValueGenerators valueGenerators; + private final ValueGenerators valueGenerators; - PartitionStateBuilder(ValueGenerators valueGenerators, + PartitionStateBuilder(ValueGenerators valueGenerators, PartitionState partitionState) { this.valueGenerators = valueGenerators; @@ -122,7 +122,7 @@ protected void endLts(long lts) if (hadTrackingRowWrite) { - long[] statics = new long[valueGenerators.staticColumnCount()]; + long[] statics = new long[valueGenerators.forPd(writeOp.pd).staticColumnCount()]; Arrays.fill(statics, MagicConstants.UNSET_DESCR); partitionState.writeStatic(statics, lts); } 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 600f388515f9..98f4048040b7 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java +++ b/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java @@ -25,11 +25,10 @@ import java.util.NavigableMap; import accord.utils.Invariants; - -import org.apache.cassandra.harry.execution.DataTracker; -import org.apache.cassandra.harry.execution.ResultSetRow; import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.op.ClusteringOrderBy; import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.execution.ResultSetRow; import static org.apache.cassandra.harry.MagicConstants.LTS_UNKNOWN; import static org.apache.cassandra.harry.MagicConstants.NIL_DESCR; @@ -41,19 +40,15 @@ * 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 { - private final DataTracker tracker; - private final Replay replay; - private final ValueGenerators valueGenerators; + private final PartialReplay replay; + private final ValueGenerators valueGenerators; - public QuiescentChecker(ValueGenerators valueGenerators, DataTracker tracker, Replay replay) + public QuiescentChecker(ValueGenerators valueGenerators, PartialReplay replay) { this.valueGenerators = valueGenerators; - this.tracker = tracker; this.replay = replay; } @@ -64,20 +59,19 @@ public void validate(Operations.SelectStatement select, List actua PartitionStateBuilder stateBuilder = new PartitionStateBuilder(valueGenerators, partitionState); long prevLts = -1; - for (LtsOperationPair potentialVisit : tracker.potentialVisits(select.pd())) + + // In case of quiescent checkers, all potential visits have to be finished + for (Operations.Operation potentialVisit : replay.potentialVisits(select.pd())) { - if (tracker.isFinished(potentialVisit.lts)) + if (potentialVisit.lts() != prevLts) { - 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); + if (prevLts != -1) + stateBuilder.endLts(prevLts); + stateBuilder.beginLts(potentialVisit.lts()); + prevLts = potentialVisit.lts(); } + + stateBuilder.operation(potentialVisit); } // Close last open LTS @@ -85,7 +79,7 @@ public void validate(Operations.SelectStatement select, List actua stateBuilder.endLts(prevLts); partitionState.filter(select); - if (select.orderBy() == Operations.ClusteringOrderBy.DESC) + if (select.orderBy() == ClusteringOrderBy.DESC) { partitionState.reverse(); } @@ -94,7 +88,7 @@ public void validate(Operations.SelectStatement select, List actua } // TODO: reverse - public static void validate(ValueGenerators valueGenerators, PartitionState partitionState, List actualRows) + public static void validate(ValueGenerators valueGenerators, PartitionState partitionState, List actualRows) { Iterator actual = actualRows.iterator(); NavigableMap expectedRows = partitionState.rows(); @@ -148,7 +142,7 @@ public static void validate(ValueGenerators valueGenerators, PartitionState part "Found a row in the model that is not present in the resultset:" + "\nExpected: %s" + "\nActual: %s", - expectedRowState.toString(valueGenerators), + expectedRowState.toString(), actualRowState.toString(valueGenerators)); } @@ -158,7 +152,7 @@ public static void validate(ValueGenerators valueGenerators, PartitionState part "Returned row state doesn't match the one predicted by the model:" + "\nExpected: %s" + "\nActual: %s.", - expectedRowState.toString(valueGenerators), + expectedRowState.toString(), actualRowState.toString(valueGenerators)); if (!ltsEqual(expectedRowState.lts, actualRowState.lts)) @@ -167,7 +161,7 @@ public static void validate(ValueGenerators valueGenerators, PartitionState part "Timestamps in the row state don't match ones predicted by the model:" + "\nExpected: %s" + "\nActual: %s.", - expectedRowState.toString(valueGenerators), + expectedRowState.toString(), actualRowState.toString(valueGenerators)); if (partitionState.staticRow() != null || actualRowState.hasStaticColumns()) @@ -242,7 +236,7 @@ public static void assertStaticRow(PartitionState partitionState, "Returned static row state doesn't match the one predicted by the model:" + "\nExpected: %s (%s)" + "\nActual: %s (%s).", - descriptorsToString(staticRow.vds), staticRow.toString(valueGenerators), + descriptorsToString(staticRow.vds), staticRow.toString(), descriptorsToString(actualRowState.sds), actualRowState); if (!ltsEqual(staticRow.lts, actualRowState.slts)) @@ -251,7 +245,7 @@ public static void assertStaticRow(PartitionState partitionState, "Timestamps in the static row state don't match ones predicted by the model:" + "\nExpected: %s (%s)" + "\nActual: %s (%s).", - Arrays.toString(staticRow.lts), staticRow.toString(valueGenerators), + Arrays.toString(staticRow.lts), staticRow.toString(), Arrays.toString(actualRowState.slts), actualRowState); } @@ -277,7 +271,7 @@ public static String toString(Collection collection, Va StringBuilder builder = new StringBuilder(); for (PartitionState.RowState rowState : collection) - builder.append(rowState.toString(valueGenerators)).append("\n"); + builder.append(rowState.toString()).append("\n"); return builder.toString(); } diff --git a/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java index a1bc0a37a473..8d81707b97b2 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java +++ b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java @@ -981,14 +981,21 @@ public static class Node implements Comparable private final int dcIdx; private final int rackIdx; private final Lookup lookup; + private final String fqdn; public Node(int tokenIdx, int idx, int dcIdx, int rackIdx, Lookup lookup) + { + this(tokenIdx, idx, dcIdx, rackIdx, lookup, null); + } + + public Node(int tokenIdx, int idx, int dcIdx, int rackIdx, Lookup lookup, String fqdn) { this.tokenIdx = tokenIdx; this.nodeIdx = idx; this.dcIdx = dcIdx; this.rackIdx = rackIdx; this.lookup = lookup; + this.fqdn = fqdn; } public String id() @@ -996,6 +1003,16 @@ public String id() return lookup.id(nodeIdx); } + public String fqdn() + { + return fqdn != null ? fqdn : id(); + } + + public Node overrideFQDN(String fqdn) + { + return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup, fqdn); + } + public int idx() { return nodeIdx; @@ -1033,22 +1050,22 @@ public int tokenIdx() public Node withNewToken() { - return new Node(tokenIdx + 100_000, nodeIdx, dcIdx, rackIdx, lookup); + return new Node(tokenIdx + 100_000, nodeIdx, dcIdx, rackIdx, lookup, fqdn); } public Node withToken(int tokenIdx) { - return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup); + return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup, fqdn); } public Node overrideToken(long override) { - return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup.forceToken(tokenIdx, override)); + return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup.forceToken(tokenIdx, override), fqdn); } public Node withNewRack(String newRack) { - return new Node(tokenIdx, nodeIdx, dcIdx, lookup.rackIdx(newRack), lookup); + return new Node(tokenIdx, nodeIdx, dcIdx, lookup.rackIdx(newRack), lookup, fqdn); } public Murmur3Partitioner.LongToken longToken() diff --git a/test/harry/main/org/apache/cassandra/harry/op/ClusteringOrderBy.java b/test/harry/main/org/apache/cassandra/harry/op/ClusteringOrderBy.java new file mode 100644 index 000000000000..18465c90524b --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/op/ClusteringOrderBy.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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; + +/** + * 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 +} diff --git a/test/harry/main/org/apache/cassandra/harry/op/Kind.java b/test/harry/main/org/apache/cassandra/harry/op/Kind.java new file mode 100644 index 000000000000..6287376e4df4 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/op/Kind.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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; + +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; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/op/Operations.java b/test/harry/main/org/apache/cassandra/harry/op/Operations.java index 0b9c15832bbd..c069eb352b75 100644 --- a/test/harry/main/org/apache/cassandra/harry/op/Operations.java +++ b/test/harry/main/org/apache/cassandra/harry/op/Operations.java @@ -28,11 +28,11 @@ 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 { + // TODO: remove lts from every op; leave only for descriptor public static class WriteOp extends PartitionOperation { private final long cd; @@ -381,34 +381,6 @@ public String toString() } } - 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; @@ -440,124 +412,4 @@ 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.require(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/Selection.java b/test/harry/main/org/apache/cassandra/harry/op/Selection.java new file mode 100644 index 000000000000..2666c52c70ca --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/op/Selection.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.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.SchemaSpec; +import org.apache.cassandra.harry.util.BitSet; + +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.require(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); + } + } + + 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); + } + } + + 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; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/op/Visit.java b/test/harry/main/org/apache/cassandra/harry/op/Visit.java index 7d77b7287159..37e4e8fff240 100644 --- a/test/harry/main/org/apache/cassandra/harry/op/Visit.java +++ b/test/harry/main/org/apache/cassandra/harry/op/Visit.java @@ -21,42 +21,50 @@ import java.util.HashSet; import java.util.Set; -import org.junit.Assert; - +import accord.utils.Invariants; import org.apache.cassandra.harry.op.Operations.Operation; public class Visit { public final long lts; + // TODO: specialize single-op visits public final Operation[] operations; - public final Set visitedPartitions; + public final long[] visitedPartitions; - public final boolean selectOnly; + public final boolean validating; public final boolean hasCustom; public Visit(long lts, Operation[] operations) { - Assert.assertTrue(operations.length > 0); + Invariants.require(operations.length > 0); this.lts = lts; this.operations = operations; - this.visitedPartitions = new HashSet<>(); boolean selectOnly = true; boolean hasCustom = false; + Set visitedPartitions = new HashSet<>(); for (Operation operation : operations) { - if (operation.kind() == Operations.Kind.CUSTOM) + if (operation.kind() == Kind.CUSTOM) hasCustom = true; if (selectOnly && !(operation instanceof Operations.SelectStatement)) selectOnly = false; if (operation instanceof Operations.PartitionOperation) visitedPartitions.add(((Operations.PartitionOperation) operation).pd()); - } - this.selectOnly = selectOnly; + this.visitedPartitions = new long[visitedPartitions.size()]; + int idx = 0; + for (Long partition : visitedPartitions) + this.visitedPartitions[idx++] = partition; + this.validating = selectOnly; this.hasCustom = hasCustom; } + public boolean validating() + { + return validating; + } + public String toString() { if (operations.length == 1) diff --git a/test/harry/main/org/apache/cassandra/harry/stress/ActivePartition.java b/test/harry/main/org/apache/cassandra/harry/stress/ActivePartition.java new file mode 100644 index 000000000000..bcd8d12c4bc4 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/ActivePartition.java @@ -0,0 +1,744 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.IntSupplier; +import java.util.function.LongConsumer; +import java.util.function.LongFunction; +import java.util.function.LongPredicate; +import java.util.function.LongSupplier; +import java.util.stream.Collectors; + +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.dsl.IndexedValueGenerators; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.IndexGenerators; +import org.apache.cassandra.harry.gen.InvertibleGenerator; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.harry.gen.rng.PureRng; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.harry.util.IteratorsUtil; +import org.apache.cassandra.utils.LazyToString; + +import static org.apache.cassandra.harry.SchemaSpec.cumulativeEntropy; +import static org.apache.cassandra.harry.SchemaSpec.forKeys; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.keyComparator; +import static org.apache.cassandra.harry.dsl.IndexedValueGenerators.IndexedPartitionValues; +import static org.apache.cassandra.harry.gen.InvertibleGenerator.fromType; + +/** + * + */ +public final class ActivePartition extends IndexedPartitionValues +{ + /** + * A helper class to convert between descriptors and indices _for partitions_ + */ + public enum DescriptorIndexBijection + { + INSTANCE; + + private final long stream = 0xffeeddccbbaal; + private final PureRng rng = new PureRng.PCGFast(0xaabbccddeeffl); + + public long toIdx(long pd) + { + return rng.sequenceNumber(pd, stream); + } + + public long toPd(long idx) + { + return rng.randomNumber(idx, stream); + } + + } + + public final long idx; + public final long pd; + + // TODO: document why two + private final HistoryBuilder.IndexedBijection cachingPkGen; + private final HistoryBuilder.IndexedBijection rawGen; + private final AtomicInteger refCount = new AtomicInteger(0); + + private final LongConsumer cleanup; + + ActivePartition(long pkIdx, + long pd, + HistoryBuilder.IndexedBijection cachingPkGen, + HistoryBuilder.IndexedBijection rawGen, + HistoryBuilder.IndexedBijection ckGen, + List> regularColumnGens, + List> staticColumnGens, + List> pkComparators, + List> ckComparators, + List> regularComparators, + List> staticComparators, + LongConsumer cleanup) + { + super(ckGen, regularColumnGens, staticColumnGens, ckComparators, regularComparators, staticComparators, + IndexGenerators.uniform(ckGen), + IndexGenerators.uniform(regularColumnGens), + IndexGenerators.uniform(regularColumnGens)); + + this.cachingPkGen = cachingPkGen; + this.rawGen = rawGen; + Invariants.require(DescriptorIndexBijection.INSTANCE.toIdx(pd) == pkIdx); + Invariants.require(DescriptorIndexBijection.INSTANCE.toPd(pkIdx) == pd); + this.idx = pkIdx; + this.pd = pd; + + this.cleanup = v -> { + cleanup.accept(v); + ckGen.discard(); + }; + } + + public HistoryBuilder.IndexedBijection pkGen() + { + return cachingPkGen; + } + + private static class ObjectWrapper + { + public final Object[] value; + + private ObjectWrapper(Object[] value) + { + this.value = value; + } + + @Override + public boolean equals(Object o) + { + if (o == null || getClass() != o.getClass()) return false; + ObjectWrapper that = (ObjectWrapper) o; + return Objects.deepEquals(value, that.value); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(value); + } + } + + public void ref() + { + refCount.incrementAndGet(); + } + + public void deref() + { + int v = refCount.decrementAndGet(); + Invariants.require(v >= 0); + if (v == 0) + cleanup.accept(pd); + } + + /** + * It is easiest to generate an operation for a given partition based knowing what values partition may + * potentially hold. + * + * This class is _not_ thread safe + */ + public static class Partitions extends IndexedValueGenerators + { + final SchemaSpec schema; + final Distribution rowPopulation; + final VisitGenerator.ColumnPopulation columnPopulation; + final List activePartitions; + final VisitedPartitions visitedPartitions; + final Map partitionCache = new ConcurrentHashMap<>(); + final AtomicLong nextPartitionIdx = new AtomicLong(); + final RotationStrategy rotationStrategy; + + final long minPartitionIdx; + final long maxPartitionIdx; + final long initialLts; + + LongConsumer onRemove; + + public Partitions(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + RotationStrategy rotationStrategy) + { + this(schema, rowPopulation, columnPopulation, rotationStrategy, 0, Long.MAX_VALUE, 0); + } + + public Partitions(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + RotationStrategy rotationStrategy, + long minPartitionIdx, + long maxPartitionIdx) + { + this(schema, rowPopulation, columnPopulation, rotationStrategy, minPartitionIdx, maxPartitionIdx, 0); + } + + public Partitions(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + RotationStrategy rotationStrategy, + long minPartitionIdx, + long maxPartitionIdx, + long initialLts) + { + super(new PartitionKeyGen(schema)); + Invariants.require(minPartitionIdx >= 0, "minPartitionIdx must be non-negative: %d", minPartitionIdx); + Invariants.require(maxPartitionIdx > minPartitionIdx, "maxPartitionIdx must be greater than minPartitionIdx: %d > %d", maxPartitionIdx, minPartitionIdx); + this.schema = schema; + this.rowPopulation = rowPopulation; + this.columnPopulation = columnPopulation; + this.activePartitions = new ArrayList<>(rotationStrategy.targetSize()); + this.minPartitionIdx = minPartitionIdx; + this.maxPartitionIdx = maxPartitionIdx; + this.initialLts = initialLts; + this.nextPartitionIdx.set(minPartitionIdx); + this.visitedPartitions = new VisitedPartitions(minPartitionIdx); + this.onRemove = (pd_) -> { + Invariants.nonNull(partitionCache.remove(pd_)); + pkGenInternal().cleanup(pd_); + long pdIdx = DescriptorIndexBijection.INSTANCE.toIdx(pd_); + visitedPartitions.add(pdIdx); + }; + this.rotationStrategy = rotationStrategy; + } + + /** + * Populates the active partitions by replaying all partition switches from LTS 0 up to + * {@code initialLts}, so that the active partitions and visited partitions are in the + * correct state for resuming from that LTS. + * + * When {@code initialLts} is 0, this simply creates the initial set of active partitions. + * + * During replay, we track only partition indices without creating full + * {@link ActivePartition} state. The actual partition objects are only created at the + * end, once the final set of active partition indices is known. + */ + public void populate() + { + List activeIdxs = new ArrayList<>(rotationStrategy.targetSize()); + + for (int i = 0; i < rotationStrategy.targetSize(); i++) + activeIdxs.add(advanceNextPartitionIdx()); + + for (long lts = 0; lts < initialLts; lts++) + { + if (!rotationStrategy.shouldSwitch(lts)) + continue; + + applyActions(lts, + activeIdxs::size, + this::advanceNextPartitionIdx, + (pos, newIdx) -> { visitedPartitions.add(activeIdxs.get(pos)); activeIdxs.set(pos, newIdx); }, + (visitedPd) -> activeIdxs.contains(DescriptorIndexBijection.INSTANCE.toIdx(visitedPd)), + (pos, visitedIdx) -> { visitedPartitions.add(activeIdxs.get(pos)); activeIdxs.set(pos, visitedIdx); }, + pos -> DescriptorIndexBijection.INSTANCE.toPd(activeIdxs.get(pos)), + action -> {}); + } + + // Now inflate all the active partition objects from the final set of indices + for (long idx : activeIdxs) + { + ActivePartition activePartition = byIdx(idx); + activePartition.ref(); + partitionCache.put(activePartition.pd, activePartition); + activePartitions.add(activePartition); + } + } + + private long advanceNextPartitionIdx() + { + long idx = nextPartitionIdx.getAndIncrement(); + Invariants.require(idx < maxPartitionIdx, "Exhausted partition index space: %d >= %d", idx, maxPartitionIdx); + return idx; + } + + private void applyActions(long lts, + IntSupplier activeSize, + LongSupplier createNew, + BiConsumer replaceWithNew, + LongPredicate activePd, + BiConsumer replaceWithVisited, + java.util.function.IntToLongFunction pdAtPosition, + Consumer onAction) + { + RotationStrategy.PartitionAction[] actions = SeedableEntropySource.computeWithSeed(lts, rotationStrategy::generate); + + for (int i = 0; i < actions.length; i++) + { + RotationStrategy.PartitionAction action = actions[i]; + int size = activeSize.getAsInt(); + switch (action) + { + case REPLACE_WITH_NEW: + { + if (size == 0) + continue; + int remove = SeedableEntropySource.computeWithSeed(Util.hash(lts, i), rng -> rng.nextInt(size)); + // Skip eviction with probability proportional to log2 of partition size + long candidatePd = pdAtPosition.applyAsLong(remove); + int partitionSize = Math.toIntExact(rowPopulation.next(candidatePd)); + boolean evict = SeedableEntropySource.computeWithSeed(Util.hash(lts, i), + rng -> rng.nextInt(Math.max(1, Integer.highestOneBit(partitionSize))) == 0); + if (!evict) + continue; + long newIdx = createNew.getAsLong(); + replaceWithNew.accept(remove, newIdx); + break; + } + case REPLACE_WITH_VISITED: + { + if (size == 0) + continue; + int remove = SeedableEntropySource.computeWithSeed(Util.hash(lts, i), rng -> rng.nextInt(size)); + // Skip eviction with probability proportional to log2 of partition size + long candidatePd = pdAtPosition.applyAsLong(remove); + int partitionSize = Math.toIntExact(rowPopulation.next(candidatePd)); + boolean evict = SeedableEntropySource.computeWithSeed(Util.hash(lts, i), + rng -> rng.nextInt(Math.max(1, Integer.highestOneBit(partitionSize))) == 0); + if (!evict) + continue; + long visitedIdx = visitedPartitions.getBySeed(Util.hash(lts, i)); + long visitedPd = visitedIdx < 0 ? -1 : DescriptorIndexBijection.INSTANCE.toPd(visitedIdx); + if (visitedPd < 0 || activePd.test(visitedPd)) + { + // No visited partitions available or picked one is still active; fall back to new + long newIdx = createNew.getAsLong(); + replaceWithNew.accept(remove, newIdx); + } + else + { + replaceWithVisited.accept(remove, visitedIdx); + } + break; + } + } + onAction.accept(action); + } + } + + /** + * Add a callback to be triggered when partition is phased out. + * + * TODO (consider): This might start racing when we allow adding partitions back. + */ + public void onRemove(LongConsumer consumer) + { + LongConsumer prev = this.onRemove; + this.onRemove = pd -> { + prev.accept(pd); + consumer.accept(pd); + }; + } + + // TODO: biased partition picker + public ActivePartition pick(EntropySource entropySource) + { + ActivePartition picked = activePartitions.get(entropySource.nextInt(activePartitions.size())); + Invariants.require(picked.refCount.get() > 0); + Invariants.require(partitionCache.containsKey(picked.pd)); + return picked; + } + + @Override + public Generator pkIdxGen() + { + throw new UnsupportedOperationException(); + } + + private PartitionKeyGen pkGenInternal() + { + return (PartitionKeyGen) super.pkGen(); + } + + private ActivePartition byIdx(long idx) + { + long pd = DescriptorIndexBijection.INSTANCE.toPd(idx); + ActivePartition partition = createActivePartition(idx, pd, schema, rowPopulation, columnPopulation, (HistoryBuilder.IndexedBijection) pkGen, onRemove); + pkGenInternal().ensure(pd, partition.rawGen::inflate); + return partition; + } + + @Override + public ActivePartition forPd(long pd) + { + return Invariants.nonNull(partitionCache.get(pd)); + } + + public void maybeSwitchPartition(long lts, Consumer consumer) + { + if (!rotationStrategy.shouldSwitch(lts)) + return; + + applyActions(lts, + activePartitions::size, + () -> { + long idx = advanceNextPartitionIdx(); + ActivePartition ap = byIdx(idx); + ap.ref(); + partitionCache.put(ap.pd, ap); + return idx; + }, + (pos, newIdx) -> { + ActivePartition next = Invariants.nonNull(partitionCache.get(DescriptorIndexBijection.INSTANCE.toPd(newIdx))); + activePartitions.set(pos, next).deref(); + }, + partitionCache::containsKey, + (pos, visitedIdx) -> { + ActivePartition next = byIdx(visitedIdx); + next.ref(); + partitionCache.put(next.pd, next); + activePartitions.set(pos, next).deref(); + }, + pos -> activePartitions.get(pos).pd, + consumer); + } + } + + public static class PartitionKeyGen implements HistoryBuilder.IndexedBijection + { + final SchemaSpec schema; + + final Map deflate = new HashMap<>(); + final Map inflate = new HashMap<>(); + + public PartitionKeyGen(SchemaSpec schema) + { + this.schema = schema; + } + + public void cleanup(long pd) + { + Object[] values = Invariants.nonNull(inflate.remove(pd)); + Invariants.nonNull(deflate.remove(new ObjectWrapper(values))); + } + + public void ensure(long pd, LongFunction value) + { + if (!inflate.containsKey(pd)) + { + // TODO: need to extract pkgen from inside value descriptors + Object[] values = value.apply(pd); + inflate.put(pd, values); + deflate.put(new ObjectWrapper(values), pd); + } + } + + @Override + public Object[] inflate(long pd) + { + return Invariants.nonNull(inflate.get(pd)); + } + + @Override + public long deflate(Object[] value) + { + return Invariants.nonNull(deflate.get(new ObjectWrapper(value)), + "Could not find deflate ", LazyToString.lazy(() -> Arrays.toString(value))); + } + + @Override + public int byteSize() + { + return 0; + } + + @Override + public int compare(long l, long r) + { + return 0; + } + + @Override + public long idxFor(long pd) + { + return DescriptorIndexBijection.INSTANCE.toIdx(pd); + } + + @Override + public long descriptorAt(long idx) + { + return DescriptorIndexBijection.INSTANCE.toPd(idx); + } + } + + /** + * For _any_ partition, its characteristics are deterministic and depend on its pd. In other words, over the lifetime + * of partition, its max number of rows (and, therefore, possible values for its rows), _can not_ be changed. However, + * partition can get rotated in and out from active set at any point in time. + */ + @SuppressWarnings("unchecked") + public static ActivePartition createActivePartition(long idx, + long pd, + SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation population, + HistoryBuilder.IndexedBijection cachingPkGen, + LongConsumer cleanup) + { + List> pkComparators = new ArrayList<>(); + List> ckComparators = new ArrayList<>(); + List> regularComparators = new ArrayList<>(); + List> staticComparators = new ArrayList<>(); + + EntropySource rng = new JdkRandomEntropySource(pd); + 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, HistoryBuilder.IndexedBijection> map = new HashMap<>(); + for (ColumnSpec column : IteratorsUtil.concat(schema.regularColumns, schema.staticColumns)) + { + int populationPerColumn = Math.toIntExact(population.distribution(column.name).next(pd)); + map.computeIfAbsent(column, (a) -> (HistoryBuilder.IndexedBijection) fromType(rng, populationPerColumn, column)); + } + + // As of now, we allow only single partition queries, and within the scope of the visit we can select + // values only from one partition, so we simply create an identity PK bijection to avoid lookups altogether. + HistoryBuilder.IndexedBijection rawPkGen = new HistoryBuilder.IndexedBijection() { + private Object[] value = null; + + @Override + public Object[] inflate(long descriptor) { + Invariants.require(pd == descriptor, "Partition descriptor mismatch: %d != %d", pd, descriptor); + return ensureValue(); + } + + private Object[] ensureValue() + { + if (value == null) + value = SeedableEntropySource.computeWithSeed(pd, forKeys(schema.partitionKeys)::generate); + return value; + } + @Override + public long deflate(Object[] value) { + Invariants.require(Arrays.equals(value, ensureValue()), "Partition key mismatch, %s != %s", ensureValue(), value); + // TODO (required): allow selecting only a subset of PK and CK + return pd; + } + + @Override + public int byteSize() { + return Long.BYTES; + } + + @Override + public int compare(long l, long r) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public long idxFor(long pd) { + return DescriptorIndexBijection.INSTANCE.toIdx(pd); + } + + @Override + public long descriptorAt(long idx) { + return DescriptorIndexBijection.INSTANCE.toPd(idx); + } + }; + + // TODO (required): at the moment, we generate the values for clusterings by pre-generating a set number of values, which + // doesn't give us enough control over the possible values. What we need to do instead is to allow + // generating a set number of values _per column_. For example, if ck1 has 5 unique value, and ck2 has + // 5 unique values, for each ck1 we will have a value of ck2, so the number of possible values grows + // combinatorically. + int combinations = Math.toIntExact(rowPopulation.next(pd)); + HistoryBuilder.IndexedBijection ckGenerator = new InvertibleGenerator<>(rng, + cumulativeEntropy(schema.clusteringKeys), + combinations, + forKeys(schema.clusteringKeys), + keyComparator(schema.clusteringKeys)); + + return new ActivePartition(idx, + pd, + cachingPkGen, + rawPkGen, + ckGenerator, + schema.regularColumns.stream() + .map(map::get) + .collect(Collectors.toList()), + schema.staticColumns.stream() + .map(map::get) + .collect(Collectors.toList()), + pkComparators, + ckComparators, + regularComparators, + staticComparators, + cleanup); + } + + /** + * Tracks which partition indices have been visited. Maintains a contiguous range [minIdx, highIdxWatermark] + * and a min-heap of visited indices above the watermark. When indices added to the heap become consecutive + * with the watermark, the watermark is advanced. + * + * {@code getBySeed} picks a visited partition index uniformly at random using a deterministic seed, + * or returns -1 if no partitions have been visited. + */ + public static class VisitedPartitions + { + private final long minIdx; + private long highIdxWatermark; + private long[] sorted; + private int sortedSize; + + public VisitedPartitions(long minIdx) + { + Invariants.require(minIdx >= 0, "minIdx must be non-negative: %d", minIdx); + this.minIdx = minIdx; + this.highIdxWatermark = -1; + this.sorted = new long[16]; + this.sortedSize = 0; + } + + public synchronized void add(long idx) + { + Invariants.require(idx >= minIdx, "Partition index %d is below minimum %d", idx, minIdx); + + if (highIdxWatermark >= 0 && idx <= highIdxWatermark) + return; + + int pos = Arrays.binarySearch(sorted, 0, sortedSize, idx); + if (pos >= 0) + return; // already present + + int insertPos = -pos - 1; + if (sortedSize == sorted.length) + sorted = Arrays.copyOf(sorted, sorted.length * 2); + System.arraycopy(sorted, insertPos, sorted, insertPos + 1, sortedSize - insertPos); + sorted[insertPos] = idx; + sortedSize++; + + drain(); + } + + private void drain() + { + int removed = 0; + while (removed < sortedSize) + { + long top = sorted[removed]; + + if (highIdxWatermark == -1) + { + if (top == minIdx) + { + removed++; + highIdxWatermark = minIdx; + } + else + { + break; + } + } + else if (top == highIdxWatermark + 1) + { + removed++; + highIdxWatermark = top; + } + else if (top <= highIdxWatermark) + { + removed++; + } + else + { + break; + } + } + + if (removed > 0) + { + sortedSize -= removed; + System.arraycopy(sorted, removed, sorted, 0, sortedSize); + } + } + + private synchronized long size() + { + long contiguous = highIdxWatermark >= 0 ? (highIdxWatermark - minIdx + 1) : 0; + return contiguous + sortedSize; + } + + public synchronized long getBySeed(long seed) + { + long total = size(); + if (total == 0) + return -1; + + long chosen = SeedableEntropySource.computeWithSeed(seed, rng -> rng.nextLong(0, total)); + + long contiguous = highIdxWatermark >= 0 ? (highIdxWatermark - minIdx + 1) : 0; + if (chosen < contiguous) + return minIdx + chosen; + + int heapIdx = Math.toIntExact(chosen - contiguous); + return sorted[heapIdx]; + } + } + + public static class TrackerWrapper implements DataTracker + { + private final DataTracker delegate; + private final Partitions partitions; + + public TrackerWrapper(DataTracker delegate, Partitions partitions) + { + this.delegate = delegate; + this.partitions = partitions; + + } + + @Override + public void begin(Visit visit) + { + delegate.begin(visit); + } + + @Override + public void end(Visit visit) + { + delegate.end(visit); + // Referencing happens before handing over to the worker + for (long pd : visit.visitedPartitions) + partitions.forPd(pd).deref(); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/ExternalClusterSut.java b/test/harry/main/org/apache/cassandra/harry/stress/ExternalClusterSut.java new file mode 100644 index 000000000000..6d45998b0969 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/ExternalClusterSut.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.stress; + +import com.datastax.driver.core.*; +import org.apache.cassandra.db.ConsistencyLevel; + +import java.util.List; + +import com.google.common.util.concurrent.MoreExecutors; + +public class ExternalClusterSut +{ + private final Session session; + + public ExternalClusterSut(Session session) + { + this(session, 10); + } + + public ExternalClusterSut(Session session, int threads) + { + this.session = session; + } + + public Session session() + { + return session; + } + + public Metadata metadata() + { + return this.session.getCluster().getMetadata(); + } + + public static ExternalClusterSut create(ConsistencyLevel cl, int port, String... contactPoints) + { + // TODO: close Cluster and Session! + return new ExternalClusterSut(Cluster.builder() + .withQueryOptions(new QueryOptions().setConsistencyLevel(toDriverCl(cl))) + .addContactPoints(contactPoints) + .withPort(port) + .withCredentials("cassandra", "cassandra") + .build() + .connect()); + } + + public boolean isShutdown() + { + return session.isClosed(); + } + + public void shutdown() + { + session.close(); + } + + // TODO: this is rather simplistic + public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) + { + return resultSetToObjectArray(session.execute(statement, bindings)); + } + + private static final Object[][] EMPTY = new Object[0][]; + public Object[][] execute(SimpleStatement statement) + { + ResultSetFuture future = session.executeAsync(statement); + return resultSetToObjectArray(future.getUninterruptibly()); + } + + public Object[][] execute(SimpleStatement statement, Runnable callback) + { + ResultSetFuture future = session.executeAsync(statement); + future.addListener(callback, MoreExecutors.directExecutor()); + return resultSetToObjectArray(future.getUninterruptibly()); + } + + public static Object[][] resultSetToObjectArray(ResultSet rs) + { + List rows = rs.all(); + if (rows.size() == 0) + return new Object[0][]; + Object[][] results = new Object[rows.size()][]; + for (int i = 0; i < results.length; i++) + { + Row row = rows.get(i); + ColumnDefinitions cds = row.getColumnDefinitions(); + Object[] result = new Object[cds.size()]; + for (int j = 0; j < cds.size(); j++) + { + if (!row.isNull(j)) + result[j] = row.getObject(j); + } + results[i] = result; + } + return results; + } + + public static com.datastax.driver.core.ConsistencyLevel toDriverCl(ConsistencyLevel cl) + { + switch (cl) + { + case ONE: + return com.datastax.driver.core.ConsistencyLevel.ONE; + case ALL: + return com.datastax.driver.core.ConsistencyLevel.ALL; + case QUORUM: + return com.datastax.driver.core.ConsistencyLevel.QUORUM; + } + throw new IllegalArgumentException("Don't know a CL: " + cl); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/HarryStress.java b/test/harry/main/org/apache/cassandra/harry/stress/HarryStress.java new file mode 100644 index 000000000000..422100d17201 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/HarryStress.java @@ -0,0 +1,329 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.execution.LockingDataTracker; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.concurrent.Condition; + +import javax.annotation.Nullable; + +/** + * At any given point, we will have only N active partitions + * + * for every N partitions there should be its own invertible generator that will generate a partition key; + * however we need to somehow guarantee that later partition keys won't have same descriptors. + */ +public class HarryStress +{ + public static final Logger LOGGER = LoggerFactory.getLogger(HarryStress.class); + + // VisitGenerator is a stateful component that generates operation specs. Operation specs are then used by + private final VisitGenerator visitGenerator; + + private final ActivePartition.Partitions partitionFactory; + private final DataTracker.ReplayingDataTracker innerTracker; + + private final Generator visitTypeGen; + private final Distribution visitSizeDistribution; + private final VisitGenerator.OpKindGenFactory operationKindGen; + + private final List workers; + + private final EndCondition endCondition; + private final MetricCollector metrics = new MetricCollector(); + private final int ratePerSecond; + private final @Nullable PrintStream metricsOut; + private final int reportIntervalSeconds; + + public HarryStress(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + Generator visitTypeGen, + Distribution visitSizeDistribution, + VisitGenerator.OpKindGenFactory operationKindGen, + RotationStrategy rotationStrategy, + @Nullable PrintStream metricsOut, + int reportIntervalSeconds, + Supplier> sutFactory, + int concurrency, + int ratePerSecond, + long minPartitionIdx, + long maxPartitionIdx, + long initialLts) + { + this.ratePerSecond = ratePerSecond; + this.metricsOut = metricsOut; + this.reportIntervalSeconds = reportIntervalSeconds; + this.visitTypeGen = visitTypeGen; + this.visitSizeDistribution = visitSizeDistribution; + this.operationKindGen = operationKindGen; + this.partitionFactory = new ActivePartition.Partitions(schema, rowPopulation, columnPopulation, rotationStrategy, minPartitionIdx, maxPartitionIdx, initialLts); + this.visitGenerator = new VisitGenerator(partitionFactory, + visitTypeGen, + visitSizeDistribution, + operationKindGen, + initialLts); + + this.innerTracker = new DataTracker.SimpleDataTracker(); + + partitionFactory.onRemove(innerTracker::gc); + partitionFactory.populate(); + DataTracker outerTracker = new ActivePartition.TrackerWrapper(new LockingDataTracker(innerTracker, Integer.MAX_VALUE, 1), + partitionFactory); + + this.workers = new ArrayList<>(); + this.endCondition = new EndCondition(); + + for (int i = 0; i < concurrency; i++) + workers.add(new StressWorker(i, innerTracker, outerTracker, partitionFactory, sutFactory, endCondition, this.ratePerSecond / concurrency)); + + metrics.partitionCount.set(partitionFactory.activePartitions.size()); + metrics.activePartitionCount.set(partitionFactory.activePartitions.size()); + } + + private static class EndCondition implements Consumer + { + final List exceptions = new CopyOnWriteArrayList<>(); + final Condition condition = Condition.newOneTimeCondition(); + + @Override + public void accept(Throwable e) + { + exceptions.add(e); + condition.signal(); + } + + public boolean awaitUntil(long nanoTimeDeadline) throws InterruptedException + { + return condition.awaitUntil(nanoTimeDeadline); + } + + public Throwable maybeReportExceptions() + { + if (!exceptions.isEmpty()) + { + RuntimeException ex = new RuntimeException("Caught exception while running bechmark"); + for (Throwable exception : exceptions) + ex.addSuppressed(exception); + return ex; + } + return null; + } + } + + /** + * Replays the history [fromLts, toLts) into the model only (no SUT execution), so that reads over partitions whose + * data was loaded out-of-band (e.g. offline-generated SSTables produced from the same history) can be validated. + */ + public void replay(long fromLts, long toLts) + { + VisitGenerator seed = new VisitGenerator(partitionFactory, visitTypeGen, visitSizeDistribution, operationKindGen, fromLts); + for (long lts = fromLts; lts < toLts; lts++) + { + Visit visit = seed.get(); + innerTracker.begin(visit); + innerTracker.end(visit); + partitionFactory.maybeSwitchPartition(visit.lts, action -> {}); + } + } + + private Visit nextVisit() + { + Visit nextVisit = visitGenerator.get(); + for (long pd : nextVisit.visitedPartitions) + partitionFactory.forPd(pd).ref(); + return nextVisit; + } + + // TODO: warm up + public void start(long maxIterations, long runUntil) throws Throwable + { + long now = Clock.Global.nanoTime(); + long nextMetricsCollect = nextReport(now, reportIntervalSeconds); + + List metricOuts = new ArrayList<>(); + metricOuts.add(System.out); + if (metricsOut != null) + { + metricOuts.add(metricsOut); + metricsOut.println(HEAD); + } + + metrics.start(now); + Visit nextVisit = nextVisit(); + + while (true) + { + if (now > nextMetricsCollect) + { + // TODO (expected): this is potentially lossy, as worker may update metrics after we grab them + for (StressWorker worker : workers) + metrics.merge(worker.resetMetrics()); + + metrics.end(now); + System.out.println(HEAD); + printRow(metrics, metricOuts); + metrics.start(now); + nextMetricsCollect = nextReport(Clock.Global.nanoTime(), reportIntervalSeconds); + } + + for (StressWorker worker : workers) + { + int remaining = worker.getFreeSlots(); + while (remaining-- > 0 && worker.offer(nextVisit)) + { + nextVisit = nextVisit(); + partitionFactory.maybeSwitchPartition(nextVisit.lts, action -> {}); + } + } + + if (endCondition.awaitUntil(now + TimeUnit.SECONDS.toNanos(1))) + { + System.out.println("Exiting early due to condition"); + break; // errored out + } + + now = Clock.Global.nanoTime(); + if (nextVisit.lts >= maxIterations || now > runUntil) + { + break; + } + } + + System.out.printf("Completed! %d%n", nextVisit.lts); + + for (StressWorker worker : workers) + worker.shutdown(); + for (StressWorker worker : workers) + worker.awaitTermination(1, TimeUnit.MINUTES); + + Throwable t = endCondition.maybeReportExceptions(); + if (t != null) + throw t; + } + + private static long nextReport(long nowNanos, int reportIntervalSeconds) + { + Calendar calendar = Calendar.getInstance(); + long nowMillis = calendar.getTimeInMillis(); + int addSeconds = reportIntervalSeconds - (calendar.get(Calendar.SECOND) % reportIntervalSeconds); + calendar.add(Calendar.SECOND, addSeconds); + return nowNanos + TimeUnit.MILLISECONDS.toNanos(calendar.getTimeInMillis() - nowMillis); + } + + public static final String HEADFORMAT = "%19s %10s %8s %8s %7s %8s %8s %8s %8s %8s %8s %8s %8s %8s"; + public static final String ROWFORMAT = "%tF %tT " + // time + "%10d " + // counts + "%8.0f " + + "%8d " + + "%7s " + + "%8d " + + "%8d " + // count + "%8.0f " + // rates + "%8.1f " + // latency + "%8.1f " + + "%8.1f " + + "%8.1f " + + "%8.1f " + + "%8.1f"; + + public static final String[] HEADMETRICS = new String[]{ "time", "pcount", "pk/s", "pactive", "type", "count", "errors", "op/s","mean","med",".95",".99",".999","max"}; + public static final String HEAD = String.format(HEADFORMAT, (Object[]) HEADMETRICS); + + public static void main(String[] args) + { + System.out.println(HEAD); + printRow(new MetricCollector(), Collections.singletonList(System.out)); + long waitUntil = nextReport(Clock.Global.nanoTime(), 30); + while (true) + { + long wait = waitUntil - Clock.Global.nanoTime(); + if (wait <= 0) + break; + + Uninterruptibles.sleepUninterruptibly(wait, TimeUnit.NANOSECONDS); + } + System.out.printf("%tT\n", Calendar.getInstance()); + } + + private static void printRow(MetricCollector metrics, List outs) + { + Calendar calendar = Calendar.getInstance(); + String reads = String.format(ROWFORMAT, calendar, calendar, + metrics.partitionCount.get(), + metrics.partitionRate(), + metrics.activePartitionCount.get(), + "read", + metrics.readCount(), + metrics.failedReads.getAndSet(0), + metrics.readRate(), + metrics.meanReadLatencyMs(), + metrics.medianReadLatencyMs(), + metrics.readLatencyAtPercentileMs(95.0), + metrics.readLatencyAtPercentileMs(99.0), + metrics.readLatencyAtPercentileMs(99.9), + metrics.maxReadLatencyMs()); + + String writes = String.format(ROWFORMAT, calendar, calendar, + metrics.partitionCount.get(), + metrics.partitionRate(), + metrics.activePartitionCount.get(), + "write", + metrics.writeCount(), + metrics.failedWrites.getAndSet(0), + metrics.writeRate(), + metrics.meanWriteLatencyMs(), + metrics.medianWriteLatencyMs(), + metrics.writeLatencyAtPercentileMs(95.0), + metrics.writeLatencyAtPercentileMs(99.0), + metrics.writeLatencyAtPercentileMs(99.9), + metrics.maxWriteLatencyMs()); + + for (PrintStream out : outs) + { + out.println(reads); + out.println(writes); + out.flush(); + } + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/LevelledSStableGenerator.java b/test/harry/main/org/apache/cassandra/harry/stress/LevelledSStableGenerator.java new file mode 100644 index 000000000000..938e59797dfb --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/LevelledSStableGenerator.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.io.sstable.HarrySSTableWriter; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.service.ActiveRepairService; + +import static org.apache.cassandra.harry.stress.ActivePartition.createActivePartition; + +public class LevelledSStableGenerator +{ + private final SchemaSpec schema; + private final boolean disableCompression; + private final int sstableSizeMiB; + private final Distribution rowPopulation; + private final Distribution visitSizeDistribution; + private final VisitGenerator.OpKindGenFactory opKindGen; + private final VisitGenerator.ColumnPopulation columnPopulation; + private final ActivePartition.PartitionKeyGen pkGen; + private final TokenIndex tokenIndex; + private final SSTableLevelPicker levelPicker; + private final File directory; + private final long maxPartitions; + private final long repairedAtMillis; + + public LevelledSStableGenerator(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + Distribution visitSizeDistribution, + VisitGenerator.OpKindGenFactory operationKindGen, + boolean disableCompression, + int sstableSizeMiB, + SSTableLevelPicker levelPicker, + TokenIndex tokenIndex, + File directory) + { + this(schema, rowPopulation, columnPopulation, visitSizeDistribution, operationKindGen, + disableCompression, sstableSizeMiB, levelPicker, tokenIndex, directory, Long.MAX_VALUE, ActiveRepairService.UNREPAIRED_SSTABLE); + } + + public LevelledSStableGenerator(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + Distribution visitSizeDistribution, + VisitGenerator.OpKindGenFactory operationKindGen, + boolean disableCompression, + int sstableSizeMiB, + SSTableLevelPicker levelPicker, + TokenIndex tokenIndex, + File directory, + long maxPartitions) + { + this(schema, rowPopulation, columnPopulation, visitSizeDistribution, operationKindGen, + disableCompression, sstableSizeMiB, levelPicker, tokenIndex, directory, maxPartitions, ActiveRepairService.UNREPAIRED_SSTABLE); + } + + public LevelledSStableGenerator(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + Distribution visitSizeDistribution, + VisitGenerator.OpKindGenFactory operationKindGen, + boolean disableCompression, + int sstableSizeMiB, + SSTableLevelPicker levelPicker, + TokenIndex tokenIndex, + File directory, + long maxPartitions, + long repairedAtMillis) + { + this.schema = schema; + this.rowPopulation = rowPopulation; + this.columnPopulation = columnPopulation; + this.visitSizeDistribution = visitSizeDistribution; + this.opKindGen = operationKindGen; + this.disableCompression = disableCompression; + this.sstableSizeMiB = sstableSizeMiB; + this.levelPicker = levelPicker; + this.tokenIndex = tokenIndex; + this.directory = directory; + this.maxPartitions = maxPartitions; + this.repairedAtMillis = repairedAtMillis; + this.pkGen = new ActivePartition.PartitionKeyGen(schema); + } + + public void generate(long minToken, long maxToken) + { + TokenIndex.EntryIterator iter = tokenIndex.range(minToken, maxToken); + HarrySSTableWriter[] writers = new HarrySSTableWriter[levelPicker.size()]; + CQLVisitExecutor[] executors = new CQLVisitExecutor[levelPicker.size()]; + // This is a bit of a hack: we do not create full blown Partitions, since we don't + // pick partitions in the same way we were picking them during "normal" generation. + CurrentPartition currentPartition = new CurrentPartition(pkGen); + for (int i = 0; i < writers.length; i++) + { + writers[i] = SSTableGenerator.newWriter(schema, disableCompression, sstableSizeMiB, directory, i, repairedAtMillis, true); + executors[i] = SSTableGenerator.createExecutor(schema, currentPartition, writers[i], null); + } + long counter = 0; + long lastChecking = System.currentTimeMillis(); + while (iter.hasNext() && counter < maxPartitions) + { + counter++; + if (counter % 1000 == 0) + { + long now = System.currentTimeMillis(); + System.out.println("Processed " + counter + " partitions " + (now - lastChecking) + "ms elapsed"); + lastChecking = now; + } + long pd = iter.pd(); + long[] ltss = iter.readLts(); + ActivePartition partition = byPd(pd); + currentPartition.current = partition; + for (long lts : ltss) + { + Visit visit = VisitGenerator.mutatingVisit(lts, visitSizeDistribution, opKindGen, rng -> partition); + CQLVisitExecutor executor = executors[levelPicker.pick(lts)]; + executor.execute(visit); + } + currentPartition.current = null; + pkGen.cleanup(pd); + iter.advance(); + } + + for (int i = 0; i < writers.length; i++) + { + try + { + writers[i].close(); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + } + + public static class SSTableLevelPicker + { + final long[] cdf; + final long total; + + public SSTableLevelPicker(int... weights) + { + cdf = new long[weights.length]; + cdf[0] = weights[0]; + for (int i = 1; i < weights.length; i++) + cdf[i] = cdf[i - 1] + weights[i]; + total = cdf[cdf.length - 1]; + } + + public int size() + { + return cdf.length; + } + + public int pick(long lts) + { + EntropySource rng = SeedableEntropySource.entropySource(lts, 1); + long val = rng.nextLong(0, total); + for (int i = 0; i < cdf.length; i++) + { + if (val < cdf[i]) + return i; + } + return cdf.length - 1; + } + } + + private ActivePartition byPd(long pd) + { + long idx = ActivePartition.DescriptorIndexBijection.INSTANCE.toIdx(pd); + ActivePartition partition = createActivePartition(idx, pd, schema, rowPopulation, columnPopulation, pkGen, (pd_) -> {}); + pkGen.ensure(pd, d -> SeedableEntropySource.computeWithSeed(d, SchemaSpec.forKeys(schema.partitionKeys)::generate)); + return partition; + } + + static class CurrentPartition extends ValueGenerators + { + ActivePartition current; + + CurrentPartition(HistoryBuilder.IndexedBijection pkGen) + { + super(pkGen); + } + + @Override + public ActivePartition forPd(long pd) + { + if (current == null || current.pd != pd) + throw new IllegalStateException("No ActivePartition for pd=" + pd + "; expected pd=" + (current != null ? current.pd : "null")); + return current; + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/MetricCollector.java b/test/harry/main/org/apache/cassandra/harry/stress/MetricCollector.java new file mode 100644 index 000000000000..8305edb9f10a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/MetricCollector.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.util.concurrent.atomic.AtomicLong; + +import org.HdrHistogram.Histogram; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MetricCollector +{ + public static final Logger LOGGER = LoggerFactory.getLogger(MetricCollector.class); + private final Histogram reads = new Histogram(3); + private final Histogram writes = new Histogram(3); + + // nanos + private long startNs = Long.MAX_VALUE; + private long endNs = Long.MIN_VALUE; + + // discrete + public final AtomicLong activePartitionCount = new AtomicLong(); + public final AtomicLong partitionCount = new AtomicLong(); + public final AtomicLong failedReads = new AtomicLong(); + public final AtomicLong failedWrites = new AtomicLong(); + + public void merge(StressWorker.WorkerMetrics metrics) + { + try + { + reads.add(metrics.reads); + writes.add(metrics.writes); + failedReads.addAndGet(metrics.failedReads.get()); + failedWrites.addAndGet(metrics.failedWrites.get()); + } + catch (Throwable t) + { + LOGGER.error("Could not merge histograms, reported values will be unreliable", t); + + } + } + + public synchronized double readRate() + { + return readCount() / ((endNs - startNs) * 0.000000001d); + } + + public synchronized double writeRate() + { + return writeCount() / ((endNs - startNs) * 0.000000001d); + } + + public synchronized double partitionRate() + { + return partitionCount.get() / ((endNs - startNs) * 0.000000001d); + } + + public synchronized double meanReadLatencyMs() + { + return readLatencies().getMean() * 0.000001d; + } + + public synchronized double maxReadLatencyMs() + { + return readLatencies().getMaxValue() * 0.000001d; + } + + public synchronized double medianReadLatencyMs() + { + return readLatencies().getValueAtPercentile(50.0) * 0.000001d; + } + + public synchronized double meanWriteLatencyMs() + { + return writeLatencies().getMean() * 0.000001d; + } + + public synchronized double maxWriteLatencyMs() + { + return writeLatencies().getMaxValue() * 0.000001d; + } + + public synchronized double medianWriteLatencyMs() + { + return writeLatencies().getValueAtPercentile(50.0) * 0.000001d; + } + + + /** + * @param percentile between 0.0 and 100.0 + * @return latency in milliseconds at percentile + */ + public synchronized double readLatencyAtPercentileMs(double percentile) + { + return readLatencies().getValueAtPercentile(percentile) * 0.000001d; + } + + public synchronized double writeLatencyAtPercentileMs(double percentile) + { + return writeLatencies().getValueAtPercentile(percentile) * 0.000001d; + } + + public synchronized long runTimeMs() + { + return (endNs - startNs) / 1000000; + } + + public long end() + { + return endNs; + } + + public long start() + { + return startNs; + } + + private Histogram readLatencies() + { + return reads; + } + + private Histogram writeLatencies() + { + return writes; + } + + public synchronized long readCount() + { + return readLatencies().getTotalCount() + failedReads.get(); + } + + public synchronized long writeCount() + { + return writeLatencies().getTotalCount() + failedWrites.get(); + } + + + public void start(long started) + { + this.startNs = started; + readLatencies().reset(); + writeLatencies().reset(); + } + + public void end(long ended) + { + this.endNs = ended; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/RangeAwareSSTableGenerator.java b/test/harry/main/org/apache/cassandra/harry/stress/RangeAwareSSTableGenerator.java new file mode 100644 index 000000000000..a05f925dcd5b --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/RangeAwareSSTableGenerator.java @@ -0,0 +1,413 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.QueryOptions; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.stress.config.StressSchemaConfig; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.harry.stress.distribution.Distributions; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.tcm.ClusterMetadataService; + +/** + * Offline range-aware SSTable generator for a live (e.g. CCM) cluster. + * + *

Discovers token ranges and their replicas from {@code system_views.data_placements} + + * {@code system_views.cluster_metadata_directory}, then generates one set of Harry-deterministic + * SSTables per range into a separate directory. Each output directory follows the layout the + * {@code nodetool import} parser expects: + * + *

{@code
+ *   /range__//-<32-hex-id>/*.db
+ * }
+ *
+ * 

The tool does NOT push the SSTables onto the cluster — it only generates them locally and + * prints a per-node import plan listing which directory should be loaded on which node. The user + * is responsible for copying the directories to each node and running {@code nodetool import}. + * + *

This is the offline / CLI sibling of {@code RangeAwareSSTableLoadAndStressTest}; the + * single-node analogue is {@code LevelledSSTableGeneratorTest}. + */ +public class RangeAwareSSTableGenerator +{ + public static void main(String... args) throws IOException + { + Args parsed; + try + { + parsed = Args.parse(args); + } + catch (Args.HelpRequested e) + { + return; + } + + System.out.println("Parsed args: " + parsed); + + // Init order matters: tool mode must be enabled before any Schema.* access (else + // local-system keyspaces are not loaded and HarrySSTableWriter.build() will fail). + // Murmur3 must match the cluster (Murmur3 is Cassandra's default). + DatabaseDescriptor.toolInitialization(false); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ClusterMetadataService.initializeForClients(); + + StressSchemaConfig config = StressSchemaConfig.load(Paths.get(parsed.schemaPath)); + SchemaSpec schema = config.schema(); + + Cluster.Builder clusterBuilder = Cluster.builder() + .addContactPoints(parsed.contactPoints) + .withPort(parsed.port) + .withQueryOptions(new QueryOptions().setConsistencyLevel(com.datastax.driver.core.ConsistencyLevel.ONE)); + if (parsed.username != null) + clusterBuilder.withCredentials(parsed.username, parsed.password == null ? "" : parsed.password); + + try (Cluster cluster = clusterBuilder.build(); + Session session = cluster.connect()) + { + session.execute("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + session.execute(schema.compile()); + + Map nodeIdToEndpoint = readNodeDirectory(session); + List ranges = readPlacements(session, schema.keyspace, schema.table, nodeIdToEndpoint); + if (ranges.isEmpty()) + throw new IllegalStateException("No placement rows found for " + schema.keyspace + "." + schema.table + + ". Make sure the keyspace+table already exist on the cluster."); + + System.out.println("Discovered " + ranges.size() + " ranges for " + schema.keyspace + "." + schema.table + ":"); + for (RangeReplicas r : ranges) + System.out.println(String.format(" (%d, %d] -> %s", r.startToken, r.endToken, r.endpoints)); + System.out.println(); + + // 2. Build the global token index (same parameters as RangeAwareSSTableLoadAndStressTest). + // A single index is used for all ranges; per-range generation just slices it by token. + Distribution visitSize = Distributions.fixed(1); + VisitGenerator.OpKindGenFactory opKindGen = new VisitGenerator.RandomOpKindGenFactory(); + Generator visitTypeGen = Generators.constant(VisitGenerator.VisitType.MUTATE); + + Path outputRoot = Paths.get(parsed.outputDir).toAbsolutePath(); + Files.createDirectories(outputRoot); + File tokenDir = new File(Files.createDirectories(outputRoot.resolve("_tokens"))); + System.out.println("Generating token index in " + tokenDir + " (initialLts=" + parsed.initialLts + + ", visits=" + parsed.visits + ")"); + TokenIndexGenerator.generate(tokenDir.toJavaIOFile(), schema, config.rotationStrategy(), + config.rowPopulation(), visitTypeGen, + visitSize, parsed.initialLts, parsed.visits); + + // 3. For each range, generate a set of levelled SSTables under its own directory. + Map> importPlan = new LinkedHashMap<>(); + try (TokenIndex tokenIndex = new TokenIndex(new File(tokenDir, "merged_tokens"), + new File(tokenDir, "merged_tokens.idx"))) + { + for (RangeReplicas range : ranges) + { + // Directory layout the nodetool importer expects: /

-<32hex>/. The 32-hex id + // is a placeholder — the importer rehomes the SSTables to the live table on load. + String rangeDirName = "range_" + range.startToken + "_" + range.endToken; + File sstableDir = new File(Files.createDirectories(outputRoot.resolve(rangeDirName) + .resolve(schema.keyspace) + .resolve(schema.table + '-' + "0".repeat(32)))); + + System.out.println(); + System.out.println("Range (" + range.startToken + ", " + range.endToken + "]"); + System.out.println(" Endpoints: " + range.endpoints); + System.out.println(" Output: " + sstableDir.absolutePath()); + + LevelledSStableGenerator generator = new LevelledSStableGenerator(schema, config.rowPopulation(), config.columnPopulation(), + visitSize, opKindGen, parsed.disableCompression, parsed.sstableSizeMiB, + new LevelledSStableGenerator.SSTableLevelPicker(parsed.levelWeights), + tokenIndex, sstableDir); + // Wraparound: a range whose start > end straddles MIN/MAX. Generate it as two slices. + if (range.startToken <= range.endToken) + { + generator.generate(range.startToken, range.endToken); + } + else + { + generator.generate(Long.MIN_VALUE, range.endToken); + generator.generate(range.startToken, Long.MAX_VALUE); + } + + for (String endpoint : range.endpoints) + importPlan.computeIfAbsent(endpoint, k -> new ArrayList<>()).add(sstableDir.absolutePath()); + } + } + + // 4. Print per-node import plan. + System.out.println(); + System.out.println("=================== IMPORT PLAN ==================="); + System.out.println("For each node below, copy the listed directories to the node and run:"); + System.out.println(" nodetool import -l -e " + schema.keyspace + ' ' + schema.table + " "); + System.out.println("(-l keeps generated LCS levels; -e runs extended verification.)"); + System.out.println(); + for (Map.Entry> e : importPlan.entrySet()) + { + System.out.println("Node " + e.getKey() + ':'); + for (String dir : e.getValue()) + System.out.println(" " + dir); + } + } + } + + private static Map readNodeDirectory(Session session) + { + // Map TCM node_id -> ":" (the address the user uses as a contact point). + Map result = new HashMap<>(); + for (Row r : session.execute("SELECT node_id, native_address, native_port FROM system_views.cluster_metadata_directory").all()) + { + int nodeId = r.getInt("node_id"); + String addr = r.getInet("native_address").getHostAddress(); + int port = r.getInt("native_port"); + result.put(nodeId, addr + ':' + port); + } + return result; + } + + @SuppressWarnings("unchecked") + private static List readPlacements(Session session, String keyspace, String table, + Map nodeIdToEndpoint) + { + // 2-component partition-key prefix returns every range for the table. write_replicas is the + // set of RF TCM NodeId.id() values for each range. + List rows = session.execute( + "SELECT range_start, range_end, write_replicas FROM system_views.data_placements " + + "WHERE keyspace_name = ? AND table_name = ?", + keyspace, table).all(); + List result = new ArrayList<>(); + for (Row row : rows) + { + long startToken = Long.parseLong(row.getString("range_start")); + long endToken = Long.parseLong(row.getString("range_end")); + Set nodeIds = row.getSet("write_replicas", Integer.class); + Set endpoints = new TreeSet<>(); + for (Integer id : nodeIds) + { + String endpoint = nodeIdToEndpoint.get(id); + if (endpoint == null) + throw new IllegalStateException("No directory entry for node_id=" + id); + endpoints.add(endpoint); + } + result.add(new RangeReplicas(startToken, endToken, endpoints)); + } + return result; + } + + private static final class RangeReplicas + { + final long startToken; + final long endToken; + final Set endpoints; + + RangeReplicas(long startToken, long endToken, Set endpoints) + { + this.startToken = startToken; + this.endToken = endToken; + this.endpoints = endpoints; + } + } + + private static final class Args + { + final String[] contactPoints; + final int port; + final String username; + final String password; + final String schemaPath; + final String outputDir; + final long initialLts; + final long visits; + final int sstableSizeMiB; + final int[] levelWeights; + final boolean disableCompression; + + Args(String[] contactPoints, int port, String username, String password, + String schemaPath, String outputDir, + long initialLts, long visits, int sstableSizeMiB, int[] levelWeights, boolean disableCompression) + { + this.contactPoints = contactPoints; + this.port = port; + this.username = username; + this.password = password; + this.schemaPath = schemaPath; + this.outputDir = outputDir; + this.initialLts = initialLts; + this.visits = visits; + this.sstableSizeMiB = sstableSizeMiB; + this.levelWeights = levelWeights; + this.disableCompression = disableCompression; + } + + static Args parse(String[] args) + { + String[] contactPoints = null; + int port = 9042; + String username = null; + String password = null; + String schemaPath = null; + String outputDir = null; + long initialLts = 1; + long visits = 100_000; + int sstableSizeMiB = 64; + int[] levelWeights = { 1, 2, 4, 8, 16 }; + boolean disableCompression = true; + + for (int i = 0; i < args.length; i++) + { + String a = args[i]; + switch (a) + { + case "-h": + case "--help": + printHelp(); + throw new HelpRequested(); + case "--contact-points": + contactPoints = require(args, ++i, a).split(","); + break; + case "--port": + port = Integer.parseInt(require(args, ++i, a)); + break; + case "--username": + username = require(args, ++i, a); + break; + case "--password": + password = require(args, ++i, a); + break; + case "--schema": + schemaPath = require(args, ++i, a); + break; + case "--output-dir": + outputDir = require(args, ++i, a); + break; + case "--initial-lts": + initialLts = Long.parseLong(require(args, ++i, a)); + break; + case "--visits": + visits = Long.parseLong(require(args, ++i, a)); + break; + case "--sstable-size-mib": + sstableSizeMiB = Integer.parseInt(require(args, ++i, a)); + break; + case "--levels": + { + String[] parts = require(args, ++i, a).split(","); + levelWeights = new int[parts.length]; + for (int j = 0; j < parts.length; j++) + levelWeights[j] = Integer.parseInt(parts[j].trim()); + break; + } + case "--disable-compression": + disableCompression = true; + break; + default: + printHelp(); + throw new IllegalArgumentException("Unknown argument: " + a); + } + } + + if (contactPoints == null || schemaPath == null || outputDir == null) + { + printHelp(); + throw new IllegalArgumentException("--contact-points, --schema, and --output-dir are required"); + } + return new Args(contactPoints, port, username, password, schemaPath, outputDir, + initialLts, visits, sstableSizeMiB, levelWeights, disableCompression); + } + + private static String require(String[] args, int idx, String flag) + { + if (idx >= args.length) + throw new IllegalArgumentException("Missing value for " + flag); + return args[idx]; + } + + private static void printHelp() + { + System.out.println("Usage: RangeAwareSSTableGenerator [options]"); + System.out.println(); + System.out.println("Required:"); + System.out.println(" --contact-points host1,host2,... Comma-separated CCM contact points"); + System.out.println(" --schema YAML schema config (keyspace, table, columnspec, rotation; same"); + System.out.println(" format StressSchemaConfig consumes). The keyspace+table MUST"); + System.out.println(" already exist on the cluster and match this definition."); + System.out.println(" --output-dir Directory to write per-range SSTable directories into"); + System.out.println(); + System.out.println("Optional:"); + System.out.println(" --port

Native protocol port (default 9042)"); + System.out.println(" --username Cassandra username"); + System.out.println(" --password

Cassandra password"); + System.out.println(" --initial-lts First LTS to generate from (default 1)"); + System.out.println(" --visits Total writes/visits to generate (default 100000)"); + System.out.println(" --sstable-size-mib Max SSTable size in MiB before rolling (default 1)"); + System.out.println(" --levels LCS level weights (default 1,2,4,8,16; index = level)"); + System.out.println(" --disable-compression Disable SSTable compression"); + System.out.println(); + System.out.println("Output layout (one subdirectory per token range):"); + System.out.println(" /range__//

-<32 zeros>/*.db"); + System.out.println(); + System.out.println("After it finishes, the tool prints which range directories should be imported into which"); + System.out.println("node. Copy each directory to its target node and run:"); + System.out.println(" nodetool import -l -e
"); + } + + static final class HelpRequested extends RuntimeException + { + HelpRequested() { super(); } + } + + @Override + public String toString() + { + return "Args{" + + "contactPoints=" + Arrays.toString(contactPoints) + + ", port=" + port + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + ", schemaPath='" + schemaPath + '\'' + + ", outputDir='" + outputDir + '\'' + + ", initialLts=" + initialLts + + ", visits=" + visits + + ", sstableSizeMiB=" + sstableSizeMiB + + ", levelWeights=" + Arrays.toString(levelWeights) + + ", disableCompression=" + disableCompression + + '}'; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/RangeAwareSSTableGeneratorTest.java b/test/harry/main/org/apache/cassandra/harry/stress/RangeAwareSSTableGeneratorTest.java new file mode 100644 index 000000000000..5fc7e00fcb04 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/RangeAwareSSTableGeneratorTest.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import org.junit.Test; + +public class RangeAwareSSTableGeneratorTest +{ + @Test + public void test() throws Throwable + { + RangeAwareSSTableGenerator.main("--contact-points", "127.0.0.1", + "--schema", "test/resources/harry/stress/levelled-sstable-schema.yaml", + "--output-dir", "/tmp/range-sstables", + "--visits", "100000000", + "--sstable-size-mib", "6400"); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/ReplicaLocator.java b/test/harry/main/org/apache/cassandra/harry/stress/ReplicaLocator.java new file mode 100644 index 000000000000..c62384ef7638 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/ReplicaLocator.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.ColumnMetadata; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.Host; +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.TypeCodec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; + +import static org.apache.cassandra.harry.SchemaSpec.forKeys; +import static org.apache.cassandra.harry.util.ByteUtils.putShortLength; + +public class ReplicaLocator +{ + @SuppressWarnings("unchecked") + public static Set getReplicaHosts(Session session, String keyspace, String table, Object[] pk) + { + Metadata metadata = session.getCluster().getMetadata(); + CodecRegistry codecRegistry = session.getCluster().getConfiguration().getCodecRegistry(); + ProtocolVersion protocolVersion = session.getCluster().getConfiguration() + .getProtocolOptions().getProtocolVersion(); + + KeyspaceMetadata ksm = metadata.getKeyspace(keyspace); + if (ksm == null) + throw new IllegalArgumentException(String.format("Keyspace not found: %s", keyspace)); + + TableMetadata tm = ksm.getTable(table); + if (tm == null) + throw new IllegalArgumentException(String.format("Table not found: %s.%s", keyspace, table)); + + List pkColumns = tm.getPartitionKey(); + if (pk.length != pkColumns.size()) + throw new IllegalArgumentException(String.format("Expected %d partition key values but got %d", + pkColumns.size(), pk.length)); + + ByteBuffer[] components = new ByteBuffer[pk.length]; + for (int i = 0; i < pk.length; i++) + { + DataType type = pkColumns.get(i).getType(); + TypeCodec codec = codecRegistry.codecFor(type); + components[i] = codec.serialize(pk[i], protocolVersion); + } + + ByteBuffer routingKey = compose(components); + + return metadata.getReplicas(keyspace, routingKey); + } + + static ByteBuffer compose(ByteBuffer... buffers) { + if (buffers.length == 1) { + return buffers[0]; + } else { + int totalLength = 0; + + for(ByteBuffer bb : buffers) { + totalLength += 2 + bb.remaining() + 1; + } + + ByteBuffer out = ByteBuffer.allocate(totalLength); + + for(ByteBuffer buffer : buffers) { + ByteBuffer bb = buffer.duplicate(); + putShortLength(out, bb.remaining()); + out.put(bb); + out.put((byte)0); + } + + out.flip(); + return out; + } + } + + public static List getReplicas(Session session, SchemaSpec schema, long pd) + { + Object[] pk = SeedableEntropySource.computeWithSeed(pd, forKeys(schema.partitionKeys)::generate); + return getReplicas(session, schema.keyspace, schema.table, pk); + + } + public static List getReplicas(Session session, String keyspace, String table, Object[] pk) + { + Set replicas = getReplicaHosts(session, keyspace, table, pk); + List addresses = new ArrayList<>(replicas.size()); + for (Host host : replicas) + addresses.add(host.getBroadcastSocketAddress()); + return addresses; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/RotationStrategy.java b/test/harry/main/org/apache/cassandra/harry/stress/RotationStrategy.java new file mode 100644 index 000000000000..81d093bb25af --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/RotationStrategy.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.util.Arrays; + +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; + +/** + * Rotation strategy is a way to control a number of active partitions. + * + * Rotation strategy has a target size, but will vary the number of partition to maintain it within a distribution over time. + */ +public interface RotationStrategy extends Generator +{ + int targetSize(); + + /** + * Determines whether a partition switch should occur at the given logical timestamp. + * Implementations track the last LTS at which a switch occurred and compare against + * the configured interval. + */ + boolean shouldSwitch(long lts); + + // TODO (required): control a total number of partitions + enum PartitionAction + { + REPLACE_WITH_NEW, // Replace partition: rotate current one out, and pick a new partition in its place + REPLACE_WITH_VISITED, // Replace partition: rotate current one out, and pick an already visited partition in its place + } + + /** + * A trivial random rotation strategy, would attempt to keep the size close to the target, but + * has no bias or gives any guarantees. + */ + class RandomRotationStrategy implements RotationStrategy + { + private static final Generator gen = Generators.pick(PartitionAction.REPLACE_WITH_VISITED, PartitionAction.REPLACE_WITH_NEW); + private final PartitionAction[] EMPTY = new PartitionAction[] {}; + private final int targetSize; + private final int switchInterval; + private long lastSwitchLts = -1; + + public RandomRotationStrategy(int targetSize) + { + this(targetSize, 500); + } + + public RandomRotationStrategy(int targetSize, int switchInterval) + { + this.targetSize = targetSize; + this.switchInterval = switchInterval; + } + + @Override + public int targetSize() + { + return targetSize; + } + + @Override + public boolean shouldSwitch(long lts) + { + if (lastSwitchLts < 0 || lts - lastSwitchLts >= switchInterval) + { + lastSwitchLts = lts; + return true; + } + return false; + } + + @Override + public PartitionAction[] generate(EntropySource rng) + { + // TODO (required): make configurable +// if (rng.nextBoolean()) +// return EMPTY; + + PartitionAction[] actions = new PartitionAction[rng.nextInt(5, 10)]; + for (int i = 0; i < actions.length; i++) + actions[i] = gen.generate(rng); + return actions; + } + + @Override + public String toString() + { + return String.format("random(target=%d, switchInterval=%d)", targetSize, switchInterval); + } + } + + class FixedRotationStrategy implements RotationStrategy + { + private final int replaceWithNew; + private final int replaceWithVisited; + private final int targetSize; + private final int switchInterval; + private long lastSwitchLts = -1; + + public FixedRotationStrategy(int targetSize, int replaceWithNew, int replaceWithVisited) + { + this(targetSize, replaceWithNew, replaceWithVisited, 500); + } + + public FixedRotationStrategy(int targetSize, int replaceWithNew, int replaceWithVisited, int switchInterval) + { + this.replaceWithNew = replaceWithNew; + this.replaceWithVisited = replaceWithVisited; + this.targetSize = targetSize; + this.switchInterval = switchInterval; + } + + @Override + public int targetSize() + { + return targetSize; + } + + @Override + public boolean shouldSwitch(long lts) + { + if (lastSwitchLts < 0 || lts - lastSwitchLts >= switchInterval) + { + lastSwitchLts = lts; + return true; + } + return false; + } + + @Override + public PartitionAction[] generate(EntropySource rng) + { + PartitionAction[] actions = new PartitionAction[replaceWithNew + replaceWithVisited]; + Arrays.fill(actions, 0, replaceWithNew, PartitionAction.REPLACE_WITH_NEW); + Arrays.fill(actions, replaceWithNew, replaceWithNew + replaceWithVisited, PartitionAction.REPLACE_WITH_VISITED); + return actions; + } + + @Override + public String toString() + { + return String.format("fixed(target=%d, replaceWithNew=%d, replaceWithVisited=%d, switchInterval=%d)", + targetSize, replaceWithNew, replaceWithVisited, switchInterval); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/SSTableGenerator.java b/test/harry/main/org/apache/cassandra/harry/stress/SSTableGenerator.java new file mode 100644 index 000000000000..527d8d4566cb --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/SSTableGenerator.java @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.harry.stress; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.function.Consumer; + +import com.google.common.io.Files; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +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.execution.ResultSetRow; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.ValueGenerators; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.io.sstable.HarrySSTableWriter; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * Generates SSTables using the same visit generation machinery as {@link HarryStress}. + * Visits are compiled into CQL statements and fed into {@link HarrySSTableWriter} to produce + * SSTables on disk. The writer automatically flushes to a new SSTable when the configured + * size threshold is reached. + */ +public class SSTableGenerator +{ + public static final Logger LOGGER = LoggerFactory.getLogger(SSTableGenerator.class); + + private final SchemaSpec schema; + private final VisitGenerator visitGenerator; + private final ActivePartition.Partitions partitionFactory; + private final boolean disableCompression; + private final int sstableSizeMiB; + private final int sstableLevel; + private final long repairedAtMillis; + + public SSTableGenerator(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + Generator visitTypeGen, + Distribution visitSizeDistribution, + VisitGenerator.OpKindGenFactory operationKindGen, + RotationStrategy rotationStrategy, + boolean disableCompression, + int sstableSizeMiB, + long minPartitionIdx, + long maxPartitionIdx, + long initialLts) + { + this(schema, rowPopulation, columnPopulation, visitTypeGen, visitSizeDistribution, + operationKindGen, rotationStrategy, disableCompression, sstableSizeMiB, + minPartitionIdx, maxPartitionIdx, initialLts, 0, ActiveRepairService.UNREPAIRED_SSTABLE); + } + + public SSTableGenerator(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + Generator visitTypeGen, + Distribution visitSizeDistribution, + VisitGenerator.OpKindGenFactory operationKindGen, + RotationStrategy rotationStrategy, + boolean disableCompression, + int sstableSizeMiB, + long minPartitionIdx, + long maxPartitionIdx, + long initialLts, + int sstableLevel) + { + this(schema, rowPopulation, columnPopulation, visitTypeGen, visitSizeDistribution, + operationKindGen, rotationStrategy, disableCompression, sstableSizeMiB, + minPartitionIdx, maxPartitionIdx, initialLts, sstableLevel, ActiveRepairService.UNREPAIRED_SSTABLE); + } + + public SSTableGenerator(SchemaSpec schema, + Distribution rowPopulation, + VisitGenerator.ColumnPopulation columnPopulation, + Generator visitTypeGen, + Distribution visitSizeDistribution, + VisitGenerator.OpKindGenFactory operationKindGen, + RotationStrategy rotationStrategy, + boolean disableCompression, + int sstableSizeMiB, + long minPartitionIdx, + long maxPartitionIdx, + long initialLts, + int sstableLevel, + long repairedAtMillis) + { + this.schema = schema; + this.disableCompression = disableCompression; + this.sstableSizeMiB = sstableSizeMiB; + this.sstableLevel = sstableLevel; + this.repairedAtMillis = repairedAtMillis; + this.partitionFactory = new ActivePartition.Partitions(schema, rowPopulation, columnPopulation, rotationStrategy, minPartitionIdx, maxPartitionIdx, initialLts); + this.visitGenerator = new VisitGenerator(partitionFactory, + visitTypeGen, + visitSizeDistribution, + operationKindGen, + initialLts); + partitionFactory.populate(); + } + + public void generate(File directory, long totalVisits, java.io.File progressFile) + { + generate(directory, totalVisits, visit -> {}, progressFile); + } + + public void generate(File directory, long totalVisits, Consumer onVisit, java.io.File progressFile) + { + HarrySSTableWriter writer = newWriter(directory); + CQLVisitExecutor executor = createExecutor(writer, progressFile); + + for (long i = 0; i < totalVisits; i++) + { + Visit visit = visitGenerator.get(); + + for (long pd : visit.visitedPartitions) + partitionFactory.forPd(pd).ref(); + + try + { + if (!visit.validating()) + executor.execute(visit); + + onVisit.accept(visit); + + partitionFactory.maybeSwitchPartition(visit.lts, action -> {}); + } + finally + { + for (long pd : visit.visitedPartitions) + partitionFactory.forPd(pd).deref(); + } + } + + closeWriter(writer); + } + + private HarrySSTableWriter newWriter(File directory) + { + return newWriter(directory, sstableLevel, repairedAtMillis); + } + + private HarrySSTableWriter newWriter(File directory, int level) + { + return newWriter(schema, disableCompression, sstableSizeMiB, directory, level, ActiveRepairService.UNREPAIRED_SSTABLE); + } + + private HarrySSTableWriter newWriter(File directory, int level, long repairedAtMillis) + { + return newWriter(schema, disableCompression, sstableSizeMiB, directory, level, repairedAtMillis); + } + + public static HarrySSTableWriter newWriter(SchemaSpec schema, boolean disableCompression, int sstableSizeMiB, File directory, int level) + { + return newWriter(schema, disableCompression, sstableSizeMiB, directory, level, ActiveRepairService.UNREPAIRED_SSTABLE); + } + + public static HarrySSTableWriter newWriter(SchemaSpec schema, boolean disableCompression, int sstableSizeMiB, File directory, int level, long repairedAtMillis) + { + return newWriter(schema, disableCompression, sstableSizeMiB, directory, level, repairedAtMillis, false); + } + + public static HarrySSTableWriter newWriter(SchemaSpec schema, boolean disableCompression, int sstableSizeMiB, File directory, int level, long repairedAtMillis, boolean sorted) + { + try + { + String tableCql = schema.compile(); + if (disableCompression) + { + String noSemicolon = tableCql.endsWith(";") ? tableCql.substring(0, tableCql.length() - 1) : tableCql; + if (noSemicolon.contains(" WITH ")) + tableCql = noSemicolon + " AND compression = {'enabled': 'false'};"; + else + tableCql = noSemicolon + " WITH compression = {'enabled': 'false'};"; + } + HarrySSTableWriter.Builder builder = HarrySSTableWriter.builder() + .forTable(tableCql) + .inDirectory(directory) + .withMaxSSTableSizeInMiB(sstableSizeMiB) + .withSSTableLevel(level) + .withRepairedAtMillis(repairedAtMillis); + if (sorted) + builder.sorted(); + return builder.build(); + } + catch (Exception e) + { + throw new RuntimeException("Failed to create SSTable writer", e); + } + } + + private CQLVisitExecutor createExecutor(HarrySSTableWriter writer, java.io.File progressFile) + { + return createExecutor(schema, partitionFactory, writer, progressFile); + } + + public static CQLVisitExecutor createExecutor(SchemaSpec schema, ValueGenerators valueGenerators, HarrySSTableWriter writer, java.io.File progressFile) + { + DataTracker tracker = new DataTracker.NoOpDataTracker(); + QueryBuildingVisitExecutor queryBuilder = new QueryBuildingVisitExecutor(schema, + QueryBuildingVisitExecutor.WrapQueries.EMPTY, + valueGenerators); + return new CQLVisitExecutor(schema, tracker, Model.NO_OP, queryBuilder) + { + long lts; + { + writer.setListener(file -> { + System.out.println(String.format("Written to %d: %s", lts, file)); + if (progressFile != null) + { + try { Files.write(ByteBufferUtil.getArray(ByteBufferUtil.bytes(lts)), progressFile); } + catch (IOException e) { throw new RuntimeException(e); } + } + }); + } + @Override + protected void executeMutatingVisit(Visit visit, CompiledStatement statement) + { + lts = visit.lts; + try + { + writer.addRow(statement.cql(), statement.bindings()); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + protected void executeValidatingVisit(Visit visit, List selects, CompiledStatement compiledStatement) + { + } + + @Override + protected List executeWithResult(Visit visit, CompiledStatement statement) + { + throw new UnsupportedOperationException("SSTable generation does not support reads"); + } + + @Override + protected void executeWithoutResult(Visit visit, CompiledStatement statement) + { + executeMutatingVisit(visit, statement); + } + + @Override + public void execute(Visit visit) + { + if (visit.visitedPartitions.length > 1) + throw new IllegalStateException("SSTable generator does not support batch statements across multiple partitions"); + + super.execute(visit); + } + }; + } + + private static void closeWriter(HarrySSTableWriter writer) + { + try + { + writer.close(); + } + catch (IOException e) + { + throw new UncheckedIOException("Failed to close SSTable writer", e); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/StressWorker.java b/test/harry/main/org/apache/cassandra/harry/stress/StressWorker.java new file mode 100644 index 000000000000..fa56a318e5cc --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/StressWorker.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.stress; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.HdrHistogram.Histogram; +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.InfiniteLoopExecutor; +import org.apache.cassandra.concurrent.Interruptible; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.DataTracker; +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.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.Clock; + +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; + +public class StressWorker implements Interruptible +{ + public static class WorkerMetrics + { + final Histogram reads, writes; + final AtomicInteger failedReads = new AtomicInteger(), failedWrites = new AtomicInteger(); + + public WorkerMetrics(Histogram reads, Histogram writes) + { + this.reads = reads; + this.writes = writes; + } + } + private final LinkedBlockingQueue tasks; + private final int capacity; + + private final CQLVisitExecutor executor; + private final Interruptible loop; + private final AtomicReference metrics = new AtomicReference<>(); + + public StressWorker(int idx, + Model.PartialReplay replay, + DataTracker tracker, + ActivePartition.Partitions partitionFactory, + Supplier> sutFactory, + Consumer onException, + int queueCapacity) + { + resetMetrics(); + Function sut = new Function<>() + { + final BiFunction delegate = sutFactory.get(); + + @Override + public Object[][] apply(CompiledStatement compiledStatement) + { + long start = Clock.Global.nanoTime(); + return delegate.apply(compiledStatement, () -> { + long end = Clock.Global.nanoTime(); + WorkerMetrics metrics1 = StressWorker.this.metrics.get(); + (compiledStatement.validating ? metrics1.reads : metrics1.writes).recordValue(end - start); + }); + } + }; + + this.tasks = new LinkedBlockingQueue<>(queueCapacity); + this.capacity = queueCapacity; + this.executor = new CQLVisitExecutor(partitionFactory.schema, + tracker, + new QuiescentChecker(partitionFactory, replay), + new QueryBuildingVisitExecutor(partitionFactory.schema, QueryBuildingVisitExecutor.WrapQueries.EMPTY, partitionFactory)) + { + @Override + protected List executeWithResult(Visit visit, CompiledStatement statement) + { + Object[][] result = sut.apply(statement); + if (result == null) + return new ArrayList<>(); + return InJvmDTestVisitExecutor.rowsToResultSet(schema, partitionFactory, + (Operations.SelectStatement) visit.operations[0], result); + } + + @Override + protected void executeWithoutResult(Visit visit, CompiledStatement statement) + { + sut.apply(statement); + } + }; + this.loop = executorFactory().infiniteLoop("visit-executor" + idx, state -> { + try + { + switch (state) + { + case NORMAL: + Visit visit = tasks.take(); + try + { + executor.execute(visit); + } + catch (Throwable e) + { + if (e.getClass().toString().contains("InterruptedException")) + return; + WorkerMetrics ms = metrics.get(); + (visit.validating ? ms.failedReads : ms.failedWrites).incrementAndGet(); + onException.accept(e); + } + break; + case INTERRUPTED: + case SHUTTING_DOWN: + break; + } + } + catch (Throwable e) + { + onException.accept(e); + } + }, SAFE, ExecutorFactory.SystemThreadTag.DAEMON, InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED); + } + + public int getFreeSlots() + { + return capacity - tasks.size(); + } + + public WorkerMetrics resetMetrics() + { + return metrics.getAndSet(new WorkerMetrics(new Histogram(3), new Histogram(3))); + } + + public boolean offer(Visit visit) + { + return tasks.offer(visit); + } + + @Override + public void interrupt() + { + loop.interrupt(); + } + + @Override + public boolean isTerminated() + { + return loop.isTerminated(); + } + + @Override + public void shutdown() + { + loop.shutdown(); + } + + @Override + public Object shutdownNow() + { + return loop.shutdownNow(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException + { + return loop.awaitTermination(timeout, units); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/TokenIndex.java b/test/harry/main/org/apache/cassandra/harry/stress/TokenIndex.java new file mode 100644 index 000000000000..2a88ca7edef3 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/TokenIndex.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Indexed reader over a merged token data file and its companion {@code .idx} file, + * both produced by {@link TokenIndexGenerator#merge}. + * + *

Data file format (sorted by token, then pd): {@code [token:8][pd:8][count:4][lts:8 * count]} repeated. + *

Index file format (sorted by token): {@code [token:8][offset:8]} repeated, fixed 16-byte entries. + * + *

The index is binary-searchable because entries are fixed-size and token-sorted. + * After locating the start of a range in the index, entries are read sequentially from the data file. + */ +public class TokenIndex implements Closeable +{ + private final FileInputStream dataFis; + private final FileInputStream idxFis; + private final FileChannel dataChannel; + private final FileChannel idxChannel; + private final long entryCount; + private final ByteBuffer idxBuf = ByteBuffer.allocate(16); // token(8) + offset(8) + + public TokenIndex(File dataFile, File idxFile) + { + try + { + this.dataFis = new FileInputStream(dataFile); + this.idxFis = new FileInputStream(idxFile); + this.dataChannel = dataFis.getChannel(); + this.idxChannel = idxFis.getChannel(); + this.entryCount = idxChannel.size() / 16; + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + /** Convenience overload accepting Cassandra's {@link org.apache.cassandra.io.util.File}. */ + public TokenIndex(org.apache.cassandra.io.util.File dataFile, org.apache.cassandra.io.util.File idxFile) + { + this(dataFile.toJavaIOFile(), idxFile.toJavaIOFile()); + } + + public long entryCount() + { + return entryCount; + } + + public long lookup(long token) + { + long idx = lowerBound(token); + if (idx >= entryCount) + return -1; + readIdxEntry(idx); + long foundToken = idxBuf.getLong(); + if (foundToken != token) + return -1; + return idxBuf.getLong(); + } + + public EntryIterator range(long minToken, long maxToken) + { + long startIdx = lowerBound(minToken); + if (startIdx >= entryCount) + return new EntryIterator(dataChannel, 0, maxToken, false); + readIdxEntry(startIdx); + idxBuf.getLong(); // skip token + long dataOffset = idxBuf.getLong(); + return new EntryIterator(dataChannel, dataOffset, maxToken, true); + } + + private long lowerBound(long target) + { + long lo = 0, hi = entryCount; + while (lo < hi) + { + long mid = lo + (hi - lo) / 2; + if (readIdxToken(mid) < target) + lo = mid + 1; + else + hi = mid; + } + return lo; + } + + private long readIdxToken(long index) + { + readIdxEntry(index); + return idxBuf.getLong(); + } + + private void readIdxEntry(long index) + { + idxBuf.clear(); + long pos = index * 16; + while (idxBuf.hasRemaining()) + { + try + { + int n = idxChannel.read(idxBuf, pos + idxBuf.position()); + if (n < 0) + throw new IllegalStateException("Unexpected EOF in index at entry " + index); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + idxBuf.flip(); + } + + @Override + public void close() throws IOException + { + try + { + dataChannel.close(); + } + finally + { + try + { + dataFis.close(); + } + finally + { + try + { + idxChannel.close(); + } + finally + { + idxFis.close(); + } + } + } + } + + public static class EntryIterator + { + private final FileChannel channel; + private final long maxToken; + private final ByteBuffer headerBuf = ByteBuffer.allocate(20); // token(8) + pd(8) + count(4) + private long currentToken; + private long currentPd; + private int currentLtsCount; + private long ltsDataOffset; + private boolean hasMore; + + EntryIterator(FileChannel channel, long startOffset, long maxToken, boolean hasData) + { + this.channel = channel; + this.maxToken = maxToken; + if (hasData) + { + try + { + channel.position(startOffset); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + readHeader(); + } + else + { + this.hasMore = false; + } + } + + public boolean hasNext() + { + return hasMore; + } + + public long token() + { + return currentToken; + } + + public long pd() + { + return currentPd; + } + + public int ltsCount() + { + return currentLtsCount; + } + + public long[] readLts() + { + try + { + ByteBuffer buf = ByteBuffer.allocate(currentLtsCount * Long.BYTES); + long filePos = ltsDataOffset; + while (buf.hasRemaining()) + { + int n = channel.read(buf, filePos); + if (n < 0) + throw new IOException("Unexpected EOF reading LTS data"); + filePos += n; + } + buf.flip(); + long[] lts = new long[currentLtsCount]; + for (int i = 0; i < currentLtsCount; i++) + lts[i] = buf.getLong(); + return lts; + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + public void advance() + { + try + { + channel.position(ltsDataOffset + (long) currentLtsCount * Long.BYTES); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + readHeader(); + } + + private void readHeader() + { + headerBuf.clear(); + try + { + while (headerBuf.hasRemaining()) + { + int read = channel.read(headerBuf); + if (read < 0) + { + hasMore = false; + return; + } + } + headerBuf.flip(); + currentToken = headerBuf.getLong(); + currentPd = headerBuf.getLong(); + currentLtsCount = headerBuf.getInt(); + ltsDataOffset = channel.position(); + hasMore = currentToken <= maxToken; + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/TokenIndexGenerator.java b/test/harry/main/org/apache/cassandra/harry/stress/TokenIndexGenerator.java new file mode 100644 index 000000000000..c2a2c4e60ea7 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/TokenIndexGenerator.java @@ -0,0 +1,451 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Set; + +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.HashMap; + +import accord.utils.Invariants; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.harry.util.ByteUtils; +import org.apache.cassandra.harry.util.TokenUtil; + +/** + * In order to generate SSTables per level, we ideally need to know partitions ahead of time, since otherwise + * we need to keep SSTableWriters open, which is not feasible. To do this, we iterate through all LTS that will + * be visited, and generate a mapping of Token / partition descriptor to set of LTS that will be visited for that + * partition. + * + * Token index files are sorted in memory and flushed on disk in token order. After all LTS were replayed, we + * merge-iterate through individual token index files and create one big merged file, in token order. + */ +public class TokenIndexGenerator +{ + static class TokenAndPd implements Comparable + { + final long token; + final long pd; + + TokenAndPd(long token, long pd) + { + this.token = token; + this.pd = pd; + } + + @Override + public int compareTo(TokenAndPd o) + { + int cmp = Long.compare(token, o.token); + return cmp != 0 ? cmp : Long.compare(pd, o.pd); + } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof TokenAndPd)) return false; + TokenAndPd that = (TokenAndPd) o; + return token == that.token && pd == that.pd; + } + + @Override + public int hashCode() + { + return Long.hashCode(token) * 31 + Long.hashCode(pd); + } + } + + public static void generate(File dir, + SchemaSpec schema, + RotationStrategy rotationStrategy, + Distribution rowPopulation, + Generator visitTypeGen, + Distribution visitSizeDistribution, + long initialLts, + long visits) throws IOException + { + System.out.println("******************** SStable Level Generator ********************"); + System.out.println(String.format(" Output Directory: %s", dir)); + System.out.println(String.format(" Initial LTS: %d", initialLts)); + System.out.println(String.format(" Visits: %d", visits)); + System.out.println(String.format(" Rotation: %s", rotationStrategy)); + System.out.println(); + int fileIdx = 0; + + long nextPartitionIdx = 0; + + class TokenCache + { + private final HashMap map = new HashMap<>(); + private final long[] ring; + private int pos = 0; + private boolean full = false; + + TokenCache(int capacity) + { + this.ring = new long[capacity]; + } + + long tokenFor(long partitionIdx) + { + Long cached = map.get(partitionIdx); + if (cached != null) + return cached; + + long pd = ActivePartition.DescriptorIndexBijection.INSTANCE.toPd(partitionIdx); + Object[] pkValues = SeedableEntropySource.computeWithSeed(pd, SchemaSpec.forKeys(schema.partitionKeys)::generate); + long token = TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(pkValues))); + + if (full) + map.remove(ring[pos]); + ring[pos] = partitionIdx; + pos = (pos + 1) % ring.length; + if (pos == 0) full = true; + map.put(partitionIdx, token); + return token; + } + } + + TokenCache tokenCache = new TokenCache(10_000); + ActivePartition.VisitedPartitions visitedPartitions = new ActivePartition.VisitedPartitions(0); + long[] activePartitionIdxs = new long[rotationStrategy.targetSize()]; + Set activePartitionIdxSet = new HashSet<>(activePartitionIdxs.length); + Map ltsPerPd = new HashMap<>(); + Map sizePerPd = new HashMap<>(); + + // Initial active set fill. Do NOT pre-add to visitedPartitions: ActivePartition.Partitions.populate() + // only adds an idx to visitedPartitions when it's evicted by a rotation, never at fill time. + for (int i = 0; i < rotationStrategy.targetSize(); i++) + { + long idx = nextPartitionIdx++; + long pd = ActivePartition.DescriptorIndexBijection.INSTANCE.toPd(idx); + activePartitionIdxs[i] = idx; + activePartitionIdxSet.add(idx); + sizePerPd.putIfAbsent(pd, Math.toIntExact(rowPopulation.next(pd))); + } + + TreeMap> visitsPerToken = new TreeMap<>(); + AtomicInteger cnt = new AtomicInteger(); + List files = new ArrayList<>(); + for (long lts = 0; lts < initialLts + visits; lts++) + { + if (lts % 100_000 == 0) + System.out.println("Seen " + lts); + + // Visit at lts is generated against state from rotations [< lts] (matching VisitGenerator.inflate + + // HarryStress.start, where visit(L) is generated before maybeSwitchPartition(L) is called). + if (lts >= initialLts) + { + VisitGenerator.VisitType visitType = SeedableEntropySource.computeWithSeed(lts, visitTypeGen::generate); + if (visitType == VisitGenerator.VisitType.MUTATE) + { + long visitSize = visitSizeDistribution.next(lts); + Invariants.require(visitSize > 0); + for (int op = 0; op < visitSize; op++) + { + // If you are changing choosing here, make sure to also change VisitGeneratort#mutationVisit + int slot = SeedableEntropySource.computeWithSeed(lts, ~op, r -> r.nextInt(activePartitionIdxs.length)); + long partitionIdx = activePartitionIdxs[slot]; + long pd = ActivePartition.DescriptorIndexBijection.INSTANCE.toPd(partitionIdx); + long token = tokenCache.tokenFor(partitionIdx); + visitsPerToken.computeIfAbsent(new TokenAndPd(token, pd), k -> new ArrayList<>(10)) + .add(lts); + ltsPerPd.merge(pd, 1, Integer::sum); + cnt.incrementAndGet(); + } + } + } + + // Rotation. Skip at lts==initialLts: populate covers [0, initialLts) and HarryStress.start's first + // maybeSwitchPartition is at initialLts+1, so shouldSwitch(initialLts) is never called by runtime. + if (lts != initialLts && rotationStrategy.shouldSwitch(lts)) + { + RotationStrategy.PartitionAction[] actions = SeedableEntropySource.computeWithSeed(lts, rotationStrategy::generate); + for (int i = 0; i < actions.length; i++) + { + RotationStrategy.PartitionAction action = actions[i]; + int remove = SeedableEntropySource.computeWithSeed(Util.hash(lts, i), r -> r.nextInt(activePartitionIdxs.length)); + long toVisitedIdx = activePartitionIdxs[remove]; + long toVisitedPd = ActivePartition.DescriptorIndexBijection.INSTANCE.toPd(toVisitedIdx); + int partitionSize = Math.toIntExact(rowPopulation.next(toVisitedPd)); + sizePerPd.putIfAbsent(toVisitedPd, partitionSize); + boolean evict = SeedableEntropySource.computeWithSeed(Util.hash(lts, i), r -> r.nextInt(Math.max(1, Integer.highestOneBit(partitionSize))) == 0); + if (!evict) continue; + switch (action) + { + case REPLACE_WITH_NEW: + { + long newIdx = nextPartitionIdx++; + long newPd = ActivePartition.DescriptorIndexBijection.INSTANCE.toPd(newIdx); + sizePerPd.putIfAbsent(newPd, Math.toIntExact(rowPopulation.next(newPd))); + activePartitionIdxSet.remove(toVisitedIdx); + activePartitionIdxs[remove] = newIdx; + activePartitionIdxSet.add(newIdx); + break; + } + case REPLACE_WITH_VISITED: + { + long toRevisitIdx = visitedPartitions.getBySeed(Util.hash(lts, i)); + if (toRevisitIdx < 0 || activePartitionIdxSet.contains(toRevisitIdx)) + { + // picked one is still active; fall back to new + long newIdx = nextPartitionIdx++; + long newPd = ActivePartition.DescriptorIndexBijection.INSTANCE.toPd(newIdx); + sizePerPd.putIfAbsent(newPd, Math.toIntExact(rowPopulation.next(newPd))); + activePartitionIdxSet.remove(activePartitionIdxs[remove]); + activePartitionIdxs[remove] = newIdx; + activePartitionIdxSet.add(newIdx); + } + else + { + activePartitionIdxSet.remove(toVisitedIdx); + activePartitionIdxs[remove] = toRevisitIdx; + activePartitionIdxSet.add(toRevisitIdx); + } + break; + } + } + visitedPartitions.add(toVisitedIdx); + } + } + + if (visitsPerToken.size() > 100000 || lts == initialLts + visits - 1) + { + File currentFile = new File(dir, "tokens_" + fileIdx++); + System.out.println("Writing " + currentFile); + currentFile.createNewFile(); + files.add(currentFile); + try (FileOutputStream s = new FileOutputStream(currentFile); + BufferedOutputStream os = new BufferedOutputStream(s); + DataOutputStream dos = new DataOutputStream(os)) + { + for (Map.Entry> entry : visitsPerToken.entrySet()) + { + dos.writeLong(entry.getKey().token); + dos.writeLong(entry.getKey().pd); + dos.writeInt(entry.getValue().size()); + for (Long l : entry.getValue()) + dos.writeLong(l); + } + } + visitsPerToken.clear(); + } + } + merge(files, new File(dir, "merged_tokens")); + + printHistogram("LTS per partition", ltsPerPd.values()); + printHistogram("Partition size", sizePerPd.values()); + } + + private static final int[] BOUNDARIES = { 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 100000 }; + + static void printHistogram(String label, Collection values) + { + int[] buckets = new int[BOUNDARIES.length + 1]; + for (int v : values) + { + int b = 0; + while (b < BOUNDARIES.length && v >= BOUNDARIES[b]) + b++; + buckets[b]++; + } + System.out.println(String.format("\n%s distribution (%d entries):", label, values.size())); + System.out.println(String.format(" [0-%d): %d", BOUNDARIES[0], buckets[0])); + for (int i = 1; i < BOUNDARIES.length; i++) + { + if (buckets[i] > 0) + System.out.println(String.format(" [%d-%d): %d", BOUNDARIES[i - 1], BOUNDARIES[i], buckets[i])); + } + System.out.println(String.format(" [%d+): %d", BOUNDARIES[BOUNDARIES.length - 1], buckets[BOUNDARIES.length])); + } + + /** + * Merges token files produced by {@link #generate}. + * If the same (token, pd) appears in multiple files, LTS bytes are concatenated in file index order. + * Also writes an.idx file with [token:8][offset:8] entries (one per unique token). + */ + public static void merge(List inputFiles, File outputFile) throws IOException + { + class PeekingIter implements Comparable, Closeable + { + private final FileInputStream fis; + private final FileChannel channel; + private final int fileIndex; + private final ByteBuffer headerBuf = ByteBuffer.allocate(20); // token(8) + pd(8) + count(4) + long currentToken; + long currentPd; + int currentLtsCount; + boolean hasMore; + + PeekingIter(FileInputStream fis, int fileIndex) throws IOException + { + this.fis = fis; + this.channel = fis.getChannel(); + this.fileIndex = fileIndex; + advance(); + } + + void advance() throws IOException + { + headerBuf.clear(); + while (headerBuf.hasRemaining()) + { + int read = channel.read(headerBuf); + if (read < 0) + { + hasMore = false; + return; + } + } + headerBuf.flip(); + currentToken = headerBuf.getLong(); + currentPd = headerBuf.getLong(); + currentLtsCount = headerBuf.getInt(); + hasMore = true; + } + + void appendTo(FileChannel out) throws IOException + { + long bytes = (long) currentLtsCount * Long.BYTES; + long pos = channel.position(); + long remaining = bytes; + while (remaining > 0) + { + long transferred = channel.transferTo(pos, remaining, out); + pos += transferred; + remaining -= transferred; + } + channel.position(pos); + } + + @Override + public int compareTo(PeekingIter other) + { + int cmp = Long.compare(this.currentToken, other.currentToken); + if (cmp != 0) return cmp; + cmp = Long.compare(this.currentPd, other.currentPd); + if (cmp != 0) return cmp; + return Integer.compare(this.fileIndex, other.fileIndex); + } + + @Override + public void close() throws IOException + { + channel.close(); + fis.close(); + } + } + + ArrayList readers = new ArrayList<>(); + PriorityQueue pq = new PriorityQueue<>(); + for (int i = 0; i < inputFiles.size(); i++) + { + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") FileInputStream fis = new FileInputStream(inputFiles.get(i)); + PeekingIter reader = new PeekingIter(fis, i); + readers.add(reader); + if (reader.hasMore) + pq.add(reader); + } + + System.out.println("Writing merged results to " + outputFile); + try (FileOutputStream fos = new FileOutputStream(outputFile); + FileChannel out = fos.getChannel(); + FileOutputStream idxFos = new FileOutputStream(outputFile.getPath() + ".idx"); + FileChannel idx = idxFos.getChannel()) + { + ByteBuffer headerBuf = ByteBuffer.allocate(20); // token(8) + pd(8) + count(4) + ByteBuffer idxBuf = ByteBuffer.allocate(16); // token(8) + offset(8) + long lastIndexedToken = Long.MIN_VALUE; + boolean firstEntry = true; + + while (!pq.isEmpty()) + { + long token = pq.peek().currentToken; + long pd = pq.peek().currentPd; + + // Write index entry only for first occurrence of each token + if (firstEntry || token != lastIndexedToken) + { + long offset = out.position(); + idxBuf.clear(); + idxBuf.putLong(token); + idxBuf.putLong(offset); + idxBuf.flip(); + while (idxBuf.hasRemaining()) + idx.write(idxBuf); + lastIndexedToken = token; + firstEntry = false; + } + + // Gather all readers at this (token, pd); PQ orders by (token, pd, fileIndex) + int totalCount = 0; + ArrayList batch = new ArrayList<>(); + while (!pq.isEmpty() && pq.peek().currentToken == token && pq.peek().currentPd == pd) + { + PeekingIter r = pq.poll(); + totalCount += r.currentLtsCount; + batch.add(r); + } + + // Write merged header + headerBuf.clear(); + headerBuf.putLong(token); + headerBuf.putLong(pd); + headerBuf.putInt(totalCount); + headerBuf.flip(); + while (headerBuf.hasRemaining()) + out.write(headerBuf); + + // Transfer LTS bytes from each reader directly to output + for (PeekingIter r : batch) + { + r.appendTo(out); + r.advance(); + if (r.hasMore) + pq.add(r); + } + } + } + finally + { + for (PeekingIter r : readers) + r.close(); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/Util.java b/test/harry/main/org/apache/cassandra/harry/stress/Util.java new file mode 100644 index 000000000000..1a7e729eca33 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/Util.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +public class Util +{ + // Cantor pairing + public static long hash(long v1, int v2) { + return ((v1 + v2) * (v1 + v2 + 1) / 2) + v2; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/VisitGenerator.java b/test/harry/main/org/apache/cassandra/harry/stress/VisitGenerator.java new file mode 100644 index 000000000000..dcb7394ed94e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/VisitGenerator.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress; + +import accord.utils.UnhandledEnum; +import org.apache.cassandra.harry.MagicConstants; +import org.apache.cassandra.harry.Relations; +import org.apache.cassandra.harry.dsl.SingleOperationVisitBuilder; +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.Surjections; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; +import org.apache.cassandra.harry.op.ClusteringOrderBy; +import org.apache.cassandra.harry.op.Kind; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.stress.distribution.Distribution; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.function.Supplier; + +import static org.apache.cassandra.harry.op.Operations.Operation; +import static org.apache.cassandra.harry.op.Operations.PartitionOperation; + +/** + * A stateful generator for operation steps. + * + * Generator maintains a list of active partitions. On each logical timestamp, a subset of these + * partitions is chosen for visit. + */ +public class VisitGenerator implements Surjections.Surjection, Supplier +{ + final ActivePartition.Partitions partitions; + final Generator visitTypeGen; + + final Distribution visitSizeDistribution; + final OpKindGenFactory operationKindGen; + + // The target number of inserts to split a partition into; if more than one, the partition will be placed in the revisit set + private final AtomicLong lts = new AtomicLong(); + + public VisitGenerator(ActivePartition.Partitions partitions, + Generator visitTypeGen, + Distribution visitSizeDistribution, + OpKindGenFactory operationKindGen) + { + this(partitions, visitTypeGen, visitSizeDistribution, operationKindGen, 0); + } + + public VisitGenerator(ActivePartition.Partitions partitions, + Generator visitTypeGen, + Distribution visitSizeDistribution, + OpKindGenFactory operationKindGen, + long initialLts) + { + this.partitions = partitions; + this.visitTypeGen = visitTypeGen; + this.visitSizeDistribution = visitSizeDistribution; + this.operationKindGen = operationKindGen; + this.lts.set(initialLts); + } + + public interface ColumnPopulation + { + Distribution distribution(String column); + } + + public interface OpKindGenFactory + { + Generator forType(VisitType type); + } + + @Override + public Visit inflate(long lts) + { + Visit visit; + VisitType visitType = SeedableEntropySource.computeWithSeed(lts, visitTypeGen::generate); + switch (visitType) + { + case MUTATE: + visit = mutatingVisit(lts); + break; + case VALIDATE: + visit = validatingVisit(lts); + break; + default: + throw UnhandledEnum.unknown(visitType); + } + return visit; + } + + @Override + public Visit get() + { + return inflate(this.lts.getAndIncrement()); + } + + private Visit mutatingVisit(long lts) + { + return mutatingVisit(lts, visitSizeDistribution, operationKindGen, partitions::pick); + } + + public static Visit mutatingVisit(long lts, Distribution visitSizeDistribution, OpKindGenFactory operationKindGen, Function partitionPicker) + { + Operation[] operations = new Operation[Math.toIntExact(visitSizeDistribution.next(lts))]; + for (int op = 0; op < operations.length; op++) + { + // If you are changing choosing here, make sure to also change TokenIndexGenerator#generate + ActivePartition partition = SeedableEntropySource.computeWithSeed(lts, ~op, partitionPicker); + operations[op] = SeedableEntropySource.computeWithSeed(lts, op, rng -> { + Kind kind = operationKindGen.forType(VisitType.MUTATE).generate(rng); + return mutatingOperation(partition, kind, lts, rng); + }); + } + return new Visit(lts, operations); + } + + static PartitionOperation mutatingOperation(ActivePartition partition, Kind kind, long lts, EntropySource rng) + { + // TODO: more sophisticated picking + int cdIdx = rng.nextInt(partition.ckPopulation()); + + int[] valueIdxs = new int[partition.regularColumnCount()]; + for (int i = 0; i < valueIdxs.length; i++) + valueIdxs[i] = rng.nextInt(partition.regularPopulation(i)); + int[] sValueIdxs = new int[partition.staticColumnCount()]; + for (int i = 0; i < sValueIdxs.length; i++) + sValueIdxs[i] = rng.nextInt(partition.staticColumnCount()); + return SingleOperationVisitBuilder.write(partition.pkGen(), + partition, + lts, + partition.idx, + cdIdx, + valueIdxs, + sValueIdxs, + kind); + } + + private Visit validatingVisit(long lts) + { + ActivePartition partition = SeedableEntropySource.computeWithSeed(lts, ~0, partitions::pick); + return SeedableEntropySource.computeWithSeed(lts, rng -> { + Kind kind = operationKindGen.forType(VisitType.VALIDATE).generate(rng); + return new Visit(lts, new Operation[] { validatingOperation(partition, kind, lts, rng) }); + }); + } + + private static PartitionOperation validatingOperation(ActivePartition partition, Kind kind, long lts, EntropySource rng) + { + switch (kind) + { + case CUSTOM: + case UPDATE: + case INSERT: + case DELETE_PARTITION: + case DELETE_ROW: + case DELETE_COLUMNS: + case DELETE_RANGE: + throw new IllegalArgumentException("Validating operation can only be SELECT"); + case SELECT_PARTITION: + return new Operations.SelectPartition(lts, partition.pd, ClusteringOrderBy.ASC); + case SELECT_ROW: + // ckIdxGen yields a clustering index; convert it to the clustering descriptor (cd) the SelectRow + // expects, exactly as SELECT_RANGE does for its bound below. + return new Operations.SelectRow(lts, partition.pd, partition.ckGen().descriptorAt(partition.ckIdxGen.generate(rng))); + case SELECT_RANGE: + long lowerBoundIdx = partition.ckIdxGen.generate(rng); + long lowerBoundCd = partition.ckGen().descriptorAt(lowerBoundIdx); + + int nonEqFrom = rng.nextInt(partition.ckColumnCount()); + boolean includeBound = rng.nextBoolean(); + + Relations.RelationKind[] lowerBoundRelations = new Relations.RelationKind[partition.ckColumnCount()]; + for (int i = 0; i < Math.min(nonEqFrom + 1, partition.ckColumnCount()); i++) + { + if (i < nonEqFrom) + lowerBoundRelations[i] = Relations.RelationKind.EQ; + else + lowerBoundRelations[i] = includeBound ? Relations.RelationKind.GTE : Relations.RelationKind.GT; + } + + return new Operations.SelectRange(lts, partition.pd, + lowerBoundCd, MagicConstants.UNSET_DESCR, + lowerBoundRelations, null); + case SELECT_CUSTOM: + default: + throw new UnsupportedOperationException(kind + " is not supported"); + } + } + + // TODO (required) The percent of a given rows columns to populate + public enum VisitType + { + MUTATE, + VALIDATE + // TODO: custom actions: repair, expand/shrink cluster + // TODO: TCM actions, such as ALTER TABLE, etc, too? + } + + public static class RandomOpKindGenFactory implements OpKindGenFactory + { + public static Kind[] VALIDATE_KINDS = new Kind[] { Kind.SELECT_PARTITION, Kind.SELECT_ROW, Kind.SELECT_RANGE }; + public static Kind[] MUTATE_KINDS = new Kind[] { Kind.INSERT, Kind.UPDATE }; + + private final Generator mutateGen = Generators.pick(MUTATE_KINDS); + private final Generator validateGen = Generators.pick(VALIDATE_KINDS); + + @Override + public Generator forType(VisitGenerator.VisitType type) + { + switch (type) + { + case MUTATE: return mutateGen; + case VALIDATE: return validateGen; + default: throw new AssertionError(); + } + } + } + + public static class WeightedOpKindGenFactory implements OpKindGenFactory + { + private final Generator mutateGen; + private final Generator validateGen; + + public WeightedOpKindGenFactory(Map mutateWeights, Map validateWeights) + { + this.mutateGen = Generators.weighted(mutateWeights); + this.validateGen = Generators.weighted(validateWeights); + } + + @Override + public Generator forType(VisitType type) + { + switch (type) + { + case MUTATE: return mutateGen; + case VALIDATE: return validateGen; + default: throw new AssertionError(); + } + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private final Map mutateWeights = new LinkedHashMap<>(); + private final Map validateWeights = new LinkedHashMap<>(); + + public Builder() + { + mutateWeights.put(Kind.INSERT, 1); + mutateWeights.put(Kind.UPDATE, 1); + validateWeights.put(Kind.SELECT_PARTITION, 1); + validateWeights.put(Kind.SELECT_ROW, 1); + validateWeights.put(Kind.SELECT_RANGE, 1); + } + + public Builder mutate(Kind kind, int weight) + { + mutateWeights.put(kind, weight); + return this; + } + + public Builder validate(Kind kind, int weight) + { + validateWeights.put(kind, weight); + return this; + } + + public WeightedOpKindGenFactory build() + { + return new WeightedOpKindGenFactory(mutateWeights, validateWeights); + } + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/config/HarryStressSettings.java b/test/harry/main/org/apache/cassandra/harry/stress/config/HarryStressSettings.java new file mode 100644 index 000000000000..69c062f10cb7 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/config/HarryStressSettings.java @@ -0,0 +1,1104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.config; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.op.Kind; +import org.apache.cassandra.harry.stress.RotationStrategy; +import org.apache.cassandra.harry.stress.VisitGenerator; + +import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized; + +/** + * CLI settings for Harry stress test, following the same pattern as cassandra-stress. + * + *

NOTE: this file was almost entirely generated by an LLM as a time-saving + * scaffold. Our usual preference is programmatic APIs over CLI tooling for Harry, so + * it wasn't obvious that hand-writing a full cassandra-stress-style argument parser + * was worth the investment. Treat the parsing, help text, and option wiring here as + * best-effort generated code: read it carefully before relying on any specific flag, + * and don't assume every documented option is implemented or behaves exactly as the + * help string suggests. If a long-lived CLI becomes important, this is a good + * candidate to rewrite by hand (or replace with a real arg-parsing library). + * + * Usage: + * harry-stress -mode stress + * -cluster contact_points=host1,host2,host3 [port=9042] [ssl] + * -schema [keyspace=ks] [replication=DC1:3,DC2:3] [config=path/to/schema.yaml] + * -rate threads=1000 fixed=20000/s + * -ratio read=50 + * -rotation strategy=fixed target=2000 replace_with_new=5 replace_with_visited=5 min_partition_idx=0 max_partition_idx=1000000 + * + * harry-stress -mode generate-sstables + * -schema [keyspace=ks] [replication=DC1:3,DC2:3] [config=path/to/schema.yaml] + * -output directory=/path/to/output visits=1000000 [sstable_size_mb=256] + * -rotation strategy=fixed target=2000 replace_with_new=5 replace_with_visited=5 min_partition_idx=0 max_partition_idx=1000000 + * + * Node is initialized, moving to UNREGISTERED state + * - Usage: harry-stress [options] + * - Usage: -mode stress|generate-sstables + * - stress Run stress test against a live cluster (default) + * - generate-sstables Generate SSTables on disk + * - Usage: -cluster contact_points=host1,host2,... [port=9042] [ssl] + * - contact_points=? Comma-separated list of node hosts/IPs to connect to (required for stress mode) + * - port=? Native protocol port (default: 9042) + * - ssl Enable SSL + * - Usage: -schema [keyspace=?] [replication=DC1:3,DC2:3] [config=path/to/schema.yaml] + * - keyspace=? Keyspace name (default: auto-generated with timestamp) + * - replication=? Replication in DC:RF format, comma-separated (default: DC1:3,DC2:3) + * - config=? Path to YAML schema config file with table definition and column populations + * - Usage: -rate [threads=?] [fixed=?/s] + * - threads=? Number of concurrent workers (default: 1000) + * - fixed=?/s Target operations per second (default: 20000/s) + * - Usage: -ratio [read=?] + * - read=? Percentage of reads/validations, 0-100 (default: 50) + * - Usage: -rotation [strategy=fixed|random] [target=?] [replace_with_new=?] [replace_with_visited=?] [min_partition_idx=?] [max_partition_idx=?] [initial_lts=?] + * - strategy=? Rotation strategy: 'fixed' or 'random' (default: fixed) + * - target=? Target number of active partitions (default: 2000) + * - replace_with_new=? Partitions replaced with new per rotation cycle, fixed only (default: 5) + * - replace_with_visited=? Partitions replaced with visited per rotation cycle, fixed only (default: 5) + * - min_partition_idx=? Minimum partition index, inclusive (default: 0) + * - max_partition_idx=? Maximum partition index, exclusive (default: 9223372036854775807) + * - initial_lts=? Initial logical timestamp to resume from (default: 0) + * - partition_switch_interval=? Visits between partition switch checks (default: 500) + * - Usage: -output directory=? visits=? [sstable_size_mb=?] [compression] + * - directory=? Output directory for generated SSTables (required for generate-sstables) + * - visits=? Total number of visits to generate (required for generate-sstables) + * - sstable_size_mb=? Max SSTable size in MiB before flushing to a new one (default: 128) + * - compression Enable SSTable compression (default: disabled) + * - local_node_id=? Generate only partitions owned by this node (requires -cluster) + */ +public class HarryStressSettings +{ + public final Mode mode; + public final ClusterSettings cluster; + public final SchemaSettings schema; + public final RateSettings rate; + public final RatioSettings ratio; + public final RotationSettings rotation; + public final OpsSettings ops; + public final OutputSettings output; + public final StressSchemaConfig schemaConfig; + + public HarryStressSettings(Mode mode, + ClusterSettings cluster, + SchemaSettings schema, + RateSettings rate, + RatioSettings ratio, + RotationSettings rotation, + OpsSettings ops, + OutputSettings output) + { + this.mode = mode; + this.cluster = cluster; + this.schema = schema; + this.rate = rate; + this.ratio = ratio; + this.rotation = rotation; + this.ops = ops; + this.output = output; + + if (schema.configPath != null) + { + try + { + this.schemaConfig = StressSchemaConfig.load(Paths.get(schema.configPath)); + } + catch (IOException e) + { + throw new RuntimeException("Failed to load schema config from " + schema.configPath, e); + } + } + else + { + this.schemaConfig = null; + } + } + + public RotationStrategy buildRotationStrategy() + { + return rotation.buildRotationStrategy(); + } + + public Generator buildVisitTypeGen() + { + final int readPercent = ratio.readPercent; + return rng -> rng.nextInt(100) < readPercent ? VisitGenerator.VisitType.VALIDATE : VisitGenerator.VisitType.MUTATE; + } + + public VisitGenerator.OpKindGenFactory buildOpKindGenFactory() + { + return ops.buildOpKindGenFactory(); + } + + public void printSettings(PrintStream out) + { + out.println("******************** Harry Stress Settings ********************"); + out.println("Mode: " + mode); + if (cluster != null) + { + out.println("Cluster:"); + cluster.printSettings(out); + } + out.println("Schema:"); + schema.printSettings(out); + if (mode == Mode.STRESS) + { + out.println("Rate:"); + rate.printSettings(out); + out.println("Ratio:"); + ratio.printSettings(out); + out.println("Ops:"); + ops.printSettings(out); + } + if (output != null) + { + out.println("Output:"); + output.printSettings(out); + } + out.println("Rotation:"); + rotation.printSettings(out); + out.println(); + } + + // ========================== Mode ========================== + + public enum Mode + { + STRESS, + GENERATE_LEVELS, + GENERATE_LEVELLED_SSTABLES; + + static Mode parse(String value) + { + switch (toLowerCaseLocalized(value)) + { + case "stress": return STRESS; + case "generate-levels": return GENERATE_LEVELS; + case "generate-levelled-sstables": return GENERATE_LEVELLED_SSTABLES; + default: + throw new IllegalArgumentException("Unknown mode: " + value); + } + } + + @Override + public String toString() + { + switch (this) + { + case STRESS: return "stress"; + case GENERATE_LEVELS: return "generate-levels"; + case GENERATE_LEVELLED_SSTABLES: return "generate-levelled-sstables"; + default: throw new AssertionError(); + } + } + + static Mode get(Map clArgs) + { + String[] params = clArgs.remove("-mode"); + if (params == null || params.length == 0) + return STRESS; + if (params.length != 1) + throw new IllegalArgumentException("-mode expects exactly one value: stress, generate-sstables, or generate-levels"); + return parse(params[0]); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -mode stress|generate-sstables|generate-levels"); + System.out.println(); + System.out.println(" stress Run stress test against a live cluster (default)"); + System.out.println(" generate-sstables Generate SSTables on disk"); + System.out.println(" generate-levels Generate token index level files"); + } + } + + // ========================== Cluster Settings ========================== + + public static class ClusterSettings + { + public final String[] contactPoints; + public final int port; + public final boolean ssl; + public final String dc; + public final String username; + public final String password; + public final String consistencyLevel; + public final String selectConsistencyLevel; + public final int selectConsistencyLevelChance; + public final String writeConsistencyLevel; + public final int writeConsistencyLevelChance; + public final String metricsOut; + public final int reportIntervalSeconds; + + public ClusterSettings(ClusterOptions options) + { + this.contactPoints = options.contactPoints.value().split("\\s*,\\s*"); + this.port = Integer.parseInt(options.port.value()); + this.ssl = options.ssl.setByUser(); + this.dc = options.dc.setByUser() ? options.dc.value() : null; + this.username = options.username.setByUser() ? options.username.value() : null; + this.password = options.password.setByUser() ? options.password.value() : null; + this.consistencyLevel = options.consistencyLevel.value(); + this.selectConsistencyLevel = options.selectConsistencyLevel.setByUser() ? options.selectConsistencyLevel.value() : null; + this.selectConsistencyLevelChance = Integer.parseInt(options.selectConsistencyLevelChance.value()); + this.writeConsistencyLevel = options.writeConsistencyLevel.setByUser() ? options.writeConsistencyLevel.value() : null; + this.writeConsistencyLevelChance = Integer.parseInt(options.writeConsistencyLevelChance.value()); + this.metricsOut = options.metricsOut.value(); + this.reportIntervalSeconds = Integer.parseInt(options.reportIntervalSeconds.value()); + } + + public void printSettings(PrintStream out) + { + out.println(String.format(" Contact points: %s", String.join(", ", contactPoints))); + out.println(String.format(" Port: %d", port)); + out.println(String.format(" SSL: %b", ssl)); + out.println(String.format(" DC: %s", dc != null ? dc : "")); + out.println(String.format(" Username: %s", username != null ? username : "")); + out.println(String.format(" Consistency Level: %s", consistencyLevel)); + if (selectConsistencyLevel != null) + out.println(String.format(" Select Consistency Level: %s (%d%%)", selectConsistencyLevel, selectConsistencyLevelChance)); + if (writeConsistencyLevel != null) + out.println(String.format(" Write Consistency Level: %s (%d%%)", writeConsistencyLevel, writeConsistencyLevelChance)); + if (metricsOut != null) + out.println(String.format(" Metrics Out: %s", metricsOut)); + } + + static ClusterSettings get(Map clArgs, boolean required) + { + String[] params = clArgs.remove("-cluster"); + if (params == null) + { + if (required) + { + printHelp(); + throw new IllegalArgumentException("-cluster is required in stress mode. Use -help for usage information."); + } + return null; + } + + ClusterOptions options = new ClusterOptions(); + for (String param : params) + { + if (!options.accept(param)) + throw new IllegalArgumentException("Invalid -cluster parameter: " + param); + } + if (!options.isValid()) + throw new IllegalArgumentException("Missing required -cluster parameters (contact_points=)"); + return new ClusterSettings(options); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -cluster contact_points=host1,host2,... [port=9042] [ssl] [dc=?] [username=?] [password=?] [consistency_level=?]"); + System.out.println(); + System.out.println(" contact_points=? Comma-separated list of node hosts/IPs to connect to (required for stress mode)"); + System.out.println(" port=? Native protocol port (default: 9042)"); + System.out.println(" ssl Enable SSL"); + System.out.println(" dc=? Data center name (optional)"); + System.out.println(" username=? Cassandra username for authentication"); + System.out.println(" password=? Cassandra password for authentication"); + System.out.println(" consistency_level=? Consistency level (default: QUORUM)"); + System.out.println(" select_consistency_level=? Consistency level for SELECTs (default: same as consistency_level)"); + System.out.println(" select_consistency_level_chance=? Probability 0-100 of using select CL instead of default (default: 100)"); + System.out.println(" write_consistency_level=? Consistency level for writes (default: same as consistency_level)"); + System.out.println(" write_consistency_level_chance=? Probability 0-100 of using write CL instead of default (default: 100)"); + } + } + + static class ClusterOptions + { + final SimpleOption contactPoints = new SimpleOption("contact_points=", ".*", null, true); + final SimpleOption port = new SimpleOption("port=", "[0-9]+", "9042", false); + final SimpleOption ssl = new SimpleOption("ssl", "", null, false); + final SimpleOption dc = new SimpleOption("dc=", ".*", null, false); + final SimpleOption username = new SimpleOption("username=", ".*", null, false); + final SimpleOption password = new SimpleOption("password=", ".*", null, false); + final SimpleOption consistencyLevel = new SimpleOption("consistency_level=", ".*", "QUORUM", false); + final SimpleOption selectConsistencyLevel = new SimpleOption("select_consistency_level=", ".*", null, false); + final SimpleOption selectConsistencyLevelChance = new SimpleOption("select_consistency_level_chance=", "[0-9]+", "100", false); + final SimpleOption writeConsistencyLevel = new SimpleOption("write_consistency_level=", ".*", null, false); + final SimpleOption writeConsistencyLevelChance = new SimpleOption("write_consistency_level_chance=", "[0-9]+", "100", false); + final SimpleOption metricsOut = new SimpleOption("metrics_out=", ".*", null, false); + final SimpleOption reportIntervalSeconds = new SimpleOption("report_interval=", "[0-9]+", "30", false); + + private final List all = Arrays.asList(contactPoints, port, ssl, dc, username, password, consistencyLevel, selectConsistencyLevel, selectConsistencyLevelChance, writeConsistencyLevel, writeConsistencyLevelChance, metricsOut); + + boolean accept(String param) + { + for (SimpleOption opt : all) + if (opt.accept(param)) + return true; + return false; + } + + boolean isValid() + { + for (SimpleOption opt : all) + if (!opt.isValid()) + return false; + return true; + } + } + + // ========================== Schema Settings ========================== + + public static class SchemaSettings + { + public final String keyspace; + public final Map replication; + public final String configPath; + + public SchemaSettings(SchemaOptions options) + { + this.keyspace = options.keyspace.setByUser() ? options.keyspace.value() : null; + this.replication = parseReplication(options.replication.value()); + this.configPath = options.config.setByUser() ? options.config.value() : null; + } + + private static Map parseReplication(String spec) + { + Map result = new LinkedHashMap<>(); + if (spec == null || spec.isEmpty()) + return result; + for (String pair : spec.split(",")) + { + String[] kv = pair.split(":"); + if (kv.length != 2) + throw new IllegalArgumentException("Invalid replication spec, expected DC:RF pairs separated by commas, got: " + spec); + result.put(kv[0].trim(), kv[1].trim()); + } + return result; + } + + public String buildKeyspaceDDL(String keyspaceName) + { + StringBuilder sb = new StringBuilder(); + sb.append("CREATE KEYSPACE IF NOT EXISTS ").append(keyspaceName) + .append(" WITH replication = { 'class' : 'org.apache.cassandra.locator.NetworkTopologyStrategy'"); + for (Map.Entry entry : replication.entrySet()) + sb.append(", '").append(entry.getKey()).append("': '").append(entry.getValue()).append("'"); + sb.append(" };"); + return sb.toString(); + } + + public void printSettings(PrintStream out) + { + out.println(String.format(" Keyspace: %s", keyspace != null ? keyspace : "auto-generated")); + out.println(String.format(" Replication: %s", replication)); + if (configPath != null) + out.println(String.format(" Config: %s", configPath)); + } + + static SchemaSettings get(Map clArgs) + { + String[] params = clArgs.remove("-schema"); + SchemaOptions options = new SchemaOptions(); + if (params != null) + { + for (String param : params) + { + if (!options.accept(param)) + throw new IllegalArgumentException("Invalid -schema parameter: " + param); + } + } + return new SchemaSettings(options); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -schema [keyspace=?] [replication=DC1:3,DC2:3] [config=path/to/schema.yaml]"); + System.out.println(); + System.out.println(" keyspace=? Keyspace name (default: auto-generated with timestamp)"); + System.out.println(" replication=? Replication in DC:RF format, comma-separated (default: DC1:3,DC2:3)"); + System.out.println(" config=? Path to YAML schema config file with table definition and column populations"); + } + } + + static class SchemaOptions + { + final SimpleOption keyspace = new SimpleOption("keyspace=", ".*", null, false); + final SimpleOption replication = new SimpleOption("replication=", ".*", "DC1:3,DC2:3", false); + final SimpleOption config = new SimpleOption("config=", ".*", null, false); + + private final List all = Arrays.asList(keyspace, replication, config); + + boolean accept(String param) + { + for (SimpleOption opt : all) + if (opt.accept(param)) + return true; + return false; + } + + boolean isValid() + { + for (SimpleOption opt : all) + if (!opt.isValid()) + return false; + return true; + } + } + + // ========================== Rate Settings ========================== + + public static class RateSettings + { + public final int concurrency; + public final int ratePerSecond; + + public RateSettings(RateOptions options) + { + this.concurrency = Integer.parseInt(options.threads.value()); + String rateStr = options.fixed.value(); + this.ratePerSecond = Integer.parseInt(rateStr.endsWith("/s") ? rateStr.substring(0, rateStr.length() - 2) : rateStr); + } + + public void printSettings(PrintStream out) + { + out.println(String.format(" Threads: %d", concurrency)); + out.println(String.format(" Rate/s: %d", ratePerSecond)); + } + + static RateSettings get(Map clArgs) + { + String[] params = clArgs.remove("-rate"); + RateOptions options = new RateOptions(); + if (params != null) + { + for (String param : params) + { + if (!options.accept(param)) + throw new IllegalArgumentException("Invalid -rate parameter: " + param); + } + } + return new RateSettings(options); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -rate [threads=?] [fixed=?/s]"); + System.out.println(); + System.out.println(" threads=? Number of concurrent workers (default: 1000)"); + System.out.println(" fixed=?/s Target operations per second (default: 20000/s)"); + } + } + + static class RateOptions + { + final SimpleOption threads = new SimpleOption("threads=", "[0-9]+", "1000", false); + final SimpleOption fixed = new SimpleOption("fixed=", "[0-9]+(/s)?", "20000/s", false); + + private final List all = Arrays.asList(threads, fixed); + + boolean accept(String param) + { + for (SimpleOption opt : all) + if (opt.accept(param)) + return true; + return false; + } + + boolean isValid() + { + for (SimpleOption opt : all) + if (!opt.isValid()) + return false; + return true; + } + } + + // ========================== Ratio Settings ========================== + + public static class RatioSettings + { + public final int readPercent; + + public RatioSettings(RatioOptions options) + { + this.readPercent = Integer.parseInt(options.read.value()); + } + + public void printSettings(PrintStream out) + { + out.println(String.format(" Read: %d%%", readPercent)); + out.println(String.format(" Write: %d%%", 100 - readPercent)); + } + + static RatioSettings get(Map clArgs) + { + String[] params = clArgs.remove("-ratio"); + RatioOptions options = new RatioOptions(); + if (params != null) + { + for (String param : params) + { + if (!options.accept(param)) + throw new IllegalArgumentException("Invalid -ratio parameter: " + param); + } + } + return new RatioSettings(options); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -ratio [read=?]"); + System.out.println(); + System.out.println(" read=? Percentage of reads/validations, 0-100 (default: 50)"); + } + } + + static class RatioOptions + { + final SimpleOption read = new SimpleOption("read=", "[0-9]+", "50", false); + + private final List all = Arrays.asList(read); + + boolean accept(String param) + { + for (SimpleOption opt : all) + if (opt.accept(param)) + return true; + return false; + } + + boolean isValid() + { + for (SimpleOption opt : all) + if (!opt.isValid()) + return false; + return true; + } + } + + // ========================== Ops Settings ========================== + + public static class OpsSettings + { + public final int insertWeight; + public final int updateWeight; + public final int selectPartitionWeight; + public final int selectRowWeight; + public final int selectRangeWeight; + + public OpsSettings(OpsOptions options) + { + this.insertWeight = Integer.parseInt(options.insert.value()); + this.updateWeight = Integer.parseInt(options.update.value()); + this.selectPartitionWeight = Integer.parseInt(options.selectPartition.value()); + this.selectRowWeight = Integer.parseInt(options.selectRow.value()); + this.selectRangeWeight = Integer.parseInt(options.selectRange.value()); + } + + public VisitGenerator.OpKindGenFactory buildOpKindGenFactory() + { + VisitGenerator.WeightedOpKindGenFactory.Builder builder = VisitGenerator.WeightedOpKindGenFactory.builder(); + builder.mutate(Kind.INSERT, insertWeight); + builder.mutate(Kind.UPDATE, updateWeight); + builder.validate(Kind.SELECT_PARTITION, selectPartitionWeight); + builder.validate(Kind.SELECT_ROW, selectRowWeight); + builder.validate(Kind.SELECT_RANGE, selectRangeWeight); + return builder.build(); + } + + public void printSettings(PrintStream out) + { + out.println(String.format(" Write: insert=%d, update=%d", + insertWeight, updateWeight)); + out.println(String.format(" Read: select_partition=%d, select_row=%d, select_range=%d", + selectPartitionWeight, selectRowWeight, selectRangeWeight)); + } + + static OpsSettings get(Map clArgs) + { + String[] params = clArgs.remove("-ops"); + OpsOptions options = new OpsOptions(); + if (params != null) + { + for (String param : params) + { + if (!options.accept(param)) + throw new IllegalArgumentException("Invalid -ops parameter: " + param); + } + } + return new OpsSettings(options); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -ops [insert=?] [update=?] [select_partition=?] [select_row=?] [select_range=?]"); + System.out.println(); + System.out.println(" insert=? Weight for INSERT (default: 1)"); + System.out.println(" update=? Weight for UPDATE (default: 1)"); + System.out.println(" select_partition=? Weight for SELECT_PARTITION (default: 1)"); + System.out.println(" select_row=? Weight for SELECT_ROW (default: 1)"); + System.out.println(" select_range=? Weight for SELECT_RANGE (default: 1)"); + } + } + + static class OpsOptions + { + final SimpleOption insert = new SimpleOption("insert=", "[0-9]+", "1", false); + final SimpleOption update = new SimpleOption("update=", "[0-9]+", "1", false); + final SimpleOption selectPartition = new SimpleOption("select_partition=", "[0-9]+", "1", false); + final SimpleOption selectRow = new SimpleOption("select_row=", "[0-9]+", "1", false); + final SimpleOption selectRange = new SimpleOption("select_range=", "[0-9]+", "1", false); + + private final List all = Arrays.asList(insert, update, selectPartition, selectRow, selectRange); + + boolean accept(String param) + { + for (SimpleOption opt : all) + if (opt.accept(param)) + return true; + return false; + } + + boolean isValid() + { + for (SimpleOption opt : all) + if (!opt.isValid()) + return false; + return true; + } + } + + // ========================== Rotation Settings ========================== + + public static class RotationSettings + { + public final String strategy; // "fixed" or "random" + public final int targetSize; + // fixed-only + public final int replaceWithNew; + public final int replaceWithVisited; + public final int partitionSwitchInterval; + public final long minPartitionIdx; + public final long maxPartitionIdx; + public final long initialLts; + public final File ltsProgressFile; + + public RotationSettings(RotationOptions options) + { + this.strategy = options.strategy.value(); + this.targetSize = Integer.parseInt(options.target.value()); + this.replaceWithNew = Integer.parseInt(options.replaceWithNew.value()); + this.replaceWithVisited = Integer.parseInt(options.replaceWithVisited.value()); + this.partitionSwitchInterval = Integer.parseInt(options.partitionSwitchInterval.value()); + this.minPartitionIdx = Long.parseLong(options.minPartitionIdx.value()); + this.maxPartitionIdx = Long.parseLong(options.maxPartitionIdx.value()); + this.initialLts = Long.parseLong(options.initialLts.value()); + this.ltsProgressFile = options.ltsProgressFile.setByUser() ? new File(options.ltsProgressFile.value()) : null; + } + + public RotationStrategy buildRotationStrategy() + { + switch (strategy) + { + case "fixed": + return new RotationStrategy.FixedRotationStrategy(targetSize, replaceWithNew, replaceWithVisited, partitionSwitchInterval); + case "random": + return new RotationStrategy.RandomRotationStrategy(targetSize, partitionSwitchInterval); + default: + throw new IllegalArgumentException("Unknown rotation strategy: " + strategy + ". Expected 'fixed' or 'random'."); + } + } + + public void printSettings(PrintStream out) + { + out.println(String.format(" Strategy: %s", strategy)); + out.println(String.format(" Target Size: %d", targetSize)); + if ("fixed".equals(strategy)) + { + out.println(String.format(" Replace With New: %d", replaceWithNew)); + out.println(String.format(" Replace With Visited: %d", replaceWithVisited)); + } + out.println(String.format(" Min Partition Idx: %d", minPartitionIdx)); + out.println(String.format(" Max Partition Idx: %d", maxPartitionIdx)); + out.println(String.format(" Initial LTS: %d", initialLts)); + out.println(String.format(" Partition Switch Interval: %d", partitionSwitchInterval)); + } + + static RotationSettings get(Map clArgs) + { + String[] params = clArgs.remove("-rotation"); + RotationOptions options = new RotationOptions(); + if (params != null) + { + for (String param : params) + { + if (!options.accept(param)) + throw new IllegalArgumentException("Invalid -rotation parameter: " + param); + } + } + return new RotationSettings(options); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -rotation [strategy=fixed|random] [target=?] [replace_with_new=?] [replace_with_visited=?] [min_partition_idx=?] [max_partition_idx=?] [initial_lts=?]"); + System.out.println(); + System.out.println(" strategy=? Rotation strategy: 'fixed' or 'random' (default: fixed)"); + System.out.println(" target=? Target number of active partitions (default: 2000)"); + System.out.println(" replace_with_new=? Partitions replaced with new per rotation cycle, fixed only (default: 5)"); + System.out.println(" replace_with_visited=? Partitions replaced with visited per rotation cycle, fixed only (default: 5)"); + System.out.println(" min_partition_idx=? Minimum partition index, inclusive (default: 0)"); + System.out.println(" max_partition_idx=? Maximum partition index, exclusive (default: " + Long.MAX_VALUE + ")"); + System.out.println(" initial_lts=? Initial logical timestamp to resume from (default: 0)"); + System.out.println(" partition_switch_interval=? Visits between partition switch checks (default: 500)"); + } + } + + static class RotationOptions + { + final SimpleOption strategy = new SimpleOption("strategy=", "(fixed|random)", "fixed", false); + final SimpleOption target = new SimpleOption("target=", "[0-9]+", "2000", false); + final SimpleOption replaceWithNew = new SimpleOption("replace_with_new=", "[0-9]+", "5", false); + final SimpleOption replaceWithVisited = new SimpleOption("replace_with_visited=", "[0-9]+", "5", false); + final SimpleOption minPartitionIdx = new SimpleOption("min_partition_idx=", "[0-9]+", "0", false); + final SimpleOption maxPartitionIdx = new SimpleOption("max_partition_idx=", "[0-9]+", Long.toString(Long.MAX_VALUE), false); + final SimpleOption initialLts = new SimpleOption("initial_lts=", "[0-9]+", "0", false); + final SimpleOption ltsProgressFile = new SimpleOption("lts_progress_file=", ".*", null, false); + final SimpleOption partitionSwitchInterval = new SimpleOption("partition_switch_interval=", "[0-9]+", "500", false); + + private final List all = Arrays.asList(strategy, target, replaceWithNew, replaceWithVisited, minPartitionIdx, maxPartitionIdx, initialLts, ltsProgressFile, partitionSwitchInterval); + + boolean accept(String param) + { + for (SimpleOption opt : all) + if (opt.accept(param)) + return true; + return false; + } + + boolean isValid() + { + for (SimpleOption opt : all) + if (!opt.isValid()) + return false; + return true; + } + } + + // ========================== Output Settings (generate-sstables mode) ========================== + + public static class OutputSettings + { + public final String directory; + public final long visits; + public final int sstableSizeMiB; + public final boolean noCompression; + public final String localNodeId; // null if not specified + public final Long minToken, maxToken; + public final String tokenIndex; // path to token index data file (without .idx extension) + public final String levels; // comma-separated level weights, e.g. "1,10,100,500,1000,2000" + public final long maxPartitions; // max partitions to write (default: unlimited) + + public OutputSettings(OutputOptions options) + { + this.directory = options.directory.value(); + this.visits = Long.parseLong(options.visits.value()); + this.sstableSizeMiB = Integer.parseInt(options.sstableSizeMb.value()); + this.noCompression = !options.compression.setByUser(); + this.localNodeId = options.localNodeId.setByUser() ? options.localNodeId.value() : null; + this.minToken = options.minToken.setByUser() ? Long.parseLong(options.minToken.value()) : null; + this.maxToken = options.maxToken.setByUser() ? Long.parseLong(options.maxToken.value()) : null; + this.tokenIndex = options.tokenIndex.setByUser() ? options.tokenIndex.value() : null; + this.levels = options.levels.setByUser() ? options.levels.value() : null; + this.maxPartitions = options.maxPartitions.setByUser() ? Long.parseLong(options.maxPartitions.value()) : Long.MAX_VALUE; + } + + public void printSettings(PrintStream out) + { + out.println(String.format(" Directory: %s", directory)); + out.println(String.format(" Visits: %d", visits)); + out.println(String.format(" SSTable Size: %d MiB", sstableSizeMiB)); + out.println(String.format(" Compression: %s", noCompression ? "disabled" : "enabled (LZ4)")); + if (localNodeId != null) + out.println(String.format(" Local Node ID: %s", localNodeId)); + if (maxPartitions != Long.MAX_VALUE) + out.println(String.format(" Max Partitions: %d", maxPartitions)); + } + + static OutputSettings get(Map clArgs, boolean required) + { + String[] params = clArgs.remove("-output"); + if (params == null) + { + if (required) + { + printHelp(); + throw new IllegalArgumentException("-output is required in generate-sstables mode (directory=, visits=). Use -help for usage information."); + } + return null; + } + + OutputOptions options = new OutputOptions(); + for (String param : params) + { + if (!options.accept(param)) + throw new IllegalArgumentException("Invalid -output parameter: " + param); + } + if (!options.isValid()) + throw new IllegalArgumentException("Missing required -output parameters (directory=, visits=)"); + return new OutputSettings(options); + } + + static void printHelp() + { + System.out.println(); + System.out.println("Usage: -output directory=? visits=? [sstable_size_mb=?] [compression]"); + System.out.println(); + System.out.println(" directory=? Output directory for generated SSTables (required for generate-sstables)"); + System.out.println(" visits=? Total number of visits to generate (required for generate-sstables)"); + System.out.println(" sstable_size_mb=? Max SSTable size in MiB before flushing to a new one (default: 128)"); + System.out.println(" compression Enable SSTable compression (default: disabled)"); + System.out.println(" local_node_id=? Generate only partitions owned by this node (requires -cluster)"); + System.out.println(" max_partitions=? Max number of partitions to write (default: unlimited)"); + } + } + + static class OutputOptions + { + final SimpleOption directory = new SimpleOption("directory=", ".*", null, true); + final SimpleOption visits = new SimpleOption("visits=", "[0-9]+", null, true); + final SimpleOption sstableSizeMb = new SimpleOption("sstable_size_mb=", "[0-9]+", "128", false); + final SimpleOption compression = new SimpleOption("compression", "", null, false); + final SimpleOption localNodeId = new SimpleOption("local_node_id=", ".*", null, false); + final SimpleOption minToken = new SimpleOption("min_token=", "-?[0-9]+", null, false); + final SimpleOption maxToken = new SimpleOption("max_token=", "-?[0-9]+", null, false); + final SimpleOption tokenIndex = new SimpleOption("token_index=", ".*", null, false); + final SimpleOption levels = new SimpleOption("levels=", ".*", null, false); + final SimpleOption maxPartitions = new SimpleOption("max_partitions=", "[0-9]+", null, false); + + private final List all = Arrays.asList(directory, visits, sstableSizeMb, compression, localNodeId, minToken, maxToken, tokenIndex, levels, maxPartitions); + + boolean accept(String param) + { + for (SimpleOption opt : all) + if (opt.accept(param)) + return true; + return false; + } + + boolean isValid() + { + for (SimpleOption opt : all) + if (!opt.isValid()) + return false; + return true; + } + } + + // ========================== Simple Option (lightweight, self-contained) ========================== + + /** + * A lightweight option parser, modeled after cassandra-stress's OptionSimple but self-contained + * so harry-stress doesn't depend on the stress tool module. + */ + static class SimpleOption + { + final String prefix; + final String pattern; + final String defaultValue; + final boolean required; + private String value; + + SimpleOption(String prefix, String pattern, String defaultValue, boolean required) + { + this.prefix = prefix; + this.pattern = pattern; + this.defaultValue = defaultValue; + this.required = required; + } + + boolean accept(String param) + { + String lower = toLowerCaseLocalized(param); + String lowerPrefix = toLowerCaseLocalized(prefix); + + // Flag-style option (no value, e.g. "ssl") + if (pattern.isEmpty()) + { + if (lower.equals(lowerPrefix)) + { + value = ""; + return true; + } + return false; + } + + // Key=value style + if (lower.startsWith(lowerPrefix)) + { + String v = param.substring(prefix.length()); + if (!v.matches(pattern)) + throw new IllegalArgumentException("Invalid value '" + v + "' for " + prefix + "; must match " + pattern); + value = v; + return true; + } + return false; + } + + boolean setByUser() + { + return value != null; + } + + String value() + { + return value != null ? value : defaultValue; + } + + boolean isValid() + { + return !required || value != null; + } + } + + // ========================== Parsing ========================== + + public static HarryStressSettings parse(String[] args) + { + args = repairParams(args); + Map clArgs = parseMap(args); + + if (clArgs.containsKey("-help") || clArgs.containsKey("--help") || clArgs.containsKey("-h")) + { + printHelp(); + System.exit(0); + } + + Mode mode = Mode.get(clArgs); + + ClusterSettings cluster = ClusterSettings.get(clArgs, mode == Mode.STRESS); + SchemaSettings schema = SchemaSettings.get(clArgs); + RateSettings rate = RateSettings.get(clArgs); + RatioSettings ratio = RatioSettings.get(clArgs); + OpsSettings ops = OpsSettings.get(clArgs); + RotationSettings rotation = RotationSettings.get(clArgs); + OutputSettings output = OutputSettings.get(clArgs, mode == Mode.GENERATE_LEVELS || mode == Mode.GENERATE_LEVELLED_SSTABLES); + + if (!clArgs.isEmpty()) + { + printHelp(); + System.out.println("Error processing command line arguments. The following were ignored:"); + for (Map.Entry e : clArgs.entrySet()) + { + System.out.print(e.getKey()); + for (String v : e.getValue()) + { + System.out.print(" "); + System.out.print(v); + } + System.out.println(); + } + System.exit(1); + } + + return new HarryStressSettings(mode, cluster, schema, rate, ratio, rotation, ops, output); + } + + private static String[] repairParams(String[] args) + { + if (args.length == 0) + return args; + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String arg : args) + { + if (!first) + sb.append(" "); + sb.append(arg); + first = false; + } + return sb.toString() + .replaceAll("\\s+([,=()])", "$1") + .replaceAll("([,=(])\\s+", "$1") + .split(" +"); + } + + private static Map parseMap(String[] args) + { + if (args.length == 0) + { + printHelp(); + System.exit(1); + } + final LinkedHashMap r = new LinkedHashMap<>(); + String key = null; + List params = new ArrayList<>(); + for (int i = 0; i < args.length; i++) + { + if (args[i].startsWith("-")) + { + if (key != null) + putParam(key, params.toArray(new String[0]), r); + key = toLowerCaseLocalized(args[i]); + params.clear(); + } + else + { + params.add(args[i]); + } + } + if (key != null) + putParam(key, params.toArray(new String[0]), r); + return r; + } + + private static void putParam(String key, String[] args, Map clArgs) + { + String[] prev = clArgs.put(key, args); + if (prev != null) + throw new IllegalArgumentException(key + " is defined multiple times. Each option can be specified at most once."); + } + + public static void printHelp() + { + System.out.println(); + System.out.println("Usage: harry-stress [options]"); + System.out.println(); + Mode.printHelp(); + ClusterSettings.printHelp(); + SchemaSettings.printHelp(); + RateSettings.printHelp(); + RatioSettings.printHelp(); + OpsSettings.printHelp(); + RotationSettings.printHelp(); + OutputSettings.printHelp(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/config/StressSchemaConfig.java b/test/harry/main/org/apache/cassandra/harry/stress/config/StressSchemaConfig.java new file mode 100644 index 000000000000..7f866a611fcf --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/config/StressSchemaConfig.java @@ -0,0 +1,425 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.harry.stress.config; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.harry.gen.TypeAdapters; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import org.apache.cassandra.config.YamlConfigurationLoader; +import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; +import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.gen.SchemaGenerators; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.harry.stress.RotationStrategy; +import org.apache.cassandra.harry.stress.VisitGenerator; +import org.apache.cassandra.harry.stress.distribution.Distribution; +import org.apache.cassandra.harry.stress.distribution.Distributions; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; + +import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized; + +public class StressSchemaConfig +{ + private final SchemaSpec schema; + private final Distribution rowPopulation; + private final Map columnPopulations; + private final Map valueSizeDistribution; + private final RotationConfig rotation; + + private StressSchemaConfig(SchemaSpec schema, Distribution rowPopulation, Map columnPopulations, Map valueSizeDistribution, RotationConfig rotation) + { + this.schema = schema; + this.rowPopulation = rowPopulation; + this.columnPopulations = columnPopulations; + this.valueSizeDistribution = valueSizeDistribution; + this.rotation = rotation; + } + + public SchemaSpec schema() + { + return schema; + } + + public Distribution rowPopulation() + { + return rowPopulation; + } + + public VisitGenerator.ColumnPopulation columnPopulation() + { + return column -> columnPopulations.getOrDefault(column, Distributions.fixed(100)); + } + + /** + * A fresh {@link RotationStrategy} built from the {@code rotation:} section of the config. Returns a new instance + * on every call because rotation strategies are stateful, so a single config can hand identical-but-independent + * strategies to, e.g., offline generation and a validation replay. + */ + public RotationStrategy rotationStrategy() + { + return rotation.build(); + } + + public static StressSchemaConfig load(Path yamlFile) throws IOException + { + try (InputStream is = Files.newInputStream(yamlFile)) + { + return load(is); + } + } + + public static StressSchemaConfig load(URI uri) throws IOException + { + try (InputStream is = uri.toURL().openStream()) + { + return load(is); + } + } + + public static StressSchemaConfig load(InputStream is) + { + Constructor constructor = new Constructor(StressSchemaYaml.class, YamlConfigurationLoader.getDefaultLoaderOptions()); + Yaml yaml = new Yaml(constructor); + StressSchemaYaml parsed = yaml.loadAs(is, StressSchemaYaml.class); + return fromYaml(parsed); + } + + static StressSchemaConfig fromYaml(StressSchemaYaml yaml) + { + if (yaml.keyspace == null) + throw new IllegalArgumentException("keyspace is required"); + if (yaml.table == null) + throw new IllegalArgumentException("table is required"); + + Map populationSpecs = parseColumnSpecs(yaml.columnspec); + + Map populations = new HashMap<>(); + Map valueSizeDistributions = new HashMap<>(); + for (Map.Entry e : populationSpecs.entrySet()) + { + populations.put(e.getKey(), parseDistribution(e.getValue().population, Distributions.uniformRandom(1, 10))); + valueSizeDistributions.put(e.getKey(), parseDistribution(e.getValue().size, null)); + } + Distribution rowPopulation = parseDistribution(yaml.rows, Distributions.uniformRandom(1, 10)); + + SchemaSpec schema; + if (yaml.table_definition != null) + schema = parseTableDefinition(yaml.keyspace, yaml.table, yaml.table_definition, valueSizeDistributions); + else + schema = generateRandomSchema(yaml.keyspace, yaml.table, yaml.seed); + return new StressSchemaConfig(schema, rowPopulation, populations, valueSizeDistributions, RotationConfig.fromYaml(yaml.rotation)); + } + + private static Map parseColumnSpecs(List> columnspec) + { + Map columnConfigs = new HashMap<>(); + if (columnspec != null) + { + for (Map spec : columnspec) + { + Map entry = new HashMap<>(spec); + lowerCaseKeys(entry); + String name = (String) entry.get("name"); + if (name == null) + throw new IllegalArgumentException("Missing 'name' in columnspec entry"); + String population = (String) entry.get("population"); + String size = (String) entry.get("size"); + if (population != null || size != null) + columnConfigs.put(name, new ColumnConfig(size, population)); + } + } + return columnConfigs; + } + + private static class ColumnConfig + { + final String size; + final String population; + + ColumnConfig(String size, String population) + { + this.size = size; + this.population = population; + } + } + + private static SchemaSpec parseTableDefinition(String keyspace, String table, String tableCql, Map valueSizeDistribution) + { + TableMetadata metadata = CreateTableStatement.parse(tableCql, keyspace).build(); + + List> pks = new ArrayList<>(); + for (ColumnMetadata cm : metadata.partitionKeyColumns()) + { + pks.add(ColumnSpec.pk(cm.name.toString(), + resolveServerType(cm.type), + TypeAdapters.forValues(cm.type, valueSizeDistribution.get(cm.name.toString())))); + } + + List> cks = new ArrayList<>(); + for (ColumnMetadata cm : metadata.clusteringColumns()) + { + cks.add(ColumnSpec.ck(cm.name.toString(), + resolveServerType(cm.type.unwrap()), + TypeAdapters.forValues(cm.type.unwrap(), valueSizeDistribution.get(cm.name.toString())), + cm.type.isReversed())); + } + + List> regulars = new ArrayList<>(); + List> statics = new ArrayList<>(); + Iterator it = metadata.allColumnsInSelectOrder(); + while (it.hasNext()) + { + ColumnMetadata cm = it.next(); + if (cm.isRegular()) + { + regulars.add(ColumnSpec.regularColumn(cm.name.toString(), + resolveServerType(cm.type), + TypeAdapters.forValues(cm.type, valueSizeDistribution.get(cm.name.toString())))); + } + else if (cm.isStatic()) + { + statics.add(ColumnSpec.staticColumn(cm.name.toString(), + resolveServerType(cm.type), + TypeAdapters.forValues(cm.type, valueSizeDistribution.get(cm.name.toString())))); + } + } + + SchemaSpec.Options options = SchemaSpec.optionsBuilder() + .ifNotExists(true) + .addWriteTimestamps(true) + // carry the compaction strategy (e.g. LeveledCompactionStrategy) through from the CQL + .compactionStrategy(metadata.params.compaction.klass().getSimpleName()); + + return new SchemaSpec(keyspace, table, pks, cks, regulars, statics, options); + } + + private static SchemaSpec generateRandomSchema(String keyspace, String table, Long seed) + { + long s = seed != null ? seed : System.nanoTime(); + EntropySource rng = new JdkRandomEntropySource(s); + return SchemaGenerators.schemaSpecGen(keyspace, table, 100).generate(rng); + } + + @SuppressWarnings("rawtypes") + static ColumnSpec.DataType resolveServerType(AbstractType serverType) + { + if (serverType instanceof ReversedType) + serverType = ((ReversedType) serverType).baseType; + + if (serverType instanceof AsciiType) return ColumnSpec.asciiType; + if (serverType instanceof UTF8Type) return ColumnSpec.textType; + if (serverType instanceof LongType) return ColumnSpec.int64Type; + if (serverType instanceof Int32Type) return ColumnSpec.int32Type; + if (serverType instanceof ShortType) return ColumnSpec.int16Type; + if (serverType instanceof ByteType) return ColumnSpec.int8Type; + if (serverType instanceof BooleanType) return ColumnSpec.booleanType; + if (serverType instanceof FloatType) return ColumnSpec.floatType; + if (serverType instanceof DoubleType) return ColumnSpec.doubleType; + if (serverType instanceof BytesType) return ColumnSpec.blobType; + if (serverType instanceof UUIDType) return ColumnSpec.uuidType; + if (serverType instanceof TimeUUIDType) return ColumnSpec.timeUuidType; + if (serverType instanceof TimestampType) return ColumnSpec.timestampType; + if (serverType instanceof IntegerType) return ColumnSpec.varintType; + if (serverType instanceof DecimalType) return ColumnSpec.decimalType; + if (serverType instanceof InetAddressType) return ColumnSpec.inetType; + if (serverType instanceof TimeType) return ColumnSpec.timeType; + + throw new IllegalArgumentException("Unsupported column type: " + serverType.asCQL3Type()); + } + + public static Distribution parseDistribution(String spec, Distribution onNull) + { + if (spec == null) return onNull; + spec = spec.trim(); + String lower = toLowerCaseLocalized(spec); + + int parenOpen = lower.indexOf('('); + if (parenOpen < 0 || !lower.endsWith(")")) + throw new IllegalArgumentException("Invalid distribution spec: " + spec); + + String name = lower.substring(0, parenOpen); + String args = spec.substring(parenOpen + 1, spec.length() - 1).trim(); + + switch (name) + { + case "fixed": + return Distributions.fixed(parseLong(args)); + case "uniform": + { + long[] range = parseRange(args); + return Distributions.uniformRandom(range[0], range[1]); + } + case "cdf": + { + String[] vs = args.split(","); + float[] chances = new float[vs.length - 1]; + long[] bounds = new long[vs.length]; + for (int i = 0 ; i < vs.length ; ++i) + { + String[] v = vs[i].split(":"); + if (i == 0 && chances[i] != 0f) + throw new IllegalArgumentException("Must specify a mapping for 0; first is " + vs[i]); + + if (i < vs.length - 1) chances[i] = Float.parseFloat(v[0]); + else if (Float.parseFloat(v[0]) != 1f) + throw new IllegalArgumentException("Must specify a mapping for 1; last is " + vs[i]); + + + bounds[i] = Long.parseLong(v[1]); + } + return new Distributions.CDF(chances, bounds); + } + case "gaussian": + case "gauss": + case "normal": + case "norm": + { + String[] parts = args.split(","); + long[] range = parseRange(parts[0].trim()); + // TODO: use a proper gaussian distribution once available + return Distributions.fixed(range[0] + (range[1] - range[0]) / 2); + } + default: + throw new IllegalArgumentException("Unsupported distribution type: " + name + + ". Supported: fixed, uniform, gaussian"); + } + } + + static long[] parseRange(String rangeSpec) + { + String[] bounds = rangeSpec.split("\\.\\.+"); + if (bounds.length != 2) + throw new IllegalArgumentException("Expected range in form min..max, got: " + rangeSpec); + return new long[]{ parseLong(bounds[0].trim()), parseLong(bounds[1].trim()) }; + } + + static long parseLong(String value) + { + value = toLowerCaseLocalized(value.trim()); + long multiplier = 1; + if (value.endsWith("b")) + { + multiplier = 1_000_000_000L; + value = value.substring(0, value.length() - 1); + } + else if (value.endsWith("m")) + { + multiplier = 1_000_000L; + value = value.substring(0, value.length() - 1); + } + else if (value.endsWith("k")) + { + multiplier = 1_000L; + value = value.substring(0, value.length() - 1); + } + return Long.parseLong(value) * multiplier; + } + + private static void lowerCaseKeys(Map map) + { + List keys = new ArrayList<>(map.keySet()); + for (String key : keys) + { + String lower = toLowerCaseLocalized(key); + if (!lower.equals(key)) + { + Object val = map.remove(key); + map.put(lower, val); + } + } + } + + /** Parsed, default-applied rotation parameters; {@link #build()} yields a fresh (stateful) strategy per call. */ + static final class RotationConfig + { + final String strategy; + final int target; + final int replaceWithNew; + final int replaceWithVisited; + final int switchInterval; + + RotationConfig(String strategy, int target, int replaceWithNew, int replaceWithVisited, int switchInterval) + { + this.strategy = strategy; + this.target = target; + this.replaceWithNew = replaceWithNew; + this.replaceWithVisited = replaceWithVisited; + this.switchInterval = switchInterval; + } + + static RotationConfig fromYaml(RotationYaml yaml) + { + if (yaml == null) + return new RotationConfig("fixed", 2000, 0, 0, 500); + return new RotationConfig(yaml.strategy != null ? toLowerCaseLocalized(yaml.strategy.trim()) : "fixed", + yaml.target != null ? yaml.target : 2000, + yaml.replace_with_new != null ? yaml.replace_with_new : 0, + yaml.replace_with_visited != null ? yaml.replace_with_visited : 0, + yaml.partition_switch_interval != null ? yaml.partition_switch_interval : 500); + } + + RotationStrategy build() + { + switch (strategy) + { + case "fixed": + return new RotationStrategy.FixedRotationStrategy(target, replaceWithNew, replaceWithVisited, switchInterval); + case "random": + return new RotationStrategy.RandomRotationStrategy(target, switchInterval); + default: + throw new IllegalArgumentException("Unknown rotation strategy: " + strategy + ". Expected 'fixed' or 'random'."); + } + } + } + + public static class StressSchemaYaml + { + public String keyspace; + public String table; + public String table_definition; + public Long seed; + public List> columnspec; + public String rows; + public RotationYaml rotation; + } + + public static class RotationYaml + { + public String strategy; + public Integer target; + public Integer replace_with_new; + public Integer replace_with_visited; + public Integer partition_switch_interval; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/distribution/Distribution.java b/test/harry/main/org/apache/cassandra/harry/stress/distribution/Distribution.java new file mode 100644 index 000000000000..21615cf1edab --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/distribution/Distribution.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.distribution; + +public interface Distribution +{ + long next(long seed); + // TODO: nextInt + double nextDouble(long seed); + + long min(); + long max(); +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/distribution/DistributionFactory.java b/test/harry/main/org/apache/cassandra/harry/stress/distribution/DistributionFactory.java new file mode 100644 index 000000000000..9e5437c2339f --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/distribution/DistributionFactory.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.distribution; + + +import java.io.Serializable; + +import org.apache.cassandra.harry.gen.EntropySource; + +public interface DistributionFactory extends Serializable +{ + Distribution get(EntropySource rng); + String getConfigAsString(); +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/distribution/Distributions.java b/test/harry/main/org/apache/cassandra/harry/stress/distribution/Distributions.java new file mode 100644 index 000000000000..9edca8c8476d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/distribution/Distributions.java @@ -0,0 +1,782 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.distribution; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.commons.math3.distribution.AbstractRealDistribution; +import org.apache.commons.math3.distribution.ExponentialDistribution; +import org.apache.commons.math3.distribution.NormalDistribution; +import org.apache.commons.math3.distribution.UniformRealDistribution; +import org.apache.commons.math3.distribution.WeibullDistribution; +import org.apache.commons.math3.random.RandomGenerator; + +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.rng.SeedableEntropySource; + +public class Distributions +{ + public static Distribution fixed(long v) + { + return new Fixed(v); + } + + public static Distribution exp(EntropySource rng, long min, long max, double mean) + { + return new Exp(rng, min, max, mean); + } + + public static Distribution extreme(EntropySource rng, long min, long max, double shape, double scale) + { + return new Extreme(rng, min, max, shape, scale); + } + + public static Distribution offset(Distribution distribution, long offset) + { + return new OffsetDistribution(distribution, offset); + } + + public static Distribution quantizedExtreme(EntropySource rng, long min, long max, double shape, double scale, int quantas) + { + return new Quantized(new Extreme(rng, min, max, shape, scale), quantas); + } + + public static Distribution gaussian(EntropySource rng, long min, long max, double mean, double stdev) + { + return new Gaussian(rng, min, max, mean, stdev); + } + + public static Distribution binomial(EntropySource rng, int n, double p) + { + return new Binomial(n, p); + } + + public static Distribution zipf(EntropySource rng, int total, double skew) + { + return new Zipf(total, skew); + } + + public static Distribution biased(EntropySource rng, long min, long max, double bias) + { + return new Biased(min, max, bias); + } + + public static Distribution uniform(EntropySource rng, long min, long max) + { + return new Uniform(rng, min, max); + } + + public static Distribution uniformRandom(long min, long max) + { + return new UniformRandom(min, max); + } + + public static Distribution sequence(long start, long end) + { + return new Sequence(start, end); + } + + public static Distribution invert(Distribution distribution) + { + if (distribution instanceof Inverted) + return ((Inverted) distribution).wrapped; + return new Inverted(distribution); + } + + public static class Fixed implements Distribution + { + private final long value; + + public Fixed(long value) + { + this.value = value; + } + + @Override + public long next(long seed) + { + return value; + } + + @Override + public double nextDouble(long seed) + { + return (double) value; + } + + @Override + public long min() + { + return value; + } + + @Override + public long max() + { + return value; + } + } + + public static class Exp extends ApacheAdaptor + { + private final long min; + private final long max; + + public Exp(EntropySource rng, long min, long max, double mean) + { + super(new ExponentialDistribution(new EntropySourceAdapter(rng), mean, ExponentialDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max); + this.min = min; + this.max = max; + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class Extreme extends ApacheAdaptor + { + private final long min; + private final long max; + + public Extreme(EntropySource rng, long min, long max, double shape, double scale) + { + super(new WeibullDistribution(new EntropySourceAdapter(rng), shape, scale, WeibullDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max); + this.min = min; + this.max = max; + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class OffsetDistribution implements Distribution + { + private final Distribution base; + private final long offset; + + public OffsetDistribution(Distribution base, long offset) + { + this.base = base; + this.offset = offset; + } + + @Override + public long next(long seed) + { + return base.next(seed) + offset; + } + + @Override + public double nextDouble(long seed) + { + return base.nextDouble(seed) + offset; + } + + @Override + public long min() + { + return base.min() + offset; + } + + @Override + public long max() + { + return base.max() + offset; + } + } + + public static class Quantized implements Distribution + { + final Distribution delegate; + final long[] bounds; + + public Quantized(ApacheAdaptor delegate, int quantas) + { + this.delegate = delegate; + this.bounds = new long[quantas + 1]; + bounds[0] = delegate.min; + bounds[quantas] = delegate.max + 1; + for (int i = 1; i < quantas; i++) + bounds[i] = delegate.inverseCumProb(i / (double) quantas); + } + + @Override + public long next(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, (rng) -> { + int quanta = quanta(delegate.next(seed)); + return bounds[quanta] + (long) (rng.nextDouble() * ((bounds[quanta + 1] - bounds[quanta]))); + }); + } + + @Override + public double nextDouble(long seed) + { + throw new UnsupportedOperationException(); + } + + @Override + public long min() + { + return delegate.min(); + } + + @Override + public long max() + { + return delegate.max(); + } + + int quanta(long val) + { + int i = Arrays.binarySearch(bounds, val); + if (i < 0) + return -2 - i; + return i - 1; + } + } + + public static class Gaussian extends ApacheAdaptor + { + public Gaussian(EntropySource rng, long min, long max, double mean, double stdev) + { + super(new NormalDistribution(new EntropySourceAdapter(rng), mean, stdev, NormalDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max); + } + } + + // Models the number of successes in n independent trials + public static class Binomial implements Distribution + { + private final int n; + private final double p; + private final long min; + private final long max; + + public Binomial(int n, double p) + { + // number of trials + this.n = n; + // probability of success + this.p = p; + this.min = 0; + // Max would be when all trials succeed + this.max = n; + } + + @Override + public long next(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, rng -> { + long successes = 0; + for (int i = 0; i < n; i++) + { + if (rng.nextDouble() < p) + { + successes++; + } + } + return successes; + }); + } + + @Override + public double nextDouble(long seed) + { + return (double) next(seed) / n; + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class Zipf implements Distribution + { + private final int n; + private final double[] probabilities; + private final long min; + private final long max; + + public Zipf(int total, double skew) + { + if (total <= 0) throw new IllegalArgumentException("n must be positive"); + if (skew <= 0) throw new IllegalArgumentException("s must be positive"); + + this.n = total; + this.min = 1; + this.max = total; + + this.probabilities = new double[total]; + double sum = 0; + for (int i = 0; i < total; i++) + { + probabilities[i] = 1.0 / Math.pow(i + 1, skew); + sum += probabilities[i]; + } + + // normalize + for (int i = 0; i < total; i++) + { + probabilities[i] /= sum; + if (i > 0) + probabilities[i] += probabilities[i - 1]; + } + } + + @Override + public long next(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, rng -> { + double rand = rng.nextDouble(); + long index = Arrays.binarySearch(probabilities, rand); + if (index < 0) + index = -(index + 1); + return index + 1; + }); + } + + + @Override + public double nextDouble(long seed) + { + return (double) next(seed) / n; + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class Biased implements Distribution + { + private final long min; + private final long max; + private final double bias; + + public Biased(long min, long max, double bias) + { + this.min = min; + this.max = max; + this.bias = bias; + } + + @Override + public long next(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, rng -> { + // TODO (desired): try and compare with a variant without exponentiation + // f(x)=ax^p+b + double v = rng.nextDouble(); + v = Math.pow(v, bias); + return min + (long) (v * (max - min)); + }); + } + + @Override + public double nextDouble(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, rng -> { + double raw = rng.nextDouble(); + return Math.pow(raw, bias); + }); + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class Uniform extends ApacheAdaptor + { + public Uniform(EntropySource rng, long min, long max) + { + super(new UniformRealDistribution(new EntropySourceAdapter(rng), min, max + 1), min, max); + } + } + + /** + * A lightweight uniform random distribution that derives values purely from the seed, + * without requiring a stateful EntropySource or Apache Commons Math. + */ + public static class UniformRandom implements Distribution + { + private final long min; + private final long max; + private final long range; + + public UniformRandom(long min, long max) + { + if (min > max) + throw new IllegalArgumentException("min (" + min + ") must be <= max (" + max + ")"); + this.min = min; + this.max = max; + this.range = max - min + 1; + } + + @Override + public long next(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, rng -> min + Math.abs(rng.next() % range)); + } + + @Override + public double nextDouble(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, rng -> min + rng.nextDouble() * (max - min)); + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class CDF implements Distribution + { + private final float[] chances; + private final long[] bounds; + + public CDF(float[] chances, long[] bounds) + { + this.chances = chances; + this.bounds = bounds; + } + + @Override + public long next(long seed) + { + return SeedableEntropySource.computeWithSeed(seed, rng -> { + float f = rng.nextFloat(); + int i = Arrays.binarySearch(chances, f); + if (i < 0) i = -1 - i; + if (i == 0) + return bounds[0]; + + return rng.nextLong(bounds[i - 1], bounds[i] + 1); + }); + } + + @Override + public double nextDouble(long seed) + { + return next(seed); + } + + @Override + public long min() + { + return bounds[0]; + } + + @Override + public long max() + { + return bounds[bounds.length - 1]; + } + } + + public static abstract class ApacheAdaptor implements Distribution + { + final AbstractRealDistribution delegate; + final long min, max, delta; + + public ApacheAdaptor(AbstractRealDistribution delegate, long min, long max) + { + this.delegate = delegate; + this.min = min; + this.max = max; + this.delta = max - min; + } + + public void setSeed(long seed) + { + delegate.reseedRandomGenerator(seed); + } + + @Override + public synchronized long next(long seed) + { + delegate.reseedRandomGenerator(seed); + return offset(min, delta, delegate.sample()); + } + + @Override + public synchronized double nextDouble(long seed) + { + delegate.reseedRandomGenerator(seed); + return offsetDouble(min, delta, delegate.sample()); + } + + public long inverseCumProb(double cumProb) + { + return offset(min, delta, delegate.inverseCumulativeProbability(cumProb)); + } + + private long offset(long min, long delta, double val) + { + long r = (long) val; + if (r < 0) + r = 0; + if (r > delta) + r = delta; + return min + r; + } + + private double offsetDouble(long min, long delta, double r) + { + if (r < 0) + r = 0; + if (r > delta) + r = delta; + return min + r; + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class Sequence implements Distribution + { + private final long start; + private final long totalCount; + private final AtomicLong next = new AtomicLong(); + + public Sequence(long start, long end) + { + if (start > end) + throw new IllegalStateException(); + this.start = start; + this.totalCount = 1 + end - start; + } + + private long nextWithWrap() + { + long next = this.next.getAndIncrement(); + return start + (next % totalCount); + } + + @Override + public long next(long seed) + { + return nextWithWrap(); + } + + @Override + public double nextDouble(long seed) + { + return nextWithWrap(); + } + + @Override + public long min() + { + return start; + } + + @Override + public long max() + { + return start + totalCount; + } + } + + public static class Inverted implements Distribution + { + final Distribution wrapped; + final long min; + final long max; + + public Inverted(Distribution wrapped) + { + this.wrapped = wrapped; + this.min = wrapped.min(); + this.max = wrapped.max(); + } + + @Override + public long next(long seed) + { + return max - (wrapped.next(seed) - min); + } + + @Override + public double nextDouble(long seed) + { + return max - (wrapped.nextDouble(seed) - min); + } + + @Override + public long min() + { + return min; + } + + @Override + public long max() + { + return max; + } + } + + public static class EntropySourceAdapter implements RandomGenerator + { + private final EntropySource entropySource; + + public EntropySourceAdapter(EntropySource entropySource) + { + this.entropySource = entropySource; + } + + @Override + public void setSeed(int seed) + { + entropySource.seed(seed); + } + + @Override + public void setSeed(int[] seed) + { + // Convert the int array to a long seed + long longSeed = 0; + for (int s : seed) + { + longSeed = longSeed * 31 + s; + } + entropySource.seed(longSeed); + } + + @Override + public void setSeed(long seed) + { + entropySource.seed(seed); + } + + @Override + public void nextBytes(byte[] bytes) + { + for (int i = 0; i < bytes.length; i++) + { + bytes[i] = (byte) entropySource.nextInt(256); + } + } + + @Override + public int nextInt() + { + return entropySource.nextInt(); + } + + @Override + public int nextInt(int n) + { + return entropySource.nextInt(n); + } + + @Override + public long nextLong() + { + return entropySource.next(); + } + + @Override + public boolean nextBoolean() + { + return entropySource.nextBoolean(); + } + + @Override + public float nextFloat() + { + return entropySource.nextFloat(); + } + + @Override + public double nextDouble() + { + return entropySource.nextDouble(); + } + + @Override + public double nextGaussian() + { + double u = nextDouble(); + double v = nextDouble(); + return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/stress/distribution/RatioDistribution.java b/test/harry/main/org/apache/cassandra/harry/stress/distribution/RatioDistribution.java new file mode 100644 index 000000000000..0f2409a2dba1 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/distribution/RatioDistribution.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.distribution; + +public class RatioDistribution +{ + + final Distribution distribution; + final double divisor; + + public RatioDistribution(Distribution distribution, double divisor) + { + this.distribution = distribution; + this.divisor = divisor; + } + + // yields a value between 0 and 1 + public double next() + { + throw new IllegalStateException(); +// return Math.max(0f, Math.min(1f, distribution.nextDouble() / divisor)); + } + + public double min() + { + return Math.min(1d, distribution.min() / divisor); + } + + public double max() + { + return Math.min(1d, distribution.max() / divisor); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/distribution/RatioDistributionFactory.java b/test/harry/main/org/apache/cassandra/harry/stress/distribution/RatioDistributionFactory.java new file mode 100644 index 000000000000..9e52b0e37beb --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/distribution/RatioDistributionFactory.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.distribution; + + +import java.io.Serializable; + +import org.apache.cassandra.harry.gen.EntropySource; + +public interface RatioDistributionFactory extends Serializable +{ + + RatioDistribution get(EntropySource rng); + String getConfigAsString(); + +} diff --git a/test/harry/main/org/apache/cassandra/harry/stress/test/HarryCcmStressTest.java b/test/harry/main/org/apache/cassandra/harry/stress/test/HarryCcmStressTest.java new file mode 100644 index 000000000000..9c022cc9e71e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/test/HarryCcmStressTest.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.test; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.QueryOptions; +import com.datastax.driver.core.Session; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.checker.TestHelper; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Generators; +import org.apache.cassandra.harry.stress.ExternalClusterSut; +import org.apache.cassandra.harry.stress.HarryStress; +import org.apache.cassandra.harry.stress.RotationStrategy; +import org.apache.cassandra.harry.stress.VisitGenerator; +import org.apache.cassandra.harry.stress.distribution.Distributions; +import org.apache.cassandra.service.consensus.TransactionalMode; + +public class HarryCcmStressTest +{ + public static final Logger LOGGER = LoggerFactory.getLogger(HarryCcmStressTest.class); + + @Test + public void stressTest() throws Throwable + { + main(); + } + + public static Generator trivialSchema(String ks, String table, SchemaSpec.Options options) + { + return (rng) -> { + List> pks = new ArrayList<>(); + for (int i = 0; i < 2; i++) + pks.add(ColumnSpec.pk("pk" + i, ColumnSpec.asciiType, Generators.ascii(10, 20))); + + List> cks = new ArrayList<>(); + for (int i = 0; i < 2; i++) + cks.add(ColumnSpec.ck("ck" + i, ColumnSpec.asciiType, Generators.ascii(10, 20), false)); + + List> regularColumns = new ArrayList<>(); + for (int i = 0; i < 2; i++) + regularColumns.add(ColumnSpec.regularColumn("regular" + i, ColumnSpec.asciiType, Generators.ascii(10, 20))); + + List> staticColumns = new ArrayList<>(); + for (int i = 0; i < 2; i++) + staticColumns.add(ColumnSpec.staticColumn("static" + i, ColumnSpec.asciiType, Generators.ascii(10, 200))); + + return new SchemaSpec(ks, table, + pks, cks, regularColumns, staticColumns, + options); + }; + } + + public static void main(String... args) + { + TestHelper.withRandom(1, rng -> { + SchemaSpec schema = trivialSchema("ks" + System.currentTimeMillis(), "tbl", + SchemaSpec.optionsBuilder() + .withTransactionalMode(TransactionalMode.full) + .addWriteTimestamps(false) + .build()) + .generate(rng); + // TODO: move + { + Session sut = new Cluster.Builder() + .addContactPoint("127.0.0.1") + .withPort(9042) + .withQueryOptions(new QueryOptions().setConsistencyLevel(com.datastax.driver.core.ConsistencyLevel.QUORUM)) + .build() + .connect(); + sut.execute(String.format("CREATE KEYSPACE %s WITH replication = { 'class' : 'org.apache.cassandra.locator.NetworkTopologyStrategy', 'datacenter1': '1' };", + //// cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};", +// "ks")); + + schema.keyspace)); +// sut.execute(String.format("create keyspace %s with replication = {'class':'SimpleStrategy', 'replication_factor':1}", +// schema.keyspace)); + + sut.execute(schema.compile()); + } + + HarryStress visitGenerator = new HarryStress(schema, + Distributions.fixed(1), + column -> Distributions.fixed(100), + Generators.pick(VisitGenerator.VisitType.values()), + Distributions.fixed(1), + new VisitGenerator.RandomOpKindGenFactory(), + new RotationStrategy.RandomRotationStrategy(100), + null, 30, + () -> { + Session sut = new Cluster.Builder() + .addContactPoint("127.0.0.1") + .withPort(9042) + .withQueryOptions(new QueryOptions().setConsistencyLevel(com.datastax.driver.core.ConsistencyLevel.QUORUM)) + .build() + .connect(); + + return (statement, run) -> { + while (true) + { + try + { + Object[][] rs = ExternalClusterSut.resultSetToObjectArray(sut.execute(statement.cql(), + statement.bindings())); + if (run != null) + run.run(); + return rs; + } + catch (Throwable t) + { + t.printStackTrace(); + LOGGER.error("Failed to execute statement, sleeping before retrying", t); + sleepUninterruptibly(100); + } + } + }; + }, + 2, + 10_000, + 0, + Long.MAX_VALUE, + 0); + + visitGenerator.start(Long.MAX_VALUE, Long.MAX_VALUE); + }); + } + + private static void sleepUninterruptibly(long millis) + { + try + { + Thread.sleep(millis); + } + catch (InterruptedException e) + { + return; + } + } +} + diff --git a/test/harry/main/org/apache/cassandra/harry/stress/test/VisitGeneratorTest.java b/test/harry/main/org/apache/cassandra/harry/stress/test/VisitGeneratorTest.java new file mode 100644 index 000000000000..4263e98f6cab --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/stress/test/VisitGeneratorTest.java @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.stress.test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IMessage; +import org.apache.cassandra.distributed.api.IMessageFilters; +import org.apache.cassandra.distributed.test.log.FuzzTestBase; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.checker.TestHelper; +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.harry.stress.ActivePartition; +import org.apache.cassandra.harry.stress.HarryStress; +import org.apache.cassandra.harry.stress.RotationStrategy; +import org.apache.cassandra.harry.stress.VisitGenerator; +import org.apache.cassandra.harry.stress.distribution.Distributions; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.service.consensus.TransactionalMode; + +/** + * A simple and lightweight way to test visit generator _without_ starting a server and executing any queries + */ +public class VisitGeneratorTest +{ + @Test + public void visitGeneratorTest() + { + TestHelper.withRandom(1,rng -> { + SchemaSpec schema = SchemaGenerators.trivialSchema("ks", "tbl").generate(rng); + + VisitGenerator visitGenerator = new VisitGenerator(new ActivePartition.Partitions(schema, Distributions.fixed(1), column -> Distributions.fixed(100), new RotationStrategy.RandomRotationStrategy(100)), + Generators.pick(VisitGenerator.VisitType.values()), + Distributions.fixed(1), + new VisitGenerator.RandomOpKindGenFactory()); + + }); + } + + @Test + public void descriptorConversionTest() throws Throwable + { + ActivePartition.DescriptorIndexBijection converter = ActivePartition.DescriptorIndexBijection.INSTANCE; + for (int i = 0; i < 100; i++) + { + long pd = converter.toPd(i); + long idx = converter.toIdx(pd); + Assert.assertEquals(idx, idx); + } + } + + @Test + public void stressTest() throws Throwable + { + FuzzTestBase tester = new FuzzTestBase(); + + try(Cluster cluster = tester.builder().withNodes(3) + .withConfig(cfg -> cfg.set("accord.enabled", true)) + .start()) + { + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};", + "ks")); + + AtomicInteger schemaIdx = new AtomicInteger(); + for (int i = 0; i < 1; i++) + { + int finalI = i; + new Thread(() -> { + TestHelper.withRandom(finalI, rng -> { + SchemaSpec schema = SchemaGenerators.trivialSchema("ks", "tbl" + finalI, + SchemaSpec.optionsBuilder() + .withTransactionalMode(TransactionalMode.full) + .addWriteTimestamps(false) + .build()) + .generate(rng); + cluster.schemaChange(schema.compile()); + HarryStress visitGenerator = new HarryStress(schema, + Distributions.fixed(1), + column -> Distributions.fixed(100), + Generators.pick(VisitGenerator.VisitType.values()), + Distributions.fixed(1), + new VisitGenerator.RandomOpKindGenFactory(), + new RotationStrategy.RandomRotationStrategy(100), + null, 30, + () -> (statement, run) -> { + while (true) + { + try + { + Object[][] rs = cluster.coordinator(1).execute(statement.cql(), + ConsistencyLevel.QUORUM, + statement.bindings()); + + if (run != null) + run.run(); + return rs; + } + catch (Throwable t) + { + // retry — failures are expected when node 2 bounces + } + } + }, + 20, + 20_000, + 0, + Long.MAX_VALUE, + 0); + + try + { + visitGenerator.start(Long.MAX_VALUE, Long.MAX_VALUE); + } + catch (Throwable t) + { + t.printStackTrace(); + System.out.println("Exiting"); + } + }); + }, "stress-" + i).start(); + } + + Thread.sleep(10_000); + cluster.filters().allVerbs().messagesMatching(new IMessageFilters.Matcher() + { + class Msg + { + final int from; + final int to; + final int id; + + Msg(int from, int to, int id) + { + this.from = from; + this.to = to; + this.id = id; + } + + @Override + public boolean equals(Object o) + { + if (o == null || getClass() != o.getClass()) return false; + Msg msg = (Msg) o; + return from == msg.from && to == msg.to && id == msg.id; + } + + @Override + public int hashCode() + { + return Objects.hash(from, to, id); + } + + @Override + public String toString() + { + return "Msg{" + + "from=" + from + + ", to=" + to + + ", id=" + id + + '}'; + } + } + + private final Set set = new HashSet<>(30_000); + private final List sent = new ArrayList<>(30_000); + private final Lock lock = new ReentrantLock(); + private int head = 0; + @Override + public boolean matches(int from, int to, IMessage message) + { + Msg msg = new Msg(from, to, message.id()); + lock.lock(); + try + { + if (set.contains(msg)) + System.out.println("Already contains " + msg); + if (sent.size() < sent.size() + 1) { + // Buffer not full yet, simply add + sent.add(msg); + } else { + // Buffer full, replace at head position + set.remove(sent.set(head, msg)); + // Move head pointer + head = (head + 1) % sent.size(); + } + } + finally + { + lock.unlock(); + } + + if (!Verb.fromId(message.verb()).name().contains("ACCORD")) + return false; + + if (ThreadLocalRandom.current().nextInt(100) > 98) + return true; + + return false; + } + }).drop().on(); + while (true) + { + + SchemaSpec schema = HarryCcmStressTest.trivialSchema("ks", "tbl" + schemaIdx.getAndIncrement(), + SchemaSpec.optionsBuilder() + .withTransactionalMode(TransactionalMode.full) + .addWriteTimestamps(false) + .build()) + .generate(new JdkRandomEntropySource(schemaIdx.get())); + cluster.schemaChange(schema.compile()); + + Thread.sleep(5_000); + cluster.forEach(i -> { + if (i.config().num() != 2) + return; + + i.shutdown(); + i.startup(); + }); + } + } + } + + + @Test + public void randomRotationStrategyTest() + { + RotationStrategy strategy = new RotationStrategy.RandomRotationStrategy(10); + + TestHelper.withRandom(rng -> { + int size = strategy.targetSize(); + for (int i = 0; i < 1000; i++) + { + for (RotationStrategy.PartitionAction action : strategy.generate(rng)) + { + switch (action) + { + case REPLACE_WITH_NEW: + case REPLACE_WITH_VISITED: + // size stays the same — one out, one in + break; + } + } + System.out.println(size); + } + }); + + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/test/CQLTesterHistoryBuilderTest.java b/test/harry/main/org/apache/cassandra/harry/test/CQLTesterHistoryBuilderTest.java new file mode 100644 index 000000000000..1ce7aba6a7d8 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/CQLTesterHistoryBuilderTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.test; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +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.model.QuiescentChecker; +import org.apache.cassandra.harry.op.Visit; + +public class CQLTesterHistoryBuilderTest extends HistoryBuilderTest +{ + private final Tester tester; + + public CQLTesterHistoryBuilderTest() + { + this.tester = new Tester(); + } + + @BeforeClass + public static void setUpClass() + { + CQLTester.setUpClass(); + } + + @AfterClass + public static void tearDownClass() + { + CQLTester.tearDownClass(); + } + + @Before + public void beforeTest() throws Throwable + { + tester.beforeTest(); + } + + @After + public void afterTest() throws Throwable + { + tester.afterTest(); + } + + @Override + protected String keyspace() + { + return CQLTester.KEYSPACE; + } + + @Override + protected void createTable(String schema) + { + tester.createTable(schema); + } + + @Override + protected void flush(String keyspace, String table) + { + tester.flush(keyspace, table); + } + + @Override + public void replay(SchemaSpec schema, HistoryBuilder historyBuilder) + { + CQLVisitExecutor executor = create(schema, historyBuilder); + for (Visit visit : historyBuilder) + executor.execute(visit); + } + + @Override + public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder) + { + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, historyBuilder.valueGenerators(), tracker, + new QuiescentChecker(historyBuilder.valueGenerators(), tracker), + statement -> { + if (logger.isTraceEnabled()) + logger.trace(statement.toString()); + return tester.execute(statement.cql(), statement.bindings()); + }); + } + + private static class Tester extends CQLTester + { + @Override + public String createTable(String query) + { + return super.createTable(query); + } + + @Override + public UntypedResultSet execute(String query, Object... values) + { + return super.execute(query, values); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java index 983215f20d41..9ee778d894f8 100644 --- a/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java +++ b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java @@ -86,9 +86,7 @@ public void perTestSetup() throws IOException } private final Generator simple_schema = rng -> { - return new SchemaSpec(rng.next(), - 1000, - keyspace, + return new SchemaSpec(keyspace, table, Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), ColumnSpec.pk("pk2", ColumnSpec.int64Type)), @@ -149,7 +147,7 @@ public void testFlushAndCompactOnce(int flushcount) throws IOException schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace)); createTable(schema.compile()); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = HistoryBuilder.fromSchema(schema, rng.next(), 1000); history.customThrowing(() -> { ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); cfs.disableAutoCompaction(); @@ -211,9 +209,9 @@ public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) { - DataTracker tracker = new DataTracker.SequentialDataTracker(); - return new CQLTesterVisitExecutor(schema, tracker, - new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder), + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, historyBuilder.valueGenerators(), tracker, + new QuiescentChecker(historyBuilder.valueGenerators(), tracker), statement -> { if (logger.isTraceEnabled()) logger.trace(statement.toString()); @@ -242,7 +240,7 @@ protected void executeValidatingVisit(Visit visit, List 1) + if (visit.visitedPartitions.length > 1) throw new IllegalStateException("SSTable Generator does not support batch statements and transactions"); super.execute(visit); diff --git a/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java index c3b1e9408654..cd8aa6e9d74d 100644 --- a/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java +++ b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java @@ -88,9 +88,7 @@ public void perTestSetup() throws IOException } private final Generator schemaSpecGenerator = rng -> { - return new SchemaSpec(rng.next(), - 1000, - keyspace, + return new SchemaSpec(keyspace, table, Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), ColumnSpec.pk("pk2", ColumnSpec.int64Type)), @@ -151,7 +149,7 @@ public void testFlushAndCompactOnce(int flushcount) throws IOException schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace)); createTable(schema.compile()); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = HistoryBuilder.fromSchema(schema, rng.next(), 1000); history.customThrowing(() -> { ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); cfs.disableAutoCompaction(); @@ -216,9 +214,9 @@ public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) { - DataTracker tracker = new DataTracker.SequentialDataTracker(); - return new CQLTesterVisitExecutor(schema, tracker, - new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder), + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, historyBuilder.valueGenerators(), tracker, + new QuiescentChecker(historyBuilder.valueGenerators(), tracker), statement -> { if (logger.isTraceEnabled()) logger.trace(statement.toString()); @@ -247,7 +245,7 @@ protected void executeValidatingVisit(Visit visit, List 1) + if (visit.visitedPartitions.length > 1) throw new IllegalStateException("SSTable Generator does not support batch statements and transactions"); super.execute(visit); diff --git a/test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java b/test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java index e83b3425705a..01f3a968de47 100644 --- a/test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java +++ b/test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.util.Arrays; -import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -44,7 +43,6 @@ import org.apache.cassandra.harry.execution.DataTracker; import org.apache.cassandra.harry.gen.Generator; 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.harry.util.ThrowingRunnable; import org.apache.cassandra.io.sstable.HarrySSTableWriter; @@ -56,11 +54,9 @@ public class HarrySSTableWriterTest extends CQLTester { private static final AtomicInteger idGen = new AtomicInteger(0); - private static final int NUMBER_WRITES_IN_RUNNABLE = 10; private String keyspace; private String table; - private String qualifiedTable; private File dataDir; @Rule @@ -71,7 +67,6 @@ public void perTestSetup() throws IOException { keyspace = "cql_keyspace" + idGen.incrementAndGet(); table = "table" + idGen.incrementAndGet(); - qualifiedTable = keyspace + '.' + table; dataDir = new File(tempFolder.newFolder().getAbsolutePath() + File.pathSeparator() + keyspace + File.pathSeparator() + table); assert dataDir.tryCreateDirectories(); @@ -81,9 +76,7 @@ public void perTestSetup() throws IOException } private final Generator simple_schema = rng -> { - return new SchemaSpec(rng.next(), - 1000, - keyspace, + return new SchemaSpec(keyspace, table, Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), ColumnSpec.pk("pk2", ColumnSpec.int64Type)), @@ -106,7 +99,7 @@ public void generateSSTableTest() schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace)); createTable(schema.compile()); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = HistoryBuilder.fromSchema(schema, rng.next(), 1000); for (int i = 0; i < 100; i++) history.insert(1); @@ -150,9 +143,9 @@ public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) { - DataTracker tracker = new DataTracker.SequentialDataTracker(); - return new CQLTesterVisitExecutor(schema, tracker, - new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder), + DataTracker.SequentialDataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, historyBuilder.valueGenerators(), tracker, + new QuiescentChecker(historyBuilder.valueGenerators(), tracker), statement -> { if (logger.isTraceEnabled()) logger.trace(statement.toString()); @@ -172,16 +165,10 @@ protected void executeMutatingVisit(Visit visit, CompiledStatement statement) } } - @Override - protected void executeValidatingVisit(Visit visit, List selects, CompiledStatement compiledStatement) - { - super.executeValidatingVisit(visit, selects, compiledStatement); - } - @Override public void execute(Visit visit) { - if (visit.visitedPartitions.size() > 1) + if (visit.visitedPartitions.length > 1) throw new IllegalStateException("SSTable Generator does not support batch statements and transactions"); super.execute(visit); diff --git a/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderInJvmDTest.java b/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderInJvmDTest.java new file mode 100644 index 000000000000..e9b9d0582fe7 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderInJvmDTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.test; + + +import org.junit.BeforeClass; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.shared.DistributedTestBase; +import org.apache.cassandra.distributed.test.log.FuzzTestBase; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.execution.InJvmDTestVisitExecutor; + +public class HistoryBuilderInJvmDTest extends HistoryBuilderTest +{ + private static final FuzzTestBase tester = new FuzzTestBase(); + + private final Cluster cluster; + + public HistoryBuilderInJvmDTest() throws Throwable + { + cluster = tester.builder().withNodes(1).start(); + cluster.schemaChange(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};", keyspace())); + } + + @BeforeClass + public static void beforeClass() throws Throwable + { + FuzzTestBase.beforeClass(); + } + + @Override + protected String keyspace() + { + return DistributedTestBase.KEYSPACE; + } + + @Override + protected void createTable(String schema) + { + cluster.schemaChange(schema); + } + + @Override + protected void flush(String keyspace, String table) + { + cluster.get(1).flush(keyspace); + } + + @Override + public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder) + { + return InJvmDTestVisitExecutor.builder().build(schema, historyBuilder.valueGenerators(), cluster); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderTest.java b/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderTest.java index ec864b10e46d..f3dec4bf1658 100644 --- a/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderTest.java +++ b/test/harry/main/org/apache/cassandra/harry/test/HistoryBuilderTest.java @@ -25,20 +25,18 @@ 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.dsl.IndexedValueGenerators; 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.ClusteringOrderBy; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.op.Visit; @@ -47,18 +45,22 @@ import static org.apache.cassandra.harry.checker.TestHelper.withRandom; import static org.apache.cassandra.harry.dsl.SingleOperationBuilder.IdxRelation; -public class HistoryBuilderTest extends CQLTester +public abstract class HistoryBuilderTest { + protected static final Logger logger = LoggerFactory.getLogger(HistoryBuilderTest.class); + + protected abstract String keyspace(); + protected abstract void createTable(String schema); + protected abstract void flush(String keyspace, String table); + + public abstract CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder); + // 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, + public final Generator simple_schema = rng -> { + return new SchemaSpec(keyspace(), "harry" + rng.nextLong(0, Long.MAX_VALUE), Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), ColumnSpec.pk("pk2", ColumnSpec.int64Type)), @@ -72,10 +74,8 @@ public class HistoryBuilderTest extends CQLTester ColumnSpec.staticColumn("s3", ColumnSpec.asciiType))); }; - private final Generator simple_schema_with_desc_ck = rng -> { - return new SchemaSpec(rng.next(), - 1000, - KEYSPACE, + public final Generator simple_schema_with_desc_ck = rng -> { + return new SchemaSpec(keyspace(), "harry" + rng.nextLong(0, Long.MAX_VALUE), Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), ColumnSpec.pk("pk2", ColumnSpec.int64Type)), @@ -98,13 +98,13 @@ public void orderByTest() SchemaSpec schema = gen.generate(rng); createTable(schema.compile()); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = HistoryBuilder.fromSchema(schema, rng.next(), 1000); 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)); + ClusteringOrderBy.DESC)); replay(schema, history); } @@ -120,7 +120,7 @@ public void historyBuilderInsertTest() SchemaSpec schema = gen.generate(rng); createTable(schema.compile()); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = HistoryBuilder.fromSchema(schema, rng.next(), 1000); for (int i = 0; i < 100; i++) history.insert(1, i, values(i, i, i), values(i, i, i)); @@ -140,7 +140,7 @@ public void historyBuilderInsertWithUnsetTest() SchemaSpec schema = gen.generate(rng); createTable(schema.compile()); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = HistoryBuilder.fromSchema(schema, rng.next(), 1000); for (int i = 0; i < 100; i++) { int v = i % 2 == 0 ? MagicConstants.UNSET_IDX : i; @@ -165,7 +165,7 @@ public void historyBuilderFilteringTest() SchemaSpec schema = gen.generate(rng); createTable(schema.compile()); - HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder history = HistoryBuilder.fromSchema(schema, rng.next(), 1000); for (int i = 0; i < 100; i++) { int v = (useUnset && i % 2 == 0) ? MagicConstants.UNSET_IDX : i; @@ -192,38 +192,39 @@ public void historyBuilderFilteringTest() @Test public void testSimpleFuzz() { - Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "harry", 100); + 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); + IndexedValueGenerators valueGenerators = HistoryBuilder.valueGenerators(schema, rng.next(), 1000); + HistoryBuilder historyBuilder = new HistoryBuilder(valueGenerators); + Generator partitionPicker = Generators.adaptLongToInt(valueGenerators.pkIdxGen()); 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)); + int pdIdx = partitionPicker.generate(rng); + history.insert(pdIdx); + history.selectPartition(pdIdx); }) .step((history, rng_) -> { - history.deleteRow(partitionPicker.generate(rng), rowPicker.generate(rng)); - history.selectPartition(partitionPicker.generate(rng)); + int pdIdx = partitionPicker.generate(rng); + history.deleteRow(pdIdx, valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng)); + history.selectPartition(pdIdx); }) .step((history, rng_) -> { - history.deletePartition(partitionPicker.generate(rng)); - history.selectPartition(partitionPicker.generate(rng)); + int pdIdx = partitionPicker.generate(rng); + history.deletePartition(pdIdx); + history.selectPartition(pdIdx); }) .step((history, rng_) -> { - HistoryBuilderHelper.deleteRandomColumns(schema, partitionPicker.generate(rng), rowPicker.generate(rng), rng, history); + int pdIdx = partitionPicker.generate(rng); + HistoryBuilderHelper.deleteRandomColumns(schema, pdIdx, valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng), rng, history); history.selectPartition(partitionPicker.generate(rng)); }) .step((history, rng_) -> { - history.deleteRowRange(partitionPicker.generate(rng), - rowPicker.generate(rng), - rowPicker.generate(rng), + int pdIdx = partitionPicker.generate(rng); + history.deleteRowRange(pdIdx, + valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng), + valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng), rng.nextInt(schema.clusteringKeys.size()), rng.nextBoolean(), rng.nextBoolean() @@ -231,12 +232,14 @@ public void testSimpleFuzz() history.selectPartition(partitionPicker.generate(rng)); }) .step((history, rng_) -> { - history.selectRow(partitionPicker.generate(rng), rowPicker.generate(rng)); + int pdIdx = partitionPicker.generate(rng); + history.selectRow(pdIdx, valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng)); }) .step((history, rng_) -> { - history.selectRowRange(partitionPicker.generate(rng), - rowPicker.generate(rng), - rowPicker.generate(rng), + int pdIdx = partitionPicker.generate(rng); + history.selectRowRange(pdIdx, + valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng), + valueGenerators.forPdIdx(pdIdx).ckIdxGen().generate(rng), rng.nextInt(schema.clusteringKeys.size()), rng.nextBoolean(), rng.nextBoolean()); @@ -260,30 +263,26 @@ public void testSimpleFuzz() @Test public void fuzzFiltering() { - Generator schemaGen = SchemaGenerators.schemaSpecGen(KEYSPACE, "fuzz_filtering", 100); + 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)); + IndexedValueGenerators valueGenerators = HistoryBuilder.valueGenerators(schema, rng.next(), 1000); ModelChecker modelChecker = new ModelChecker<>(); - HistoryBuilder historyBuilder = new HistoryBuilder(schema.valueGenerators); + HistoryBuilder historyBuilder = new HistoryBuilder(valueGenerators); modelChecker.init(historyBuilder) - .step((history, rng_) -> HistoryBuilderHelper.insertRandomData(schema, pkGen, ckGen, rng, history)) + .step((history, rng_) -> history.insert()) .step((history, rng_) -> { for (int i = 0; i < 10; i++) { - List ckRelations = HistoryBuilderHelper.generateClusteringRelations(rng, schema.clusteringKeys.size(), ckGen); + long pdIdx = valueGenerators.pkIdxGen().generate(rng); + List ckRelations = HistoryBuilderHelper.generateClusteringRelations(rng, schema.clusteringKeys.size(), valueGenerators.forPd(pdIdx).ckIdxGen()); List regularRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.regularColumns.size(), - column -> Math.min(schema.valueGenerators.regularPopulation(column), maxPartitionSize)); + column -> Math.toIntExact(valueGenerators.forPdIdx(pdIdx).regularColumnGen(column).population())); List staticRelations = HistoryBuilderHelper.generateValueRelations(rng, schema.staticColumns.size(), - column -> Math.min(schema.valueGenerators.staticPopulation(column), maxPartitionSize)); - history.select(rng.nextInt(maxPartitions), + column -> Math.toIntExact(valueGenerators.forPdIdx(pdIdx).staticColumnGen(column).population())); + history.select((int) pdIdx, ckRelations.toArray(new IdxRelation[0]), regularRelations.toArray(new IdxRelation[0]), staticRelations.toArray(new IdxRelation[0])); @@ -309,19 +308,7 @@ public void replay(SchemaSpec schema, HistoryBuilder 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) + public static int[] values(int... values) { return values; } diff --git a/test/harry/main/org/apache/cassandra/harry/test/InvertibleGeneratorTest.java b/test/harry/main/org/apache/cassandra/harry/test/InvertibleGeneratorTest.java new file mode 100644 index 000000000000..2a422fcde5ea --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/InvertibleGeneratorTest.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.test; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.gen.InvertibleGenerator; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; +import static org.junit.Assert.assertEquals; + +public class InvertibleGeneratorTest +{ + private static final Logger logger = LoggerFactory.getLogger(InvertibleGeneratorTest.class); + + private static final int POPULATION = 10_000; + + @Test + public void benchmark() + { + withRandom(rng -> { + long start = System.nanoTime(); + HistoryBuilder.IndexedBijection generator = InvertibleGenerator.fromType(rng, POPULATION, ColumnSpec.regularColumn("regular", ColumnSpec.asciiType)); + long buildNanos = System.nanoTime() - start; + + int n = (int) generator.population(); + long[] descriptors = new long[n]; + String[] values = new String[n]; + for (int i = 0; i < n; i++) + { + descriptors[i] = generator.descriptorAt(i); + values[i] = generator.inflate(descriptors[i]); + } + + start = System.nanoTime(); + for (int i = 0; i < n; i++) + generator.inflate(descriptors[i]); + long inflateNanos = System.nanoTime() - start; + + start = System.nanoTime(); + for (int i = 0; i < n; i++) + generator.deflate(values[i]); + long deflateNanos = System.nanoTime() - start; + + start = System.nanoTime(); + for (int i = 0; i < n; i++) + generator.idxFor(descriptors[i]); + long idxForNanos = System.nanoTime() - start; + + logger.info("InvertibleGenerator benchmark (population={}): build {} ms, inflate {} ms, deflate {} ms, idxFor {} ms ({} ops each)", + n, + TimeUnit.NANOSECONDS.toMillis(buildNanos), + TimeUnit.NANOSECONDS.toMillis(inflateNanos), + TimeUnit.NANOSECONDS.toMillis(deflateNanos), + TimeUnit.NANOSECONDS.toMillis(idxForNanos), + n); + }); + } + + @Test + public void fifoCache() + { + AtomicInteger computes = new AtomicInteger(); + InvertibleGenerator.Cache cache = new InvertibleGenerator.FifoCache<>(2, d -> { computes.incrementAndGet(); return "v" + d; }); + + assertEquals("v1", cache.get(1)); // miss -> compute + assertEquals("v1", cache.get(1)); // hit + assertEquals(1, computes.get()); + + assertEquals("v2", cache.get(2)); // miss -> compute; cache now holds {1, 2} + assertEquals(2, computes.get()); + + assertEquals("v3", cache.get(3)); // miss -> evicts oldest (1); cache now holds {2, 3} + assertEquals("v3", cache.get(3)); // hit + assertEquals(3, computes.get()); + + assertEquals("v1", cache.get(1)); // 1 was evicted -> recompute + assertEquals(4, computes.get()); + } + + @Test + public void growingCache() + { + AtomicInteger computes = new AtomicInteger(); + InvertibleGenerator.Cache cache = new InvertibleGenerator.UnboundedCache<>(4, d -> { computes.incrementAndGet(); return "v" + d; }); + + for (long d = 0; d < 100; d++) + assertEquals("v" + d, cache.get(d)); // 100 distinct misses + assertEquals(100, computes.get()); + + for (long d = 0; d < 100; d++) + assertEquals("v" + d, cache.get(d)); // nothing is ever evicted, so all hits + assertEquals(100, computes.get()); + } + + @Test + public void binarySearchPathCache() + { + long[] sorted = { 10, 20, 30, 40, 50, 60, 70 }; + AtomicInteger computes = new AtomicInteger(); + // depth 2 over [0, 6] visits indices {3, 1, 5} -> descriptors {40, 20, 60}, precomputed on construction. + InvertibleGenerator.Cache cache = new InvertibleGenerator.BinarySearchPathCache<>(sorted, sorted.length, 2, + d -> { computes.incrementAndGet(); return "v" + d; }); + assertEquals(3, computes.get()); + + // entries on the cached search path are served without recomputing + assertEquals("v40", cache.get(40)); + assertEquals("v20", cache.get(20)); + assertEquals("v60", cache.get(60)); + assertEquals(3, computes.get()); + + // entries off the path are computed on every lookup + assertEquals("v10", cache.get(10)); + assertEquals(4, computes.get()); + assertEquals("v10", cache.get(10)); + assertEquals(5, computes.get()); + } + + @Test + public void hierarchicalCache() + { + long[] sorted = { 10, 20, 30, 40, 50, 60, 70 }; + AtomicInteger pathComputes = new AtomicInteger(); + AtomicInteger fifoComputes = new AtomicInteger(); + // primary pins the depth-2 binary-search path {40, 20, 60}; backstop is a tiny FIFO + InvertibleGenerator.Cache primary = new InvertibleGenerator.BinarySearchPathCache<>(sorted, sorted.length, 2, + d -> { pathComputes.incrementAndGet(); return "v" + d; }); + InvertibleGenerator.Cache backstop = new InvertibleGenerator.FifoCache<>(2, d -> { fifoComputes.incrementAndGet(); return "v" + d; }); + InvertibleGenerator.Cache cache = new InvertibleGenerator.HierarchicalCache<>(primary, backstop); + + assertEquals(3, pathComputes.get()); // primary precomputed its 3 path entries + + // entries on the pinned path are served by the primary; the backstop is never consulted + assertEquals("v40", cache.get(40)); + assertEquals("v20", cache.get(20)); + assertEquals("v60", cache.get(60)); + assertEquals(3, pathComputes.get()); + assertEquals(0, fifoComputes.get()); + + // a miss on the primary falls through to the FIFO backstop, which computes and caches it + assertEquals("v10", cache.get(10)); + assertEquals(1, fifoComputes.get()); + assertEquals("v10", cache.get(10)); // now a backstop hit + assertEquals(1, fifoComputes.get()); + assertEquals(3, pathComputes.get()); // primary still only ever computed its path + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/test/QueryBuilderTest.java b/test/harry/main/org/apache/cassandra/harry/test/QueryBuilderTest.java index 55759019a4e2..cc83846131e7 100644 --- a/test/harry/main/org/apache/cassandra/harry/test/QueryBuilderTest.java +++ b/test/harry/main/org/apache/cassandra/harry/test/QueryBuilderTest.java @@ -22,9 +22,11 @@ import org.junit.Test; import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; 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.gen.ValueGenerators; import org.apache.cassandra.harry.op.Operations; import org.apache.cassandra.harry.op.Visit; @@ -36,8 +38,10 @@ public class QueryBuilderTest 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)); + SchemaSpec schemaSpec = SchemaGenerators.trivialSchema("harry", "simplified").generate(rng); + QueryBuildingVisitExecutor queryBuilder = new QueryBuildingVisitExecutor(schemaSpec, + (v, q) -> String.format("__START__\n%s\n__END__;", q), + HistoryBuilder.valueGenerators(schemaSpec, rng.next(), 1000)); CompiledStatement compiled = queryBuilder.compile(new Visit(1, new Operations.Operation[]{ new Operations.SelectPartition(1, 1L) })); Assert.assertTrue(compiled.cql().contains("SELECT")); diff --git a/test/harry/main/org/apache/cassandra/harry/test/SimpleBijectionTest.java b/test/harry/main/org/apache/cassandra/harry/test/SimpleBijectionTest.java index e1a04fb00ca5..79ffbf1ad6d0 100644 --- a/test/harry/main/org/apache/cassandra/harry/test/SimpleBijectionTest.java +++ b/test/harry/main/org/apache/cassandra/harry/test/SimpleBijectionTest.java @@ -45,8 +45,7 @@ public void testOrder() 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); - + HistoryBuilder.IndexedBijection generator = InvertibleGenerator.fromType(rng, 100, column); Object previous = null; for (int i = 0; i < generator.population(); i++) diff --git a/test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java b/test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java index 020c3ac6df42..88aeb3babb2d 100644 --- a/test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java +++ b/test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java @@ -34,7 +34,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Sets; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +63,7 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; +import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.Keyspaces; @@ -81,6 +81,7 @@ import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tcm.transformations.AlterSchema; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.JavaDriverUtils; @@ -108,6 +109,11 @@ private HarrySSTableWriter(AbstractSSTableSimpleWriter writer) this.writer = writer; } + public void setListener(Consumer listener) + { + writer.setSSTableProducedListener((writer, readers) -> listener.accept(writer.getFilename())); + } + public static Builder builder() { return new Builder(); @@ -116,7 +122,7 @@ public static Builder builder() public HarrySSTableWriter addRow(String cql, Object... values) throws IOException { ModificationStatement statement = prepare(cql); - List boundNames = statement.getBindVariables(); + List boundNames = statement.getBindVariables(); // CACHE? // TODO: avoid materializing this List> typeCodecs = boundNames.stream() .map(bn -> JavaDriverUtils.codecFor(JavaDriverUtils.driverType(bn.type))) @@ -262,6 +268,8 @@ public static class Builder private boolean buildIndexes = true; private Consumer> sstableProducedListener; private boolean openSSTableOnProduced = false; + private int sstableLevel = 0; + private long repairedAtMillis = ActiveRepairService.UNREPAIRED_SSTABLE; protected Builder() { @@ -479,6 +487,30 @@ public Builder openSSTableOnProduced() return this; } + /** + * Set the SSTable level for the produced SSTables. + * This is used to simulate LeveledCompactionStrategy layouts where + * SSTables are assigned to specific levels (L0, L1, L2, etc.). + * + * By default, SSTables are written at level 0. + * + * @param level the SSTable level (0-8) + * @return this builder. + */ + public Builder withSSTableLevel(int level) + { + if (level < 0 || level >= 9) + throw new IllegalArgumentException("SSTable level must be between 0 and 8, got: " + level); + this.sstableLevel = level; + return this; + } + + public Builder withRepairedAtMillis(long repairedAtMillis) + { + this.repairedAtMillis = repairedAtMillis; + return this; + } + public HarrySSTableWriter build() { if (directory == null) @@ -570,6 +602,8 @@ public HarrySSTableWriter build() if (format != null) writer.setSSTableFormatType(format); + writer.setSSTableLevel(sstableLevel); + writer.setReparedAtMillis(repairedAtMillis); if (buildIndexes && !indexStatements.isEmpty() && cfs != null) { @@ -630,7 +664,7 @@ public Keyspaces apply(ClusterMetadata metadata) @Override public boolean compatibleWith(ClusterMetadata metadata) { - return true; + return metadata.directory.commonSerializationVersion.isAtLeast(Version.V0); } }; ClusterMetadataService.instance().commit(new AlterSchema(schemaTransformation)); diff --git a/test/resources/harry/stress/default-stress-schema.yaml b/test/resources/harry/stress/default-stress-schema.yaml new file mode 100644 index 000000000000..70a13ed88bac --- /dev/null +++ b/test/resources/harry/stress/default-stress-schema.yaml @@ -0,0 +1,48 @@ +keyspace: harry_stress +table: default_table_2 + +table_definition: | + CREATE TABLE IF NOT EXISTS harry_stress.default_table_2 ( + pk1 text, + pk2 bigint, + pk3 bigint, + pk4 text, + ck1 text, + ck2 text, + ck3 bigint, + ck4 text, + ck5 blob, + val1 blob, + val2 blob, + PRIMARY KEY((pk1,pk2,pk3,pk4),ck1,ck2,ck3,ck4,ck5) + ) WITH transactional_mode = 'full'; + +rows: "cdf(0:1,0.1:2,0.5:2,0.75:20,0.9:200,0.99:2000,0.999:15000,1:90000)" + +columnspec: + - name: pk1 + size: uniform(10..20) + - name: pk2 + - name: pk3 + - name: pk4 + size: uniform(10..20) + - name: ck1 + population: fixed(10000) + size: uniform(10..90) + - name: ck2 + population: fixed(10000) + size: uniform(10..90) + - name: ck3 + population: fixed(10000) + - name: ck4 + population: fixed(10000) + size: uniform(10..90) + - name: ck5 + population: fixed(10000) + size: uniform(10..90) + - name: val1 + population: fixed(10000) + size: uniform(10..90) + - name: val2 + population: fixed(10000) + size: uniform(10..90) diff --git a/test/resources/harry/stress/levelled-sstable-schema.yaml b/test/resources/harry/stress/levelled-sstable-schema.yaml new file mode 100644 index 000000000000..9a6915f87783 --- /dev/null +++ b/test/resources/harry/stress/levelled-sstable-schema.yaml @@ -0,0 +1,61 @@ +# Schema used by org.apache.cassandra.fuzz.harry.sstable.LevelledSSTableGeneratorTest. +# Copied from default-stress-schema.yaml, but kept small (fixed row population) and using +# LeveledCompactionStrategy so the imported, level-tagged SSTables are placed on a real LCS table. +keyspace: harry_levelled +table: levelled_sstables + +table_definition: | + CREATE TABLE IF NOT EXISTS harry_levelled.levelled_sstables ( + pk1 text, + pk2 bigint, + pk3 bigint, + pk4 text, + ck1 text, + ck2 text, + ck3 bigint, + ck4 text, + ck5 blob, + val1 blob, + val2 blob, + PRIMARY KEY((pk1,pk2,pk3,pk4),ck1,ck2,ck3,ck4,ck5) + ) WITH compaction = {'class': 'LeveledCompactionStrategy'}; + +rows: "fixed(100)" + +# Rotation strategy used both to build the token index and to rebuild the validation model. +# fixed with replace_with_new=0/replace_with_visited=0 means no rotation: the active set is the +# first `target` partitions for the whole run, which keeps offline generation and validation a +# single coherent deterministic history. +rotation: + strategy: fixed + target: 1000 + replace_with_new: 100 + replace_with_visited: 0 + +columnspec: + - name: pk1 + size: uniform(10..20) + - name: pk2 + - name: pk3 + - name: pk4 + size: uniform(10..20) + - name: ck1 + population: fixed(1000) + size: uniform(10..90) + - name: ck2 + population: fixed(1000) + size: uniform(10..90) + - name: ck3 + population: fixed(1000) + - name: ck4 + population: fixed(1000) + size: uniform(10..90) + - name: ck5 + population: fixed(1000) + size: uniform(10..90) + - name: val1 + population: fixed(1000) + size: uniform(10..90) + - name: val2 + population: fixed(1000) + size: uniform(10..90) 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 0d0122309fc1..ce5a53630e93 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java @@ -25,7 +25,6 @@ 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; @@ -52,6 +51,7 @@ import org.apache.cassandra.distributed.impl.Query; import org.apache.cassandra.distributed.shared.WithProperties; import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.IndexedValueGenerators; import org.apache.cassandra.harry.execution.CompiledStatement; import org.apache.cassandra.harry.execution.DataTracker; import org.apache.cassandra.harry.execution.QueryBuildingVisitExecutor; @@ -60,7 +60,6 @@ import org.apache.cassandra.harry.gen.OperationsGenerators; 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.harry.op.Operations; @@ -110,6 +109,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.harry.dsl.HistoryBuilder.valueGenerators; 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; @@ -381,13 +381,14 @@ static class HarrySimulation implements Simulation protected final EntropySource rng; protected final SchemaSpec schema; + protected final IndexedValueGenerators valueGenerators; protected final Generator insertGen; protected final QueryBuildingVisitExecutor queryBuilder; protected final QuiescentChecker model; protected final Map log; protected final Generator ltsGen; - protected final DataTracker tracker; + protected final DataTracker.SimpleDataTracker tracker; private HarrySimulation(SchemaSpec schema, EntropySource rng, @@ -398,49 +399,14 @@ private HarrySimulation(SchemaSpec schema, Function schedule, Map log, Generator ltsGen, - DataTracker tracker) + DataTracker.SimpleDataTracker 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.valueGenerators = valueGenerators(schema, rng.next(), 1000); + this.insertGen = OperationsGenerators.writeOp(schema, valueGenerators); + this.queryBuilder = new QueryBuildingVisitExecutor(schema, QueryBuildingVisitExecutor.WrapQueries.UNLOGGED_BATCH, valueGenerators); + this.model = new QuiescentChecker(valueGenerators, tracker); this.simulated = simulated; this.scheduler = scheduler; @@ -859,9 +825,6 @@ public static Action validateAllLocal(HarrySimulation simulation, List { - 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<>(); diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/HarryValidatingQuery.java b/test/simulator/test/org/apache/cassandra/simulator/test/HarryValidatingQuery.java index fd2526f5e1ce..f540533a9379 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/HarryValidatingQuery.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/HarryValidatingQuery.java @@ -98,7 +98,7 @@ public void run() { CompiledStatement compiled = queryBuilder.compile(visit); Object[][] objects = executeNodeLocal(compiled.cql(), replica.node(), compiled.bindings()); - List actualRows = InJvmDTestVisitExecutor.rowsToResultSet(simulation.schema, select, objects); + List actualRows = InJvmDTestVisitExecutor.rowsToResultSet(simulation.schema, simulation.valueGenerators, select, objects); simulation.model.validate(select, actualRows); } } @@ -107,7 +107,7 @@ public void run() Operations.SelectStatement select = (Operations.SelectStatement) visit.operations[0]; CompiledStatement compiled = queryBuilder.compile(visit); Object[][] objects = execute(compiled.cql(), rng.nextInt(cluster.size()) + 1, compiled.bindings()); - List actualRows = InJvmDTestVisitExecutor.rowsToResultSet(simulation.schema, select, objects); + List actualRows = InJvmDTestVisitExecutor.rowsToResultSet(simulation.schema, simulation.valueGenerators, select, objects); simulation.model.validate(select, actualRows); } } @@ -123,7 +123,7 @@ public void run() protected long token(long pd) { - return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(simulation.schema.valueGenerators.pkGen().inflate(pd)))); + return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(simulation.valueGenerators.pkGen().inflate(pd)))); } protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) diff --git a/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java b/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java index a9a304787bf7..d7b4faf6b7f1 100644 --- a/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java +++ b/test/unit/org/apache/cassandra/io/sstable/CQLSSTableWriterTest.java @@ -1795,7 +1795,7 @@ public void testWritingWithZstdDictionary() throws Exception } } - protected static void loadSSTables(File dataDir, final String ks, final String tb) throws ExecutionException, InterruptedException + public static void loadSSTables(File dataDir, final String ks, final String tb) throws ExecutionException, InterruptedException { SSTableLoader loader = new SSTableLoader(dataDir, new SSTableLoader.Client() {