diff --git a/src/java/org/apache/cassandra/index/sai/IndexContext.java b/src/java/org/apache/cassandra/index/sai/IndexContext.java index e93fdbb2872a..9da02000f195 100644 --- a/src/java/org/apache/cassandra/index/sai/IndexContext.java +++ b/src/java/org/apache/cassandra/index/sai/IndexContext.java @@ -971,7 +971,10 @@ public Pair, Set> getBuiltIndexes(Collection 0) + logger.debug(logMessage("Successfully loaded index for SSTable {} with {} rows."), context.descriptor(), count); + else + logger.debug(logMessage("Skipped loading index for SSTable {} as it is empty."), context.descriptor()); } // Try to add new index to the set, if set already has such index, we'll simply release and move on. diff --git a/src/java/org/apache/cassandra/index/sai/SSTableContext.java b/src/java/org/apache/cassandra/index/sai/SSTableContext.java index e18a697dcddd..2ca213d31abc 100644 --- a/src/java/org/apache/cassandra/index/sai/SSTableContext.java +++ b/src/java/org/apache/cassandra/index/sai/SSTableContext.java @@ -146,7 +146,8 @@ public PrimaryKeyMap.Factory primaryKeyMapFactory() */ public int openFilesPerSSTable() { - return perSSTableComponents.onDiskFormat().openFilesPerSSTable(); + return perSSTableComponents.onDiskFormat() + .openFilesPerSSTable(sstable.metadata().hasClustering()); } @Override diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java index 439dbb8315c2..9e49f7169a5b 100644 --- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java +++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java @@ -28,6 +28,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; + import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; @@ -268,7 +269,7 @@ public StorageAttachedIndexQueryPlan queryPlanFor(RowFilter rowFilter) @Override public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata, long keyCount) { - IndexDescriptor indexDescriptor = IndexDescriptor.empty(descriptor); + IndexDescriptor indexDescriptor = IndexDescriptor.empty(descriptor, tableMetadata.comparator); try { return new StorageAttachedIndexWriter(indexDescriptor, tableMetadata, indices, tracker, keyCount, baseCfs.metric); @@ -293,7 +294,7 @@ public boolean handles(IndexTransaction.Type type) @Override public Set componentsForNewSSTable() { - return IndexDescriptor.componentsForNewlyFlushedSSTable(indices, version); + return IndexDescriptor.componentsForNewlyFlushedSSTable(indices, version, baseCfs.metadata().hasClustering()); } @Override diff --git a/src/java/org/apache/cassandra/index/sai/disk/PerSSTableWriter.java b/src/java/org/apache/cassandra/index/sai/disk/PerSSTableWriter.java index af323b91ecac..c4d1463ad0e5 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/PerSSTableWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/PerSSTableWriter.java @@ -21,6 +21,7 @@ import com.google.common.base.Stopwatch; +import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.index.sai.utils.PrimaryKey; /** @@ -30,7 +31,13 @@ public interface PerSSTableWriter { public static final PerSSTableWriter NONE = (key) -> {}; - default void startPartition(long position) throws IOException + /** + * Allows implementations to perform any necessary setup for a new partition. + * + * @param decoratedKey The key being appended to SSTable. + * @param position The position of the key in the component preferred for reading keys + */ + default void startPartition(DecoratedKey decoratedKey, long position) throws IOException {} void nextRow(PrimaryKey primaryKey) throws IOException; diff --git a/src/java/org/apache/cassandra/index/sai/disk/PostingListKeyRangeIterator.java b/src/java/org/apache/cassandra/index/sai/disk/PostingListKeyRangeIterator.java index ca4dcc7e0c10..f6e38c9f78a9 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/PostingListKeyRangeIterator.java +++ b/src/java/org/apache/cassandra/index/sai/disk/PostingListKeyRangeIterator.java @@ -67,7 +67,7 @@ public class PostingListKeyRangeIterator extends KeyRangeIterator private final AtomicBoolean isClosed = new AtomicBoolean(false); private boolean needsSkipping = false; - private PrimaryKey skipToToken = null; + private PrimaryKey skipToKey = null; private long lastSegmentRowId = -1; /** @@ -100,13 +100,13 @@ protected void performSkipTo(PrimaryKey nextKey) if (indexContext.getDefinition().isStatic()) nextKey = nextKey.forStaticRow(); - // If skipToToken is equal to nextKey, we take the nextKey because in practice, it is greater than or equal - // to the skipToToken. This is because token only PKs are considered equal to all PKs with the same token, + // If skipToKey is equal to nextKey, we take the nextKey because in practice, it is greater than or equal + // to the skipToKey. This is because token only PKs are considered equal to all PKs with the same token, // and for a range query, we first skip on the token-only PK. - if (skipToToken != null && skipToToken.compareTo(nextKey) > 0) + if (skipToKey != null && skipToKey.compareTo(nextKey) > 0) return; - skipToToken = nextKey; + skipToKey = nextKey; needsSkipping = true; } @@ -150,16 +150,16 @@ public void close() throws IOException FileUtils.closeQuietly(postingList, primaryKeyMap); } - else { + else + { logger.warn("PostingListKeyRangeIterator is already closed", new IllegalStateException("PostingListKeyRangeIterator is already closed")); } - } private boolean exhausted() { - return needsSkipping && skipToToken.compareTo(getMaximum()) > 0; + return needsSkipping && skipToKey.compareTo(getMaximum()) > 0; } /** @@ -170,8 +170,8 @@ private long getNextRowId() throws IOException long segmentRowId; if (needsSkipping) { - long targetSstableRowId = primaryKeyMap.ceiling(skipToToken); - // skipToToken is larger than max token in token file + long targetSstableRowId = primaryKeyMap.ceiling(skipToKey); + // skipToKey is larger than max token in token file if (targetSstableRowId < 0) { return PostingList.END_OF_STREAM; diff --git a/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java index adaf47dd9139..02bbdb9dbf09 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java @@ -131,7 +131,7 @@ public void startPartition(DecoratedKey key, long position, long keyPositionForS try { - perSSTableWriter.startPartition(position); + perSSTableWriter.startPartition(key, position); } catch (Throwable t) { diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentType.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentType.java index bf38d6b8fbd8..b3569b93016c 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentType.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponentType.java @@ -34,7 +34,7 @@ public enum IndexComponentType */ META("Meta"), /** - * KDTree written by {@code BKDWriter} indexes mappings of term to one ore more segment row IDs + * KDTree written by {@code BKDWriter} indexes mappings of term to one or more segment row IDs * (segment row ID = SSTable row ID - segment row ID offset). * * V1 @@ -116,7 +116,53 @@ public enum IndexComponentType /** * Stores document length information for BM25 scoring */ - DOC_LENGTHS("DocLengths"); + DOC_LENGTHS("DocLengths"), + + /** + * An on-disk block packed index mapping rowIds to token values. + *

+ * V9 + */ + ROW_TO_TOKEN("RowToToken"), + + /** + * An on-disk block packed index mapping rowIds to partitionIds. + *

+ * V9 + */ + ROW_TO_PARTITION("RowToPartition"), + + /** + * An on-disk block packed index mapping partitionIds to the number of rows for the partition. + *

+ * V9 + */ + PARTITION_TO_SIZE("PartitionToSize"), + + /** + * Prefix-compressed blocks of partition keys used for rowId to partition key lookups + *

+ * V9 + */ + PARTITION_KEY_BLOCKS("PKBlocks"), + /** + * Encoded sequence of offsets to partition key blocks + *

+ * V9 + */ + PARTITION_KEY_BLOCK_OFFSETS("PKBlockOffsets"), + /** + * Prefix-compressed blocks of clustering keys used for rowId to clustering key lookups + *

+ * V9 + */ + CLUSTERING_KEY_BLOCKS("CKBlocks"), + /** + * Encoded sequence of offsets to clustering key blocks + *

+ * V9 + */ + CLUSTERING_KEY_BLOCK_OFFSETS("CKBlockOffsets"); public final String representation; diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponents.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponents.java index 17293ea910e5..f42ecd594d98 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponents.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponents.java @@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.io.sstable.Component; @@ -186,9 +187,13 @@ default Set expectedComponentsForVersion() { return isPerIndexGroup() ? onDiskFormat().perIndexComponentTypes(context()) - : onDiskFormat().perSSTableComponentTypes(); + : onDiskFormat().perSSTableComponentTypes(hasClustering()); } + boolean hasClustering(); + + ClusteringComparator comparator(); + default ByteComparable.Version byteComparableVersionFor(IndexComponentType component) { return version().byteComparableVersionFor(component, descriptor().version); diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java index f69589c34f79..85211fab123f 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java @@ -35,11 +35,14 @@ import javax.annotation.Nullable; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.StorageAttachedIndex; @@ -55,6 +58,7 @@ import org.apache.cassandra.io.storage.StorageProvider; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.lucene.store.BufferedChecksumIndexInput; import org.apache.lucene.store.ChecksumIndexInput; @@ -89,22 +93,32 @@ public class IndexDescriptor */ public final Descriptor descriptor; + public final boolean hasClustering; private final ComponentsBuildId emptyGroupMarker; + private final ClusteringComparator comparator; // The per-sstable components for this descriptor. This is never `null` in practice, but 1) it's a bit easier to // initialize it outsides of the ctor, and 2) it can actually change upon calls to `reload`. private IndexComponentsImpl perSSTable; private final Map perIndexes = new ConcurrentHashMap<>(); - private IndexDescriptor(Descriptor descriptor) + private IndexDescriptor(Descriptor descriptor, ClusteringComparator comparator) { this.descriptor = descriptor; this.emptyGroupMarker = ComponentsBuildId.of(Version.current(descriptor.ksname), -1); + this.comparator = comparator; + this.hasClustering = comparator.size() > 0; } + @VisibleForTesting public static IndexDescriptor empty(Descriptor descriptor) { - IndexDescriptor created = new IndexDescriptor(descriptor); + return empty(descriptor, new ClusteringComparator()); + } + + public static IndexDescriptor empty(Descriptor descriptor, ClusteringComparator comparator) + { + IndexDescriptor created = new IndexDescriptor(descriptor, comparator); // Some code assumes that you can always at least call `perSSTableComponents()` and not get `null`, so we // set it to an empty group here. created.perSSTable = created.createEmptyGroup(null); @@ -114,7 +128,8 @@ public static IndexDescriptor empty(Descriptor descriptor) public static IndexDescriptor load(SSTableReader sstable, Set indices) { SSTableIndexComponentsState discovered = IndexComponentDiscovery.instance().discoverComponents(sstable); - IndexDescriptor descriptor = new IndexDescriptor(sstable.descriptor); + IndexDescriptor descriptor = new IndexDescriptor(sstable.descriptor, + sstable.metadata().comparator); descriptor.initialize(indices, discovered); return descriptor; } @@ -134,7 +149,7 @@ private void initializeIndexes(Set indices, SSTableIndexComponents private Set expectedComponentsForVersion(Version version, @Nullable IndexContext context) { return context == null - ? version.onDiskFormat().perSSTableComponentTypes() + ? version.onDiskFormat().perSSTableComponentTypes(hasClustering) : version.onDiskFormat().perIndexComponentTypes(context); } @@ -182,11 +197,11 @@ private IndexComponentsImpl createEmptyGroup(@Nullable IndexContext context) * Please note that the final sstable may not contain all of these components, as some may be empty or not written * due to the specific of the flush, but this should be a superset of the components written. */ - public static Set componentsForNewlyFlushedSSTable(Collection indices, Version version) + public static Set componentsForNewlyFlushedSSTable(Collection indices, Version version, boolean hasClustering) { ComponentsBuildId buildId = ComponentsBuildId.forNewSSTable(version); Set components = new HashSet<>(); - for (IndexComponentType component : buildId.version().onDiskFormat().perSSTableComponentTypes()) + for (IndexComponentType component : buildId.version().onDiskFormat().perSSTableComponentTypes(hasClustering)) components.add(customComponentFor(buildId, component, null)); for (StorageAttachedIndex index : indices) @@ -197,7 +212,7 @@ public static Set componentsForNewlyFlushedSSTable(Collection - * This is a subset of {@link #componentsForNewlyFlushedSSTable(Collection, Version)} and has the same caveats. + * This is a subset of {@link #componentsForNewlyFlushedSSTable(Collection, Version, boolean)} and has the same caveats. */ public static Set perIndexComponentsForNewlyFlushedSSTable(IndexContext context) { @@ -430,6 +445,18 @@ public boolean isEmpty() return isComplete() && components.size() == 1; } + @Override + public boolean hasClustering() + { + return hasClustering; + } + + @Override + public ClusteringComparator comparator() + { + return comparator; + } + @Override public Collection all() { @@ -656,8 +683,13 @@ public File file() @Override public FileHandle createFileHandle() { - var builder = StorageProvider.instance.fileHandleBuilderFor(this); - var b = builder.order(byteOrder()); + FileHandle.Builder builder = StorageProvider.instance.fileHandleBuilderFor(this); + if (logger.isTraceEnabled()) + { + logger.trace(this.parent().logMessage("Opening file handle for {} ({})"), + file, FBUtilities.prettyPrintMemory(file.length())); + } + FileHandle.Builder b = builder.order(byteOrder()); return b.complete(); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java index a39d66651645..59679154e734 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java @@ -150,9 +150,10 @@ public PerIndexWriter newPerIndexWriter(StorageAttachedIndex index, * This is a complete set of componentstypes that could exist on-disk. It does not imply that the * components currently exist on-disk. * + * @param hasClustering true if the SSTable forms part of a table using clustering columns * @return The set of {@link IndexComponentType} for the per-SSTable index */ - public Set perSSTableComponentTypes(); + Set perSSTableComponentTypes(boolean hasClustering); /** * Returns the set of {@link IndexComponentType} for the per-index part of an index. @@ -174,9 +175,10 @@ default public Set perIndexComponentTypes(IndexContext index * This is a static indication of the files that can be held open by an index * for queries. It is not a dynamic calculation. * + * @param hasClustering true if the SSTable forms part of a table using clustering columns * @return The number of open per-SSTable files */ - public int openFilesPerSSTable(); + int openFilesPerSSTable(boolean hasClustering); /** * Return the number of open per-index files that can be open during a query. @@ -215,5 +217,4 @@ default public Set perIndexComponentTypes(IndexContext index * @return the JVector file format version that this on-disk format uses. */ int jvectorFileFormatVersion(); - } diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/Version.java b/src/java/org/apache/cassandra/index/sai/disk/format/Version.java index 198ecb24af41..6f9ae9da6e8f 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/format/Version.java +++ b/src/java/org/apache/cassandra/index/sai/disk/format/Version.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Optional; import java.util.regex.Pattern; + import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -40,6 +41,7 @@ import org.apache.cassandra.index.sai.disk.v6.V6OnDiskFormat; import org.apache.cassandra.index.sai.disk.v7.V7OnDiskFormat; import org.apache.cassandra.index.sai.disk.v8.V8OnDiskFormat; +import org.apache.cassandra.index.sai.disk.v9.V9OnDiskFormat; import org.apache.cassandra.index.sai.utils.TypeUtil; import org.apache.cassandra.io.sstable.format.bti.BtiFormat; import org.apache.cassandra.schema.SchemaConstants; @@ -78,10 +80,13 @@ public class Version implements Comparable public static final Version ED = new Version("ed", V7OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "ed")); // jvector file format version 6 (skipped 5) public static final Version FA = new Version("fa", V8OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "fa")); + // Replaces primary key components with partition key and clustering key components and + // uses key lookup store instead of sorted terms. + public static final Version GA = new Version("ga", V9OnDiskFormat.instance, (c, i, g) -> stargazerFileNameFormat(c, i, g, "ga")); // These are in reverse-chronological order so that the latest version is first. Version matching tests // are more likely to match the latest version, so we want to test that one first. - public static final List ALL = Lists.newArrayList(FA, ED, EC, EB, DC, DB, CA, BA, AA); + public static final List ALL = Lists.newArrayList(GA, FA, ED, EC, EB, DC, DB, CA, BA, AA); public static final Version EARLIEST = AA; public static final Version VECTOR_EARLIEST = BA; diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java b/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java index e13c14c8e9bf..82fb37485adc 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java @@ -46,6 +46,13 @@ public interface LongArray extends Closeable */ long ceilingIndex(long targetValue); + /** + * @param targetValue Value to look up. + * @return The index of the largest value equal to or smaller than the target, + * or negative value if the target value is smaller than all values + */ + long floorIndex(long targetValue); + /** * Using the target value returns the first index corresponding to the value. * @@ -92,6 +99,13 @@ public long ceilingIndex(long targetValue) return longArray.ceilingIndex(targetValue); } + @Override + public long floorIndex(long targetValue) + { + open(); + return longArray.floorIndex(targetValue); + } + @Override public long indexOf(long targetValue) { diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java index e23d7041396f..61585877c945 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java @@ -94,7 +94,7 @@ public class V1OnDiskFormat implements OnDiskFormat /** * Global limit on heap consumed by all index segment building that occurs outside the context of Memtable flush. * - * Note that to avoid flushing extremly small index segments, a segment is only flushed when + * Note that to avoid flushing extremely small index segments, a segment is only flushed when * both the global size of all building segments has breached the limit and the size of the * segment in question reaches (segment_write_buffer_space_mb / # currently building column indexes). * @@ -176,7 +176,7 @@ public IndexSearcher newIndexSearcher(SSTableContext sstableContext, SegmentMetadata segmentMetadata) throws IOException { if (indexContext.isLiteral()) - // We filter because the CA format wrote maps acording to a different order than their abstract type. + // We filter because the CA format wrote maps according to a different order than their abstract type. return new InvertedIndexSearcher(sstableContext, indexFiles, segmentMetadata, indexContext, Version.AA, true); return new KDTreeIndexSearcher(sstableContext.primaryKeyMapFactory(), indexFiles, segmentMetadata, indexContext); } @@ -184,7 +184,7 @@ public IndexSearcher newIndexSearcher(SSTableContext sstableContext, @Override public PerSSTableWriter newPerSSTableWriter(IndexDescriptor indexDescriptor) throws IOException { - return new SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); + return new V1SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); } @Override @@ -260,7 +260,7 @@ public void validateIndexComponent(IndexComponent.ForRead component, boolean che } @Override - public Set perSSTableComponentTypes() + public Set perSSTableComponentTypes(boolean hasClustering) { return PER_SSTABLE_COMPONENTS; } @@ -274,7 +274,7 @@ public Set perIndexComponentTypes(AbstractType validator) } @Override - public int openFilesPerSSTable() + public int openFilesPerSSTable(boolean hasClustering) { return 2; } @@ -282,7 +282,9 @@ public int openFilesPerSSTable() @Override public int openFilesPerIndex(IndexContext indexContext) { - // For the V1 format there are always 2 open files per index - index (kdtree or terms) + postings + // For the V1 format there are always 2 open files per index: + // - index (balanced tree or terms) + // - auxiliary postings for the balanced tree and postings for the literal terms return 2; } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableComponentsWriter.java similarity index 86% rename from src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java rename to src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableComponentsWriter.java index 263ac680cd73..4db74dce4711 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableComponentsWriter.java @@ -24,19 +24,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.index.sai.disk.PerSSTableWriter; import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexComponentType; import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.Throwables; import org.apache.lucene.util.IOUtils; /** * Writes all SSTable-attached index token and offset structures. */ -public class SSTableComponentsWriter implements PerSSTableWriter +public class V1SSTableComponentsWriter implements PerSSTableWriter { - protected static final Logger logger = LoggerFactory.getLogger(SSTableComponentsWriter.class); + protected static final Logger logger = LoggerFactory.getLogger(V1SSTableComponentsWriter.class); private final IndexComponents.ForWrite perSSTableComponents; private final NumericValuesWriter tokenWriter; @@ -45,7 +47,7 @@ public class SSTableComponentsWriter implements PerSSTableWriter private long currentKeyPartitionOffset; - public SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) throws IOException + public V1SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) throws IOException { this.perSSTableComponents = perSSTableComponents; this.metadataWriter = new MetadataWriter(perSSTableComponents); @@ -56,7 +58,7 @@ public SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) th } @Override - public void startPartition(long position) + public void startPartition(DecoratedKey decoratedKey, long position) { currentKeyPartitionOffset = position; } @@ -75,9 +77,11 @@ public void complete(Stopwatch stopwatch) throws IOException } @Override + @SuppressWarnings("ThrowableNotThrown") public void abort(Throwable accumulator) { logger.debug(perSSTableComponents.logMessage("Aborting token/offset writer for {}..."), perSSTableComponents.descriptor()); + Throwables.close(accumulator, tokenWriter, offsetWriter, metadataWriter); perSSTableComponents.forceDeleteAllComponents(); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java index b43b7f1611b2..25e6a50b7777 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.java @@ -85,10 +85,27 @@ public long ceilingIndex(long targetValue) return -1; long index = findBlockIndex(targetValue); - lastIndex = index >= 0 ? index : -index - 1; + lastIndex = index >= 0 ? index : ~index; return isOutOfRangeState() ? -1 : lastIndex; } + /** + * Find the index of the largest value less than or equal to the target value. + * This is the floor operation, complementary to ceilingIndex. + * + * @param targetValue Value to search for + * @return The index of the floor value, or -1 if the target is smaller than all values + */ + @Override + public long floorIndex(long targetValue) + { + // Check if we're before the start of the array + if (targetValue < get(0)) + return -1; + + return findBlockIndexForFloor(targetValue); + } + @Override public long indexOf(long targetValue) { @@ -97,7 +114,7 @@ public long indexOf(long targetValue) return Long.MIN_VALUE; long index = findBlockIndex(targetValue); - lastIndex = index >= 0 ? index : -index - 1; + lastIndex = index >= 0 ? index : ~index; return isOutOfRangeState() ? Long.MIN_VALUE : index; } @@ -139,6 +156,19 @@ private long findBlockIndex(long targetValue) return findBlockIndex(targetValue, blockIndex, exactMatch); } + /** + * Find the block and index for floor operation. + * Similar to findBlockIndex but searches for the largest value <= target. + */ + private long findBlockIndexForFloor(long targetValue) + { + int blockIndex = binarySearchBlockMaxValues(targetValue); + + // blockIndex is now the block that might contain our floor value + // Search for the floor value within the identified block + return findBlockIndexForFloor(targetValue, blockIndex); + } + /** * * @return a positive block index for an exact match, or a negative one for a non-exact match @@ -163,9 +193,9 @@ private int binarySearchBlockMinValues(long targetValue) } else if (cmp < 0) { - // We're in the same block. Indicate a non-exact match, and this value will be both - // negated and then decremented to wind up at the current value of "low" here. - return -low - 1; + // We're in the same block. Indicate a non-exact match; the caller inverts this + // to recover the current value of "low". + return ~low; } // The target is greater than the next block's min value, so advance to that @@ -206,6 +236,34 @@ else if (midVal > targetValue) return -low; // no exact match found } + /** + * Binary search block max values to find the block containing the floor. + * Searches the last value of each block (block max) to determine which block + * could contain the largest value <= target. + * + * @return positive block index for exact match on block max, negative for non-exact match + */ + private int binarySearchBlockMaxValues(long targetValue) + { + int min = binarySearchBlockMinValues(targetValue); + int highest = Math.toIntExact(blockBitsPerValue.length) - 1; + if (min < 0) + min = -min; + if (min > highest) + min--; + // Check if the target value is smaller than the first value in min block + if (targetValue < delta(min, 0)) + return Math.max(min - 1, 0); + int max = min; + // Check for duplicates in the next blocks + while (min <= highest && delta(min, 0) <= targetValue) + { + max = min; + min++; + } + return max; + } + private long findBlockIndex(long targetValue, long blockIdx, boolean exactMatch) { // Calculate the global offset for the selected block: @@ -220,6 +278,21 @@ private long findBlockIndex(long targetValue, long blockIdx, boolean exactMatch) return binarySearchBlock(targetValue, low, high); } + /** + * Find the floor index within the specific block. + */ + private long findBlockIndexForFloor(long targetValue, int blockIdx) + { + assert blockIdx >= 0 : "Block index cannot be negative"; + + // Calculate the global offset for the selected block + long offset = (long) blockIdx << blockShift; + + // Search from the start of the block to the end of the block + long high = Math.min(offset + blockSize - 1, valueCount - 1); + return binarySearchBlockForFloor(targetValue, offset, high); + } + /** * binary search target value between low and high. * @@ -263,6 +336,35 @@ else if (midVal > target) return -(low + 1); } + /** + * Binary search for floor value between low and high indices. + * + * @return index of the largest value <= target, or -1 if all values > target + */ + private long binarySearchBlockForFloor(long target, long low, long high) + { + long result = -1; // Track the best floor candidate found so far + + while (low <= high) + { + long mid = low + ((high - low) >> 1); + long midVal = get(mid); + + if (midVal <= target) + { + // This could be our floor, but there might be a larger one further right + result = mid; + low = mid + 1; + } + else + { + high = mid - 1; + } + } + + return result; + } + @Override public long length() { diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java index b606cb7fd146..35d366858eda 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java @@ -106,13 +106,7 @@ protected long blockOffsetAt(int block) } @Override - public long ceilingIndex(long targetValue) - { - throw new UnsupportedOperationException(); - } - - @Override - public long indexOf(long targetValue) + public long indexOf(long value) { throw new UnsupportedOperationException(); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java index 39b38123f986..293494ff6b64 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java @@ -208,6 +208,12 @@ public long ceilingIndex(long targetValue) throw new UnsupportedOperationException(); } + @Override + public long floorIndex(long targetValue) + { + throw new UnsupportedOperationException(); + } + @Override public long indexOf(long targetValue) { diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/PrimaryKeyWithSource.java b/src/java/org/apache/cassandra/index/sai/disk/v2/PrimaryKeyWithSource.java index 35201b74a1e9..37dd63999a60 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v2/PrimaryKeyWithSource.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/PrimaryKeyWithSource.java @@ -28,7 +28,7 @@ import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; -class PrimaryKeyWithSource implements PrimaryKey +public class PrimaryKeyWithSource implements PrimaryKey { private final SSTableId sourceSstableId; private final long sourceRowId; @@ -37,7 +37,7 @@ class PrimaryKeyWithSource implements PrimaryKey private final PrimaryKey sourceSstableMinKey; private final PrimaryKey sourceSstableMaxKey; - PrimaryKeyWithSource(PrimaryKeyMap primaryKeyMap, long sstableRowId, PrimaryKey sourceSstableMinKey, PrimaryKey sourceSstableMaxKey) + public PrimaryKeyWithSource(PrimaryKeyMap primaryKeyMap, long sstableRowId, PrimaryKey sourceSstableMinKey, PrimaryKey sourceSstableMaxKey) { this.primaryKeyMap = primaryKeyMap; this.sourceSstableId = primaryKeyMap.getSSTableId(); diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyMap.java index c708545f75b8..f43a4d7982ca 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyMap.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyMap.java @@ -81,11 +81,11 @@ public static class RowAwarePrimaryKeyMapFactory implements Factory private final FileHandle termsTrie; private final IPartitioner partitioner; private final ClusteringComparator clusteringComparator; - private final RowAwarePrimaryKeyFactory primaryKeyFactory; + private final V2RowAwarePrimaryKeyFactory primaryKeyFactory; private final SSTableId sstableId; private final boolean hasStaticColumns; - public RowAwarePrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, RowAwarePrimaryKeyFactory primaryKeyFactory, SSTableReader sstable) + public RowAwarePrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, V2RowAwarePrimaryKeyFactory primaryKeyFactory, SSTableReader sstable) { FileHandle token = null; FileHandle termsDataBlockOffsets = null; @@ -103,7 +103,6 @@ public RowAwarePrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents token = perSSTableComponents.get(IndexComponentType.TOKEN_VALUES).createFileHandle(); this.tokenReaderFactory = new BlockPackedReader(token, tokensMeta); - termsDataBlockOffsets = perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_BLOCK_OFFSETS).createFileHandle(); termsData = perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_BLOCKS).createFileHandle(); termsTrie = perSSTableComponents.get(IndexComponentType.PRIMARY_KEY_TRIE).createFileHandle(); @@ -142,7 +141,7 @@ public PrimaryKeyMap newPerSSTablePrimaryKeyMap() } catch (IOException e) { - throw new UncheckedIOException(e); + throw new UncheckedIOException("Failed to load PrimaryKeyMap for sstable: " + sstableId, e); } } @@ -163,7 +162,7 @@ public void close() throws IOException private final SortedTermsReader sortedTermsReader; private final SortedTermsReader.Cursor cursor; private final IPartitioner partitioner; - private final RowAwarePrimaryKeyFactory primaryKeyFactory; + private final V2RowAwarePrimaryKeyFactory primaryKeyFactory; private final ClusteringComparator clusteringComparator; private final SSTableId sstableId; private final boolean hasStaticColumns; @@ -172,7 +171,7 @@ private RowAwarePrimaryKeyMap(LongArray rowIdToToken, SortedTermsReader sortedTermsReader, SortedTermsReader.Cursor cursor, IPartitioner partitioner, - RowAwarePrimaryKeyFactory primaryKeyFactory, + V2RowAwarePrimaryKeyFactory primaryKeyFactory, ClusteringComparator clusteringComparator, SSTableId sstableId, boolean hasStaticColumns) @@ -249,9 +248,9 @@ public long exactRowIdOrInvertedCeiling(PrimaryKey key) long pointId = cursor.getExactPointId(v -> key.asComparableBytes(v)); if (pointId >= 0) return pointId; - long ceiling = cursor.ceiling(v -> key.asComparableBytesMinPrefix(v)); - // Use min value since -(Long.MIN_VALUE) - 1 == Long.MAX_VALUE. - return ceiling < 0 ? Long.MIN_VALUE : -ceiling - 1; + long ceiling = cursor.ceiling(key::asComparableBytesMinPrefix); + // Use min value since inverting Long.MIN_VALUE gives Long.MAX_VALUE. + return ceiling < 0 ? Long.MIN_VALUE : ~ceiling; } @Override @@ -273,7 +272,7 @@ public long ceiling(PrimaryKey key) if (rowId == Long.MIN_VALUE) return -1; else - return -rowId - 1; + return ~rowId; } return cursor.ceiling(key::asComparableBytesMinPrefix); @@ -285,7 +284,6 @@ public long floor(PrimaryKey key) return cursor.floor(key::asComparableBytesMaxPrefix); } - @Override public void close() throws IOException { diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/TokenOnlyPrimaryKey.java b/src/java/org/apache/cassandra/index/sai/disk/v2/TokenOnlyPrimaryKey.java index aaaccab34b38..b0b078a6e8d8 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v2/TokenOnlyPrimaryKey.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/TokenOnlyPrimaryKey.java @@ -21,7 +21,6 @@ import org.apache.cassandra.dht.Token; import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.utils.bytecomparable.ByteComparable; -import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; import org.apache.cassandra.utils.bytecomparable.ByteSource; public final class TokenOnlyPrimaryKey implements PrimaryKey diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskFormat.java index ae6baaf9e5d5..a6f80964575a 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskFormat.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2OnDiskFormat.java @@ -23,6 +23,8 @@ import java.util.EnumSet; import java.util.Set; +import com.google.common.base.Preconditions; + import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.index.sai.IndexContext; @@ -87,14 +89,14 @@ public IndexFeatureSet indexFeatureSet() @Override public PrimaryKey.Factory newPrimaryKeyFactory(ClusteringComparator comparator) { - return new RowAwarePrimaryKeyFactory(comparator); + return new V2RowAwarePrimaryKeyFactory(comparator); } @Override public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, PrimaryKey.Factory primaryKeyFactory, SSTableReader sstable) { - assert primaryKeyFactory instanceof RowAwarePrimaryKeyFactory; - return new RowAwarePrimaryKeyMap.RowAwarePrimaryKeyMapFactory(perSSTableComponents, (RowAwarePrimaryKeyFactory) primaryKeyFactory, sstable); + Preconditions.checkArgument(primaryKeyFactory instanceof V2RowAwarePrimaryKeyFactory); + return new RowAwarePrimaryKeyMap.RowAwarePrimaryKeyMapFactory(perSSTableComponents, (V2RowAwarePrimaryKeyFactory) primaryKeyFactory, sstable); } @Override @@ -113,7 +115,7 @@ public IndexSearcher newIndexSearcher(SSTableContext sstableContext, @Override public PerSSTableWriter newPerSSTableWriter(IndexDescriptor indexDescriptor) throws IOException { - return new SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); + return new V2SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); } @Override @@ -125,13 +127,13 @@ public Set perIndexComponentTypes(AbstractType validator) } @Override - public Set perSSTableComponentTypes() + public Set perSSTableComponentTypes(boolean hasClustering) { return PER_SSTABLE_COMPONENTS; } @Override - public int openFilesPerSSTable() + public int openFilesPerSSTable(boolean hasClustering) { return 4; } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyFactory.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2RowAwarePrimaryKeyFactory.java similarity index 75% rename from src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyFactory.java rename to src/java/org/apache/cassandra/index/sai/disk/v2/V2RowAwarePrimaryKeyFactory.java index f35da6871e92..112b71f1db4e 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v2/RowAwarePrimaryKeyFactory.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2RowAwarePrimaryKeyFactory.java @@ -38,13 +38,13 @@ * A row-aware {@link PrimaryKey.Factory}. This creates {@link PrimaryKey} instances that are * sortable by {@link DecoratedKey} and {@link Clustering}. */ -public class RowAwarePrimaryKeyFactory implements PrimaryKey.Factory +public class V2RowAwarePrimaryKeyFactory implements PrimaryKey.Factory { - private final ClusteringComparator clusteringComparator; - private final boolean hasClustering; + protected final ClusteringComparator clusteringComparator; + public final boolean hasClustering; - public RowAwarePrimaryKeyFactory(ClusteringComparator clusteringComparator) + public V2RowAwarePrimaryKeyFactory(ClusteringComparator clusteringComparator) { this.clusteringComparator = clusteringComparator; this.hasClustering = clusteringComparator.size() > 0; @@ -62,19 +62,19 @@ public PrimaryKey create(DecoratedKey partitionKey, Clustering clustering) return new RowAwarePrimaryKey(partitionKey.getToken(), partitionKey, clustering, null); } - PrimaryKey createWithSource(PrimaryKeyMap primaryKeyMap, long sstableRowId, PrimaryKey sourceSstableMinKey, PrimaryKey sourceSstableMaxKey) + public PrimaryKey createWithSource(PrimaryKeyMap primaryKeyMap, long sstableRowId, PrimaryKey sourceSstableMinKey, PrimaryKey sourceSstableMaxKey) { return new PrimaryKeyWithSource(primaryKeyMap, sstableRowId, sourceSstableMinKey, sourceSstableMaxKey); } - private class RowAwarePrimaryKey implements PrimaryKey + protected class RowAwarePrimaryKey implements PrimaryKey { - private Token token; - private DecoratedKey partitionKey; - private Clustering clustering; + private final Token token; + protected DecoratedKey partitionKey; + protected Clustering clustering; private Supplier primaryKeySupplier; - private RowAwarePrimaryKey(Token token, DecoratedKey partitionKey, Clustering clustering, Supplier primaryKeySupplier) + protected RowAwarePrimaryKey(Token token, DecoratedKey partitionKey, Clustering clustering, Supplier primaryKeySupplier) { this.token = token; this.partitionKey = partitionKey; @@ -148,47 +148,53 @@ public ByteSource asComparableBytesMaxPrefix(ByteComparable.Version version) return asComparableBytes(ByteSource.GT_NEXT_COMPONENT, version, true); } - private ByteSource asComparableBytes(int terminator, ByteComparable.Version version, boolean isPrefix) + protected ByteSource asComparableBytes(int terminator, ByteComparable.Version version, boolean isPrefix) + { + return ByteSource.withTerminator(terminator, buildComparableSources(version, isPrefix, true)); + } + + protected ByteSource[] buildComparableSources(ByteComparable.Version version, boolean isPrefix, boolean includeToken) { // We need to make sure that the key is loaded before returning a - // byte comparable representation. If we don't we won't get a correct + // byte comparable representation. If we don't, we won't get a correct // comparison because we potentially won't be using the partition key // and clustering for the lookup loadDeferred(); - ByteSource tokenComparable = token.asComparableBytes(version); - ByteSource keyComparable = ByteSource.of(partitionKey.getKey(), version); + int size = (includeToken ? 1 : 0) + 1 + ((hasClustering() || !isPrefix) ? 1 : 0); + ByteSource[] comparableSources = new ByteSource[size]; - // It is important that the ClusteringComparator.asBytesComparable method is used - // to maintain the correct clustering sort order - ByteSource clusteringComparable = clusteringComparator.size() == 0 || - clustering == null || - clustering.isEmpty() ? null - : clusteringComparator.asByteComparable(clustering) - .asComparableBytes(version); + int index = 0; - // prefix doesn't include null components - if (isPrefix && clusteringComparable == null) - return ByteSource.withTerminator(terminator, tokenComparable, keyComparable); - else - return ByteSource.withTerminator(terminator, tokenComparable, keyComparable, clusteringComparable); + if (includeToken) + comparableSources[index++] = token.asComparableBytes(version); + + comparableSources[index++] = ByteSource.of(partitionKey.getKey(), version); + + if (hasClustering()) + // It is important that the ClusteringComparator.asBytesComparable method is used + // to maintain the correct clustering sort order + comparableSources[index++] = clusteringComparator.asByteComparable(clustering).asComparableBytes(version); + else if (!isPrefix) + // prefix doesn't include null components + comparableSources[index++] = null; + + assert index == comparableSources.length; + return comparableSources; } @Override public int compareTo(PrimaryKey o) { + // Always start comparison with token, since comparing directly on partition key requires loading it int cmp = token().compareTo(o.token()); - - // If the tokens don't match then we don't need to compare any more of the key. - // Otherwise if either this key or given key are token only, - // then we can only compare tokens - if ((cmp != 0) || isTokenOnly() || o.isTokenOnly()) + if (cmp != 0 || o.isTokenOnly()) return cmp; - // Next compare the partition keys. If they are not equal or + // Compare the partition keys. If they are not equal or // this is a single row partition key or there are no - // clusterings then we can return the result of this without - // needing to compare the clusterings + // clusterings, then return the result of this without + // needing to compare the clusterings. cmp = partitionKey().compareTo(o.partitionKey()); if (cmp != 0 || !hasClustering() || !o.hasClustering()) return cmp; diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/SSTableComponentsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2SSTableComponentsWriter.java similarity index 90% rename from src/java/org/apache/cassandra/index/sai/disk/v2/SSTableComponentsWriter.java rename to src/java/org/apache/cassandra/index/sai/disk/v2/V2SSTableComponentsWriter.java index f4bfe3c34e0e..3178fe029b0e 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v2/SSTableComponentsWriter.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2SSTableComponentsWriter.java @@ -32,11 +32,12 @@ import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; import org.apache.cassandra.index.sai.disk.v2.sortedterms.SortedTermsWriter; import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.Throwables; import org.apache.lucene.util.IOUtils; -public class SSTableComponentsWriter implements PerSSTableWriter +public class V2SSTableComponentsWriter implements PerSSTableWriter { - protected static final Logger logger = LoggerFactory.getLogger(SSTableComponentsWriter.class); + protected static final Logger logger = LoggerFactory.getLogger(V2SSTableComponentsWriter.class); private final IndexComponents.ForWrite perSSTableComponents; private final MetadataWriter metadataWriter; @@ -44,7 +45,7 @@ public class SSTableComponentsWriter implements PerSSTableWriter private final NumericValuesWriter blockFPWriter; private final SortedTermsWriter sortedTermsWriter; - public SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) throws IOException + public V2SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) throws IOException { this.perSSTableComponents = perSSTableComponents; this.metadataWriter = new MetadataWriter(perSSTableComponents); @@ -74,9 +75,11 @@ public void complete(Stopwatch stopwatch) throws IOException } @Override + @SuppressWarnings("ThrowableNotThrown") public void abort(Throwable accumulator) { logger.debug(perSSTableComponents.logMessage("Aborting per-SSTable index component writer for {}..."), perSSTableComponents.descriptor()); + Throwables.close(accumulator, tokenWriter, sortedTermsWriter, metadataWriter); perSSTableComponents.forceDeleteAllComponents(); } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java index 4910c470bf5a..6307d2a8addf 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v2/V2VectorIndexSearcher.java @@ -542,9 +542,9 @@ private SegmentRowIdOrdinalPairs flatmapPrimaryKeysToBitsAndRows(List metadata.maxSSTableRowId) { // The next greatest primary key is greater than all the primary keys in this segment @@ -576,8 +576,8 @@ private SegmentRowIdOrdinalPairs flatmapPrimaryKeysToBitsAndRows(List keysRemaining = keysInRange.subList(i, keysInRange.size()); int nextIndexForCeiling = Collections.binarySearch(keysRemaining, ceilingPrimaryKey); if (nextIndexForCeiling < 0) - // We got: -(insertion point) - 1. Invert it so we get the insertion point. - nextIndexForCeiling = -nextIndexForCeiling - 1; + // We got the inversion of the insertion point. Invert it to get the insertion point. + nextIndexForCeiling = ~nextIndexForCeiling; else ceilingPrimaryKeyMatchesKeyInRange = true; diff --git a/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskOrdinalsMap.java b/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskOrdinalsMap.java index 677abff6450f..25ef338ea6f6 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskOrdinalsMap.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v5/V5OnDiskOrdinalsMap.java @@ -334,7 +334,7 @@ public int getOrdinalForRowId(int rowId) public void forEachOrdinalInRange(int startRowId, int endRowId, OrdinalConsumer consumer) throws IOException { int rawIndex = Arrays.binarySearch(extraRowIds, startRowId); - int extraIndex = rawIndex >= 0 ? rawIndex : -rawIndex - 1; + int extraIndex = rawIndex >= 0 ? rawIndex : ~rawIndex; for (int rowId = max(0, startRowId); rowId <= min(endRowId, maxRowId); rowId++) { if (extraIndex < extraRowIds.length && extraRowIds[extraIndex] == rowId) diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/SkinnyPrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v9/SkinnyPrimaryKeyMap.java new file mode 100644 index 000000000000..e7ec5e2c429a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/SkinnyPrimaryKeyMap.java @@ -0,0 +1,320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.function.LongUnaryOperator; + +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import org.apache.cassandra.db.BufferDecoratedKey; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.v1.LongArray; +import org.apache.cassandra.index.sai.disk.v1.MetadataSource; +import org.apache.cassandra.index.sai.disk.v1.bitpack.BlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.MonotonicBlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; +import org.apache.cassandra.index.sai.disk.v2.PrimaryKeyWithSource; +import org.apache.cassandra.index.sai.disk.v9.keystore.KeyLookup; +import org.apache.cassandra.index.sai.disk.v9.keystore.KeyLookupMeta; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; + +/** + * A {@link PrimaryKeyMap} for skinny tables (those with no clustering columns). + *

+ * This uses the following on-disk structures: + *

    + *
  • A block-packed structure for rowId to token lookups using {@link BlockPackedReader}. + * Uses the {@link IndexComponentType#ROW_TO_TOKEN} component.
  • + *
  • A monotonic block packed structure for rowId to partitionId lookups using {@link MonotonicBlockPackedReader}. + * Uses the {@link IndexComponentType#ROW_TO_PARTITION} component.
  • + *
  • A key store for rowId to {@link PrimaryKey} and {@link PrimaryKey} to rowId lookups using + * {@link KeyLookup}. Uses the {@link IndexComponentType#PARTITION_KEY_BLOCKS} and + * {@link IndexComponentType#PARTITION_KEY_BLOCK_OFFSETS} components.
  • + *
+ *

+ * While the {@link Factory} is thread-safe, individual instances of the {@link SkinnyPrimaryKeyMap} + * are not. + */ +@NotThreadSafe +public class SkinnyPrimaryKeyMap implements PrimaryKeyMap +{ + @ThreadSafe + public static class Factory implements PrimaryKeyMap.Factory + { + // The class member is needed to avoid memory leaks and to be addressed by CNDB-17902 + @SuppressWarnings("unused") + private final IndexComponents.ForRead perSSTableComponents; + protected final SSTableId sstableId; + protected final boolean hasStaticColumns; + + protected final MetadataSource metadataSource; + protected final LongArray.Factory rowToTokenReaderFactory; + protected final LongArray.Factory rowToPartitionReaderFactory; + protected final KeyLookup partitionKeyReader; + protected final IPartitioner partitioner; + protected final V9RowAwarePrimaryKeyFactory primaryKeyFactory; + + private final FileHandle rowToTokenFile; + private final FileHandle rowToPartitionFile; + private final FileHandle partitionKeyBlockOffsetsFile; + private final FileHandle partitionKeyBlocksFile; + + public Factory(IndexComponents.ForRead perSSTableComponents, V9RowAwarePrimaryKeyFactory primaryKeyFactory, SSTableReader sstable) + { + FileHandle rowToTokenFileLocal = null; + FileHandle rowToPartitionFileLocal = null; + FileHandle partitionKeyBlockOffsetsFileLocal = null; + FileHandle partitionKeyBlocksFileLocal = null; + try + { + this.metadataSource = MetadataSource.loadMetadata(perSSTableComponents); + + NumericValuesMeta tokensMeta = new NumericValuesMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.ROW_TO_TOKEN))); + rowToTokenFileLocal = perSSTableComponents.get(IndexComponentType.ROW_TO_TOKEN).createFileHandle(); + this.rowToTokenReaderFactory = new BlockPackedReader(rowToTokenFileLocal, tokensMeta); + + NumericValuesMeta partitionsMeta = new NumericValuesMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.ROW_TO_PARTITION))); + rowToPartitionFileLocal = perSSTableComponents.get(IndexComponentType.ROW_TO_PARTITION).createFileHandle(); + this.rowToPartitionReaderFactory = new MonotonicBlockPackedReader(rowToPartitionFileLocal, partitionsMeta); + + NumericValuesMeta partitionKeyBlockOffsetsMeta = new NumericValuesMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS))); + KeyLookupMeta partitionKeysMeta = new KeyLookupMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.PARTITION_KEY_BLOCKS))); + partitionKeyBlocksFileLocal = perSSTableComponents.get(IndexComponentType.PARTITION_KEY_BLOCKS).createFileHandle(); + partitionKeyBlockOffsetsFileLocal = perSSTableComponents.get(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS).createFileHandle(); + this.partitionKeyReader = new KeyLookup(partitionKeyBlocksFileLocal, partitionKeyBlockOffsetsFileLocal, partitionKeysMeta, partitionKeyBlockOffsetsMeta); + } + catch (IOException e) + { + throw Throwables.unchecked(Throwables.close(e, rowToTokenFileLocal, rowToPartitionFileLocal, partitionKeyBlocksFileLocal, partitionKeyBlockOffsetsFileLocal)); + } + this.perSSTableComponents = perSSTableComponents; + + this.rowToTokenFile = rowToTokenFileLocal; + this.rowToPartitionFile = rowToPartitionFileLocal; + this.partitionKeyBlockOffsetsFile = partitionKeyBlockOffsetsFileLocal; + this.partitionKeyBlocksFile = partitionKeyBlocksFileLocal; + + this.partitioner = sstable.metadata().partitioner; + this.primaryKeyFactory = primaryKeyFactory; + this.sstableId = sstable.getId(); + this.hasStaticColumns = sstable.metadata().hasStaticColumns(); + } + + @Override + @SuppressWarnings({ "resource", "RedundantSuppression" }) + public PrimaryKeyMap newPerSSTablePrimaryKeyMap() + { + LongArray rowIdToToken = new LongArray.DeferredLongArray(rowToTokenReaderFactory::open); + LongArray rowIdToPartitionId = new LongArray.DeferredLongArray(rowToPartitionReaderFactory::open); + return new SkinnyPrimaryKeyMap(rowIdToToken, + rowIdToPartitionId, + partitionKeyReader.openCursor(), + partitioner, + primaryKeyFactory, + sstableId, + hasStaticColumns); + } + + @Override + public void close() + { + FileUtils.closeQuietly(Arrays.asList(rowToTokenFile, rowToPartitionFile, partitionKeyBlocksFile, partitionKeyBlockOffsetsFile)); + } + } + + protected final LongArray rowIdToTokenArray; + protected final LongArray rowIdToPartitionIdArray; + protected final KeyLookup.Cursor partitionKeyCursor; + protected final IPartitioner partitioner; + protected final V9RowAwarePrimaryKeyFactory primaryKeyFactory; + protected final SSTableId sstableId; + private final boolean hasStaticColumns; + + protected SkinnyPrimaryKeyMap(LongArray rowIdToTokenArray, + LongArray rowIdToPartitionIdArray, + KeyLookup.Cursor partitionKeyCursor, + IPartitioner partitioner, + V9RowAwarePrimaryKeyFactory primaryKeyFactory, + SSTableId sstableId, + boolean hasStaticColumns) + { + this.rowIdToTokenArray = rowIdToTokenArray; + this.rowIdToPartitionIdArray = rowIdToPartitionIdArray; + this.partitionKeyCursor = partitionKeyCursor; + this.partitioner = partitioner; + this.primaryKeyFactory = primaryKeyFactory; + this.sstableId = sstableId; + this.hasStaticColumns = hasStaticColumns; + } + + @Override + public SSTableId getSSTableId() + { + return sstableId; + } + + @Override + public long count() + { + return rowIdToTokenArray.length(); + } + + @Override + public PrimaryKey primaryKeyFromRowId(long sstableRowId) + { + long token = rowIdToTokenArray.get(sstableRowId); + return primaryKeyFactory.createDeferred(partitioner.getTokenFactory().fromLongValue(token), () -> supplier(sstableRowId)); + } + + @Override + public PrimaryKey primaryKeyFromRowId(long sstableRowId, PrimaryKey lowerBound, PrimaryKey upperBound) + { + return hasStaticColumns ? primaryKeyFromRowId(sstableRowId) + : primaryKeyFactory.createWithSource(this, sstableRowId, lowerBound, upperBound); + } + + /** + * Common implementation for row ID lookup operations that handles PrimaryKeyWithSource optimization + * and token collision detection. + * + * @param key the primary key to lookup + * @param tokenLookup function to perform the initial token-based lookup + * @param collisionDetection function to handle token collisions + * @return the row ID + */ + protected long lookupRowId(PrimaryKey key, + LongUnaryOperator tokenLookup, + BiFunction collisionDetection) + { + if (key instanceof PrimaryKeyWithSource) + { + PrimaryKeyWithSource pkws = (PrimaryKeyWithSource) key; + if (pkws.getSourceSstableId().equals(sstableId)) + return pkws.getSourceRowId(); + } + long rowId = tokenLookup.applyAsLong(key.token().getLongValue()); + if (key.isTokenOnly() || rowId < 0) + return rowId; + // The first index might not have been the correct match in the case of token collisions. + return collisionDetection.apply(key, rowId); + } + + @Override + public long exactRowIdOrInvertedCeiling(PrimaryKey key) + { + return lookupRowId(key, rowIdToTokenArray::indexOf, this::tokenCeilingCollisionDetection); + } + + @Override + public long ceiling(PrimaryKey key) + { + return lookupRowId(key, rowIdToTokenArray::ceilingIndex, this::tokenCeilingCollisionDetection); + } + + @Override + public long floor(PrimaryKey key) + { + return lookupRowId(key, rowIdToTokenArray::floorIndex, this::tokenFloorCollisionDetection); + } + + @Override + public void close() + { + FileUtils.closeQuietly(Arrays.asList(partitionKeyCursor, rowIdToTokenArray, rowIdToPartitionIdArray)); + } + + /** + * Generic token collision detection that handles both ceiling and floor operations. + * Look for token collision if the adjacent token in the token array matches the + * current token. If we find a collision, we need to compare the partition key instead. + * + * @param key the key to search for + * @param rowId the initial row ID from token lookup + * @param direction 1 for ceiling (forward search), -1 for floor (backward search) + * @return the adjusted row ID after collision detection + */ + private long tokenCollisionDetection(PrimaryKey key, long rowId, int direction) + { + assert direction == 1 || direction == -1 : "Direction must be 1 (ceiling) or -1 (floor)"; + + long tokenValue = key.token().getLongValue(); + long nextRowId = rowId + direction; + + // Look for collisions while we haven't reached the boundaries and tokens match + while (nextRowId >= 0 && nextRowId < rowIdToTokenArray.length() && tokenValue == rowIdToTokenArray.get(nextRowId)) + { + // For ceiling: check if the partition key at current rowId is >= lookup key + // For floor: check if the partition key at current rowId is <= lookup key + int comparison = readPartitionKey(rowId).compareTo(key.partitionKey()); + if ((direction == 1 && comparison >= 0) || (direction == -1 && comparison <= 0)) + return rowId; + + rowId = nextRowId; + nextRowId = rowId + direction; + } + // Note: We would normally expect to get here without going into the while loop + return rowId; + } + + protected long tokenCeilingCollisionDetection(PrimaryKey primaryKey, long rowId) + { + return tokenCollisionDetection(primaryKey, rowId, 1); + } + + protected long tokenFloorCollisionDetection(PrimaryKey primaryKey, long rowId) + { + return tokenCollisionDetection(primaryKey, rowId, -1); + } + + protected PrimaryKey supplier(long sstableRowId) + { + return primaryKeyFactory.create(readPartitionKey(sstableRowId), Clustering.EMPTY); + } + + protected DecoratedKey readPartitionKey(long sstableRowId) + { + long partitionId = rowIdToPartitionIdArray.get(sstableRowId); + ByteSource.Peekable peekable = ByteSource.peekable(partitionKeyCursor.seekToPointId(partitionId).asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + + byte[] keyBytes = ByteSourceInverse.getUnescapedBytes(peekable); + + assert keyBytes != null : "Primary key from map did not contain a partition key"; + + ByteBuffer keyBuffer = ByteBuffer.wrap(keyBytes); + return new BufferDecoratedKey(partitioner.getToken(keyBuffer), keyBuffer); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/V9OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v9/V9OnDiskFormat.java new file mode 100644 index 000000000000..3a8d172dcd1b --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/V9OnDiskFormat.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.EnumSet; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.index.sai.IndexContext; +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.v8.V8OnDiskFormat; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +/** + * Updates SAI OnDiskFormat to separate partition key and clustering key components. + */ +public class V9OnDiskFormat extends V8OnDiskFormat +{ + @VisibleForTesting + protected static final Set SKINNY_PER_SSTABLE_COMPONENTS = EnumSet.of(IndexComponentType.GROUP_COMPLETION_MARKER, + IndexComponentType.GROUP_META, + IndexComponentType.ROW_TO_TOKEN, + IndexComponentType.ROW_TO_PARTITION, + IndexComponentType.PARTITION_TO_SIZE, + IndexComponentType.PARTITION_KEY_BLOCKS, + IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS); + + @VisibleForTesting + protected static final Set WIDE_PER_SSTABLE_COMPONENTS = EnumSet.of(IndexComponentType.GROUP_COMPLETION_MARKER, + IndexComponentType.GROUP_META, + IndexComponentType.ROW_TO_TOKEN, + IndexComponentType.ROW_TO_PARTITION, + IndexComponentType.PARTITION_TO_SIZE, + IndexComponentType.PARTITION_KEY_BLOCKS, + IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS, + IndexComponentType.CLUSTERING_KEY_BLOCKS, + IndexComponentType.CLUSTERING_KEY_BLOCK_OFFSETS); + + public static final V9OnDiskFormat instance = new V9OnDiskFormat(); + + @Override + public PrimaryKey.Factory newPrimaryKeyFactory(ClusteringComparator comparator) + { + return new V9RowAwarePrimaryKeyFactory(comparator); + } + + @Override + public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexComponents.ForRead perSSTableComponents, PrimaryKey.Factory primaryKeyFactory, SSTableReader sstable) + { + Preconditions.checkArgument(primaryKeyFactory instanceof V9RowAwarePrimaryKeyFactory); + V9RowAwarePrimaryKeyFactory rowAwareFactory = (V9RowAwarePrimaryKeyFactory) primaryKeyFactory; + return rowAwareFactory.hasClustering ? new WidePrimaryKeyMap.Factory(perSSTableComponents, rowAwareFactory, sstable) + : new SkinnyPrimaryKeyMap.Factory(perSSTableComponents, rowAwareFactory, sstable); + } + + @Override + public PerSSTableWriter newPerSSTableWriter(IndexDescriptor indexDescriptor) throws IOException + { + return new V9SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); + } + + @Override + public Set perSSTableComponentTypes(boolean hasClustering) + { + return hasClustering ? WIDE_PER_SSTABLE_COMPONENTS : SKINNY_PER_SSTABLE_COMPONENTS; + } + + @Override + public int openFilesPerSSTable(boolean hasClustering) + { + // For the V9 format the number of open files depends on whether the table has clustering. + // The number of open files correspond to the number of components except {@link IndexComponentType.GROUP_COMPLETION_MARKER}. + return (hasClustering ? SKINNY_PER_SSTABLE_COMPONENTS.size() : WIDE_PER_SSTABLE_COMPONENTS.size()) - 1; + } + + @Override + public ByteOrder byteOrderFor(IndexComponentType indexComponentType, IndexContext context) + { + // The little-endian files are written by Lucene, and the upgrade to Lucene 9 switched the byte order from big to little. + switch (indexComponentType) + { + case META: + case GROUP_META: + case ROW_TO_TOKEN: + case ROW_TO_PARTITION: + case PARTITION_TO_SIZE: + case PARTITION_KEY_BLOCKS: + case CLUSTERING_KEY_BLOCKS: + case PARTITION_KEY_BLOCK_OFFSETS: + case CLUSTERING_KEY_BLOCK_OFFSETS: + case KD_TREE: + case KD_TREE_POSTING_LISTS: + return ByteOrder.LITTLE_ENDIAN; + case POSTING_LISTS: + return (context != null && context.isVector()) ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; + default: + return ByteOrder.BIG_ENDIAN; + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/V9RowAwarePrimaryKeyFactory.java b/src/java/org/apache/cassandra/index/sai/disk/v9/V9RowAwarePrimaryKeyFactory.java new file mode 100644 index 000000000000..a9daf8039ff6 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/V9RowAwarePrimaryKeyFactory.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9; + +import java.util.function.Supplier; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.disk.v2.V2RowAwarePrimaryKeyFactory; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * Factory for creating row aware primary keys, which does not use the token as prefix in byte comparison. + * Thus, the byte comparables are not prefixed with tokens and allowing better compression. + */ +public class V9RowAwarePrimaryKeyFactory extends V2RowAwarePrimaryKeyFactory +{ + public V9RowAwarePrimaryKeyFactory(ClusteringComparator clusteringComparator) + { + super(clusteringComparator); + } + + @Override + public PrimaryKey createDeferred(Token token, Supplier primaryKeySupplier) + { + return new RowAwarePrimaryKey(token, null, null, primaryKeySupplier); + } + + @Override + public PrimaryKey create(DecoratedKey partitionKey, Clustering clustering) + { + return new RowAwarePrimaryKey(partitionKey.getToken(), partitionKey, clustering, null); + } + + private class RowAwarePrimaryKey extends V2RowAwarePrimaryKeyFactory.RowAwarePrimaryKey + { + private RowAwarePrimaryKey(Token token, DecoratedKey partitionKey, Clustering clustering, Supplier primaryKeySupplier) + { + super(token, partitionKey, clustering, primaryKeySupplier); + } + + @Override + protected ByteSource asComparableBytes(int terminator, ByteComparable.Version version, boolean isPrefix) + { + return ByteSource.withTerminator(terminator, buildComparableSources(version, isPrefix, false)); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/V9SSTableComponentsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v9/V9SSTableComponentsWriter.java new file mode 100644 index 000000000000..2df832f39cfa --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/V9SSTableComponentsWriter.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9; + +import java.io.IOException; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.v1.MetadataWriter; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; +import org.apache.cassandra.index.sai.disk.v9.keystore.KeyStoreWriter; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.lucene.util.IOUtils; + +public class V9SSTableComponentsWriter implements PerSSTableWriter +{ + protected static final Logger logger = LoggerFactory.getLogger(V9SSTableComponentsWriter.class); + + /** + * Default block shift value for partition keys. + * Used to determine the block size and block mask for the partition key store writer. + * The blocks should not be too small and not be too large. + * See {@link KeyStoreWriter} for details on how this affects index size and performance. + */ + private static final int DEFAULT_PARTITION_BLOCK_SHIFT = 4; + + /** + * Default block shift value for clustering keys. + * Used to determine the block size and block mask for the clustering key store writer. + * The blocks should not be too small and not be too large. + * See {@link KeyStoreWriter} for details on how this affects index size and performance. + */ + private static final int DEFAULT_CLUSTERING_BLOCK_SHIFT = 4; + + /** + * Configurable partition block shift. Can be set for testing/benchmarking purposes. + */ + private static volatile int partitionBlockShift = DEFAULT_PARTITION_BLOCK_SHIFT; + + /** + * Configurable clustering block shift. Can be set for testing/benchmarking purposes. + */ + private static volatile int clusteringBlockShift = DEFAULT_CLUSTERING_BLOCK_SHIFT; + + private final IndexComponents.ForWrite perSSTableComponents; + private final MetadataWriter metadataWriter; + private final NumericValuesWriter tokenWriter; + private final NumericValuesWriter partitionSizeWriter; + private final NumericValuesWriter partitionRowsWriter; + private final KeyStoreWriter partitionKeysWriter; + private final KeyStoreWriter clusteringKeysWriter; + + private Token prevToken = null; + private long partitionId = -1; + // This is used to record the number of rows in each partition + private long partitionRowCount = 0; + + @SuppressWarnings("resource") + public V9SSTableComponentsWriter(IndexComponents.ForWrite perSSTableComponents) throws IOException + { + this.perSSTableComponents = perSSTableComponents; + this.metadataWriter = new MetadataWriter(perSSTableComponents); + this.tokenWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.ROW_TO_TOKEN), + metadataWriter, false); + + this.partitionRowsWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.ROW_TO_PARTITION), metadataWriter, true); + this.partitionSizeWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.PARTITION_TO_SIZE), metadataWriter, false); + NumericValuesWriter partitionKeyBlockOffsetWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS), metadataWriter, true); + this.partitionKeysWriter = new KeyStoreWriter(perSSTableComponents.addOrGet(IndexComponentType.PARTITION_KEY_BLOCKS), + metadataWriter, + partitionKeyBlockOffsetWriter, + partitionBlockShift, + false); + if (perSSTableComponents.hasClustering()) + { + NumericValuesWriter clusteringKeyBlockOffsetWriter = new NumericValuesWriter(perSSTableComponents.addOrGet(IndexComponentType.CLUSTERING_KEY_BLOCK_OFFSETS), metadataWriter, true); + this.clusteringKeysWriter = new KeyStoreWriter(perSSTableComponents.addOrGet(IndexComponentType.CLUSTERING_KEY_BLOCKS), + metadataWriter, + clusteringKeyBlockOffsetWriter, + clusteringBlockShift, + true); + } + else + { + this.clusteringKeysWriter = null; + } + } + + /** + * Sets the partition block shift value. Primarily for testing and benchmarking. + * + * @param shift the block shift value + */ + @VisibleForTesting + public static void setPartitionBlockShift(int shift) + { + partitionBlockShift = shift; + } + + /** + * Sets the clustering block shift value. Primarily for testing and benchmarking. + * + * @param shift the block shift value + */ + @VisibleForTesting + public static void setClusteringBlockShift(int shift) + { + clusteringBlockShift = shift; + } + + @Override + public void startPartition(DecoratedKey partitionKey, long position) throws IOException + { + if (partitionId >= 0) + { + if (prevToken.compareTo(partitionKey.getToken()) >= 0) + throw new IllegalArgumentException("Partition keys must be in ascending token order"); + + partitionSizeWriter.add(partitionRowCount); + } + prevToken = partitionKey.getToken(); + + partitionId++; + partitionRowCount = 0; + partitionKeysWriter.add(v -> ByteSource.of(partitionKey.getKey(), v)); + if (perSSTableComponents.hasClustering()) + clusteringKeysWriter.startPartition(); + } + + @Override + public void nextRow(PrimaryKey primaryKey) throws IOException + { + assert partitionId >= 0; + + tokenWriter.add(primaryKey.token().getLongValue()); + partitionRowsWriter.add(partitionId); + partitionRowCount++; + if (perSSTableComponents.hasClustering()) + clusteringKeysWriter.add(perSSTableComponents.comparator().asByteComparable(primaryKey.clustering())); + } + + @Override + public void complete(Stopwatch stopwatch) throws IOException + { + partitionSizeWriter.add(partitionRowCount); + IOUtils.close(tokenWriter, partitionSizeWriter, partitionRowsWriter, + partitionKeysWriter, clusteringKeysWriter, metadataWriter); + perSSTableComponents.markComplete(); + } + + @Override + @SuppressWarnings("ThrowableNotThrown") + public void abort(Throwable accumulator) + { + logger.debug(perSSTableComponents.logMessage("Aborting per-SSTable index component writer for {}..."), perSSTableComponents.descriptor()); + Throwables.close(accumulator, tokenWriter, partitionSizeWriter, partitionRowsWriter, + partitionKeysWriter, clusteringKeysWriter, metadataWriter); + perSSTableComponents.forceDeleteAllComponents(); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/WidePrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v9/WidePrimaryKeyMap.java new file mode 100644 index 000000000000..45b3d30a58ed --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/WidePrimaryKeyMap.java @@ -0,0 +1,338 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9; + +import java.io.IOException; +import java.util.Arrays; + +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.v1.LongArray; +import org.apache.cassandra.index.sai.disk.v1.bitpack.BlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; +import org.apache.cassandra.index.sai.disk.v2.PrimaryKeyWithSource; +import org.apache.cassandra.index.sai.disk.v9.keystore.KeyLookup; +import org.apache.cassandra.index.sai.disk.v9.keystore.KeyLookupMeta; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.sstable.SSTableId; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.bytecomparable.ByteSource; + +/** + * An extension of the {@link SkinnyPrimaryKeyMap} for wide tables (those with clustering columns). + *

+ * This used the following additional on-disk structures to the {@link SkinnyPrimaryKeyMap} + *

    + *
  • A block-packed structure for partitionId to partition size (number of rows in the partition) lookups using + * {@link BlockPackedReader}. Uses the {@link IndexComponentType#PARTITION_TO_SIZE} component
  • + *
  • A key store for rowId to {@link Clustering} and {@link Clustering} to rowId lookups using + * {@link KeyLookup}. Uses the {@link org.apache.cassandra.index.sai.disk.format.IndexComponentType#CLUSTERING_KEY_BLOCKS} and + * {@link org.apache.cassandra.index.sai.disk.format.IndexComponentType#CLUSTERING_KEY_BLOCK_OFFSETS} components
  • + *
+ * While the {@link Factory} is thread-safe, individual instances of the {@link WidePrimaryKeyMap} + * are not. + */ +@NotThreadSafe +public class WidePrimaryKeyMap extends SkinnyPrimaryKeyMap +{ + @ThreadSafe + public static class Factory extends SkinnyPrimaryKeyMap.Factory + { + // The class member is needed to avoid memory leaks and to be addressed by CNDB-17902 + private final ClusteringComparator clusteringComparator; + private final KeyLookup clusteringKeyReader; + private final LongArray.Factory partitionToSizeReaderFactory; + private final FileHandle clusteringKeyBlockOffsetsFile; + private final FileHandle clustingingKeyBlocksFile; + private final FileHandle partitionToSizeFile; + + public Factory(IndexComponents.ForRead perSSTableComponents, + V9RowAwarePrimaryKeyFactory primaryKeyFactory, + SSTableReader sstable) + { + super(perSSTableComponents, primaryKeyFactory, sstable); + + FileHandle clusteringKeyBlockOffsetsFileLocal = null; + FileHandle clustingingKeyBlocksFileLocal = null; + FileHandle partitionToSizeFileLocal = null; + + try + { + clusteringKeyBlockOffsetsFileLocal = perSSTableComponents.get(IndexComponentType.CLUSTERING_KEY_BLOCK_OFFSETS).createFileHandle(); + clustingingKeyBlocksFileLocal = perSSTableComponents.get(IndexComponentType.CLUSTERING_KEY_BLOCKS).createFileHandle(); + + NumericValuesMeta partitionSizeMeta = new NumericValuesMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.PARTITION_TO_SIZE))); + partitionToSizeFileLocal = perSSTableComponents.get(IndexComponentType.PARTITION_TO_SIZE).createFileHandle(); + this.partitionToSizeReaderFactory = new BlockPackedReader(partitionToSizeFileLocal, partitionSizeMeta); + + NumericValuesMeta clusteringKeyBlockOffsetsMeta = new NumericValuesMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.CLUSTERING_KEY_BLOCK_OFFSETS))); + KeyLookupMeta clusteringKeyMeta = new KeyLookupMeta(metadataSource.get(perSSTableComponents.get(IndexComponentType.CLUSTERING_KEY_BLOCKS))); + this.clusteringKeyReader = new KeyLookup(clustingingKeyBlocksFileLocal, clusteringKeyBlockOffsetsFileLocal, + clusteringKeyMeta, clusteringKeyBlockOffsetsMeta); + } + catch (IOException e) + { + throw Throwables.unchecked(Throwables.close(e, clusteringKeyBlockOffsetsFileLocal, clustingingKeyBlocksFileLocal, partitionToSizeFileLocal)); + } + this.clusteringComparator = sstable.metadata().comparator; + + this.clusteringKeyBlockOffsetsFile = clusteringKeyBlockOffsetsFileLocal; + this.clustingingKeyBlocksFile = clustingingKeyBlocksFileLocal; + this.partitionToSizeFile = partitionToSizeFileLocal; + } + + @Override + @SuppressWarnings({ "resource", "RedundantSuppression" }) + public PrimaryKeyMap newPerSSTablePrimaryKeyMap() + { + LongArray rowIdToToken = new LongArray.DeferredLongArray(rowToTokenReaderFactory::open); + LongArray partitionIdToToken = new LongArray.DeferredLongArray(rowToPartitionReaderFactory::open); + LongArray partitionIdToSize = new LongArray.DeferredLongArray(partitionToSizeReaderFactory::open); + + return new WidePrimaryKeyMap(rowIdToToken, + partitionIdToToken, + partitionIdToSize, + partitionKeyReader.openCursor(), + clusteringKeyReader.openCursor(), + partitioner, + primaryKeyFactory, + clusteringComparator, + sstableId, + hasStaticColumns); + } + + @Override + public void close() + { + FileUtils.closeQuietly(Arrays.asList(clustingingKeyBlocksFile, clusteringKeyBlockOffsetsFile, partitionToSizeFile)); + super.close(); + } + } + + private final LongArray partitionIdToSizeArray; + private final ClusteringComparator clusteringComparator; + private final KeyLookup.Cursor clusteringKeyCursor; + + private WidePrimaryKeyMap(LongArray rowIdToTokenArray, + LongArray rowIdToPartitionIdArray, + LongArray partitionIdToSizeArray, + KeyLookup.Cursor partitionKeyCursor, + KeyLookup.Cursor clusteringKeyCursor, + IPartitioner partitioner, + V9RowAwarePrimaryKeyFactory primaryKeyFactory, + ClusteringComparator clusteringComparator, + SSTableId sstableId, + boolean hasStaticColumns) + { + super(rowIdToTokenArray, rowIdToPartitionIdArray, partitionKeyCursor, partitioner, primaryKeyFactory, + sstableId, hasStaticColumns); + + this.partitionIdToSizeArray = partitionIdToSizeArray; + this.clusteringComparator = clusteringComparator; + this.clusteringKeyCursor = clusteringKeyCursor; + } + + /** + * Returns a row Id for a {@link PrimaryKey}. If there is no such term, + * returns the `-(next row id) - 1` where `next row id` is the row id + * of the next greatest {@link PrimaryKey} in the map. + * + * @param key the {@link PrimaryKey} to lookup + * @return a row id + */ + @Override + public long exactRowIdOrInvertedCeiling(PrimaryKey key) + { + if (key instanceof PrimaryKeyWithSource) + { + PrimaryKeyWithSource pkws = (PrimaryKeyWithSource) key; + if (pkws.getSourceSstableId().equals(sstableId)) + return pkws.getSourceRowId(); + } + + // Find the partition using the token array for initial lookup + long rowId = rowIdToTokenArray.indexOf(key.token().getLongValue()); + if (key.isTokenOnly() || rowId < 0) + return rowId; + // If we have skipped a token (shouldn't happen with indexOf, but check for safety) + if (rowIdToTokenArray.get(rowId) != key.token().getLongValue()) + return rowId; + + // Handle token collisions by comparing partition keys using partitionKeyCursor + rowId = tokenCeilingCollisionDetection(key, rowId); + if (key.clustering().isEmpty()) + return rowId; + + // Now search within the partition for the clustering key + long nextPartitionStart = jumpToNextPartitionStart(rowId); + long clusteringRowId = clusteringKeyCursor.clusteredSeekToKey( + clusteringComparator.asByteComparable(key.clustering()), rowId, nextPartitionStart); + + // clusteredSeekToKey returns the ceiling (next greater or equal key) or -1 if not found + if (clusteringRowId < 0) + return Long.MIN_VALUE; + assert clusteringRowId < rowIdToTokenArray.length() : "Row ID should not be after the last row"; + + // If clusteringRowId points to the next partition, it means the search key is greater + // than all keys in the current partition. Return the inverted ceiling. + if (clusteringRowId >= nextPartitionStart) + return ~clusteringRowId; + + Clustering foundClustering = readClusteringKey(clusteringRowId); + // If STATIC CLUSTERING, then no clustering key is present. + if (foundClustering.isEmpty()) + return ~clusteringRowId; + + // Check if this is an exact match by comparing the clustering key + int cmp = clusteringComparator.compare(foundClustering, key.clustering()); + if (cmp == 0) + return clusteringRowId; + else + return ~clusteringRowId; + } + + /** + * Returns the row ID of the smallest primary key greater than or equal to the given key. + * Returns -1 if no such key exists (i.e., the given key is greater than all keys in the map). + *

+ * For wide tables, this method leverages {@link #exactRowIdOrInvertedCeiling(PrimaryKey)} + * and converts the inverted ceiling format to a regular ceiling. + * + * @param key the primary key to find the ceiling for + * @return the row ID of the ceiling key, or -1 if no ceiling exists + */ + @Override + public long ceiling(PrimaryKey key) + { + long rowId = exactRowIdOrInvertedCeiling(key); + if (rowId >= 0) + return rowId; + else if (rowId == Long.MIN_VALUE) + return -1; + else + return ~rowId; + } + + /** + * Returns the row ID of the greatest primary key less than or equal to the given key. + * Returns -1 if no such key exists (i.e., the given key is less than all keys in the map). + *

+ * For wide tables, this method handles both token-only keys and full primary keys with clustering. + * For token-only keys, it returns the last row of the matching partition if found. + * + * @param key the primary key to find the floor for + * @return the row ID of the floor key, or -1 if no floor exists + */ + @Override + public long floor(PrimaryKey key) + { + if (key instanceof PrimaryKeyWithSource) + { + PrimaryKeyWithSource pkws = (PrimaryKeyWithSource) key; + if (pkws.getSourceSstableId().equals(sstableId)) + return pkws.getSourceRowId(); + } + + long rowId = exactRowIdOrInvertedCeiling(key); + + // Exact match + if (rowId >= 0) + { + // If the key is a prefix (token-only or partition-only), + // the floor is the *greatest* row ID associated with this prefix. + if (key.isTokenOnly() || key.clustering().isEmpty()) + return startOfNextPartition(rowId) - 1; + + // If STATIC CLUSTERING, then rowID is the last row in the matching partition. + // Floor reverts to return the first row in the partition instead. + if (readClusteringKey(rowId).isEmpty()) + { + long partitionId = rowIdToPartitionIdArray.get(rowId); + return rowIdToPartitionIdArray.ceilingIndex(partitionId); + } + + return rowId; + } + + if (rowId == Long.MIN_VALUE) + return rowIdToTokenArray.length() - 1; + + // rowId is -(ceiling) - 1. The floor is the row immediately before the ceiling. + return -rowId - 2; + } + + @Override + public void close() + { + super.close(); + FileUtils.closeQuietly(clusteringKeyCursor, partitionIdToSizeArray); + } + + @Override + protected PrimaryKey supplier(long sstableRowId) + { + return primaryKeyFactory.create(readPartitionKey(sstableRowId), readClusteringKey(sstableRowId)); + } + + private Clustering readClusteringKey(long sstableRowId) + { + ByteSource.Peekable peekable = ByteSource.peekable(clusteringKeyCursor.seekToPointId(sstableRowId) + .asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + + Clustering clustering = clusteringComparator.clusteringFromByteComparable(ByteBufferAccessor.instance, v -> peekable, TypeUtil.BYTE_COMPARABLE_VERSION); + + if (clustering == null) + clustering = Clustering.EMPTY; + + return clustering; + } + + // Returns the rowId of the next partition or the number of rows if supplied rowId is in the last partition. + // Requires that given row id is the first row in the current partition + private long jumpToNextPartitionStart(long partitionStartRowId) + { + long partitionSize = partitionIdToSizeArray.get(rowIdToPartitionIdArray.get(partitionStartRowId)); + return partitionSize == -1 ? rowIdToPartitionIdArray.length() : partitionStartRowId + partitionSize; + } + + // Returns the first rowId of the next partition or the number of rows if supplied rowId is in the last partition + private long startOfNextPartition(long rowId) + { + long partitionId = rowIdToPartitionIdArray.get(rowId); + long partitionSize = partitionIdToSizeArray.get(partitionId); + if (partitionSize == -1) + return rowIdToPartitionIdArray.length(); + + // Find the first row of this partition, then add partition size + long firstRowOfPartition = rowIdToPartitionIdArray.ceilingIndex(partitionId); + return firstRowOfPartition + partitionSize; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookup.java b/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookup.java new file mode 100644 index 000000000000..6a7e4149636d --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookup.java @@ -0,0 +1,458 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9.keystore; + +import java.io.IOException; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; + +import org.apache.cassandra.index.sai.disk.io.IndexInputReader; +import org.apache.cassandra.index.sai.disk.v1.LongArray; +import org.apache.cassandra.index.sai.disk.v1.bitpack.MonotonicBlockPackedReader; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.utils.FastByteOperations; +import org.apache.cassandra.utils.Throwables; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; + +/** + * Provides read access to an on-disk sequence of partition or clustering keys written by {@link KeyStoreWriter}. + *

+ * It's important that the implementation is performant. + *

+ * Because the blocks are prefix compressed, random access applies only to the locating the whole block. + * In order to jump to a concrete key inside the block, the block keys are iterated from the block beginning. + * + * @see KeyStoreWriter + */ +@NotThreadSafe +public class KeyLookup +{ + public static final String INDEX_OUT_OF_BOUNDS = "The target point id [%d] cannot be less than 0 or greater than or equal to the key count [%d]"; + private final FileHandle keysFileHandle; + private final KeyLookupMeta keyLookupMeta; + private final LongArray.Factory keyBlockOffsetsFactory; + + /** + * Creates a new reader based on its data components. + *

+ * It does not own the components, so you must close them separately after you're done with the reader. + * + * @param keysFileHandle handle to the file with a sequence of prefix-compressed blocks, + * each storing a fixed number of keys + * @param keysBlockOffsets handle to the file containing an encoded sequence of the file offsets pointing to the blocks + * @param keyLookupMeta metadata object created earlier by the writer + * @param keyBlockOffsetsMeta metadata object for the block offsets + */ + public KeyLookup(@Nonnull FileHandle keysFileHandle, + @Nonnull FileHandle keysBlockOffsets, + @Nonnull KeyLookupMeta keyLookupMeta, + @Nonnull NumericValuesMeta keyBlockOffsetsMeta) throws IOException + { + this.keysFileHandle = keysFileHandle; + this.keyLookupMeta = keyLookupMeta; + this.keyBlockOffsetsFactory = new MonotonicBlockPackedReader(keysBlockOffsets, keyBlockOffsetsMeta); + } + + /** + * Opens a cursor over the keys stored in the keys file. + *

+ * This will read the first key into the key buffer and point to the first point in the keys file. + *

+ * The cursor is to be used in a single thread. + * The cursor is valid as long as this object hasn't been closed. + * You must close the cursor when you no longer need it. + */ + public @Nonnull Cursor openCursor() + { + if (keyLookupMeta.keyCount == 0) + return new EmptyCursor(); + return new IndexInputCursor(keysFileHandle, keyBlockOffsetsFactory); + } + + /** + * This interface is introduced a workaround, when a cursor is open for partition with no data. + * For this an Empty Cursor is implemented + *

+ * Otherwise, the main goal is to allow reading the keys from a keys file + * and quickly seek to a random key by point id. + *

+ * Its instances can be stateful and not thread-safe and + * maintain a position to the current key as well as a buffer that can hold one key. + */ + @NotThreadSafe + public interface Cursor extends AutoCloseable + { + /** + * Finds a pointId for the clustering key in a partition defined by the range of point ids. + * The start and end of the range must not exceed the number of keys available. + * The keys within the range are expected to be in lexicographical order. + *

+ * Should not be used without clustering. + * + * @param key the key to seek for within the partition + * @param startingPointId the inclusive starting point for the partition + * @param endingPointId the exclusive ending point for the partition + * Note: this can be equal to the number of keys if this is the last partition + * @return a {@code long} representing the pointId of the closest key from the partition + * that is >= to the key passed to the method, or -1 if the key passed is > all the keys. + */ + long clusteredSeekToKey(ByteComparable key, long startingPointId, long endingPointId); + + /** + * Positions the cursor on the target point id and reads the key at the target to the current key buffer. + *

+ * It is allowed to position the cursor before the first item or after the last item; + * in these cases the internal buffer is cleared. + * + * @param target point id to lookup + * @return The {@link ByteComparable} containing the key + * @throws IndexOutOfBoundsException if the target point id is less than -1 or greater than the number of keys + */ + @Nonnull + ByteComparable seekToPointId(long target); + + void close() throws IOException; + } + + /** + * An empty cursor implementation that is returned when keyCount is 0. + * This cursor has no keys, and all operations either throw exceptions or return sentinel values. + */ + private static class EmptyCursor implements Cursor + { + @Override + public long clusteredSeekToKey(ByteComparable key, long startingPointId, long endingPointId) + { + return -1; + } + + @Override + public @Nonnull ByteComparable seekToPointId(long target) + { + throw new IndexOutOfBoundsException(String.format(INDEX_OUT_OF_BOUNDS, target, 0)); + } + + + @Override + public void close() + { + // No-op for empty cursor + } + } + + /** + * Allows reading the keys from the keys file. + * Can quickly seek to a random key by point id. + *

+ * This object is stateful and not thread-safe. + * It maintains a position to the current key as well as a buffer that can hold one key. + */ + @NotThreadSafe + public class IndexInputCursor implements Cursor + { + private final IndexInputReader keysInput; + private final int blockShift; + private final int blockMask; + private final boolean clustering; + private final long keysFilePointer; + private final LongArray blockOffsets; + + // The key the cursor currently points to. Initially empty. + private final BytesRef currentKey; + + // A temporary buffer used to hold the key at the start of the next block. + private final BytesRef nextBlockKey; + + // The point id the cursor currently points to. + private long currentPointId; + private long currentBlockIndex; + + IndexInputCursor(FileHandle keysFileHandle, LongArray.Factory blockOffsetsFactory) + { + try + { + this.keysInput = IndexInputReader.create(keysFileHandle); + SAICodecUtils.validate(this.keysInput); + + this.blockShift = this.keysInput.readVInt(); + this.blockMask = (1 << this.blockShift) - 1; + this.clustering = this.keysInput.readByte() == 1; + } + catch (IOException e) + { + throw Throwables.unchecked(Throwables.close(e, keysFileHandle)); + } + + this.keysFilePointer = this.keysInput.getFilePointer(); + this.blockOffsets = new LongArray.DeferredLongArray(blockOffsetsFactory::open); + this.currentKey = new BytesRef(keyLookupMeta.maxKeyLength); + this.nextBlockKey = new BytesRef(keyLookupMeta.maxKeyLength); + + keysInput.seek(keysFilePointer); + readKey(currentPointId, currentKey); + } + + /** + * Finds a pointId for the clustering key within a partition. + *

+ * It assumes the keys within the partition are sorted, as it uses binary search. + *

+ * If the key is not in the block containing the start of the range, a binary search is done to find + * the block containing the search key. That block is then searched to return the pointId that corresponds + * to the key that is either equal to or next highest to the search key. + */ + @Override + public long clusteredSeekToKey(ByteComparable key, long startingPointId, long endingPointId) + { + assert clustering : "Requires clustering so the clustering keys are sorted"; + + BytesRef searchKey = asBytesRef(key); + + positionAtPointId(startingPointId); + + // We can return immediately if the currentPointId is within the requested partition range and the keys match + if (currentPointId >= startingPointId && currentPointId < endingPointId && compareKeys(currentKey, searchKey) == 0) + return currentPointId; + + binarySearchKeyInRange(startingPointId, endingPointId, searchKey); + + // Depending on where we are in the block, we may need to move forwards to the starting point ID + while (currentPointId < startingPointId) + advanceToNextKey(); + + // Move forward to the ending point ID, returning the point ID if we find our key + while (currentPointId < endingPointId) + { + if (compareKeys(currentKey, searchKey) >= 0) + return currentPointId; + + if (!advanceToNextKey()) + return -1; + } + return endingPointId < keyLookupMeta.keyCount ? endingPointId : -1; + } + + private void binarySearchKeyInRange(long startingPointId, long endingPointId, BytesRef searchKey) + { + long lowSearchId = startingPointId; + long highSearchId = endingPointId; + + // We will keep going with the binary shift while the search consists of at least one block + while ((highSearchId - lowSearchId) >>> blockShift > 0) + { + long midSearchId = lowSearchId + (highSearchId - lowSearchId) / 2; + + // See if searchKey exists in the block containing the midSearchId or is above or below it + int position = moveToBlockAndCompareTo(midSearchId, searchKey); + + if (position == 0) + { + lowSearchId = currentPointId; + break; + } + + if (position < 0) + highSearchId = midSearchId; + else + lowSearchId = midSearchId; + } + + positionAtPointId(lowSearchId); + } + + /** + * Positions the cursor at the specified point ID by moving to the appropriate block + * and reading the key at that position. + */ + private void positionAtPointId(long pointId) + { + updateCurrentBlockIndex(pointId); + resetToCurrentBlock(); + } + + /** + * Advances to the next key in the sequence. + * + * @return true if successfully advanced, false if reached the end of keys + */ + private boolean advanceToNextKey() + { + currentPointId++; + if (currentPointId == keyLookupMeta.keyCount) + return false; + + readCurrentKey(); + updateCurrentBlockIndex(currentPointId); + return true; + } + + /** + * Moves to a block and determines if the key is in the block. + * + * @return -1 if the key is before the block, 0 if the key is in the block, 1 if the key is after the block + */ + private int moveToBlockAndCompareTo(long pointId, BytesRef key) + { + positionAtPointId(pointId); + + if (compareKeys(key, currentKey) < 0) + return -1; + + // If we are in the last block, we will assume for now that the key is in the last block and defer + // the final decision to later (if we can't find it). + if (currentBlockIndex == blockOffsets.length() - 1) + return 0; + + // Finish by getting the starting key of the next block and comparing that with the key. + keysInput.seek(blockOffsets.get(currentBlockIndex + 1) + keysFilePointer); + readKey((currentBlockIndex + 1) << blockShift, nextBlockKey); + return compareKeys(key, nextBlockKey) < 0 ? 0 : 1; + } + + private void updateCurrentBlockIndex(long pointId) + { + currentBlockIndex = pointId >>> blockShift; + } + + // Reset currentPointId and currentKey to be at the start of the block pointed to by currentBlockIndex. + private void resetToCurrentBlock() + { + keysInput.seek(blockOffsets.get(currentBlockIndex) + keysFilePointer); + currentPointId = currentBlockIndex << blockShift; + readCurrentKey(); + } + + private void readCurrentKey() + { + readKey(currentPointId, currentKey); + } + + // Read the next key indicated by pointId. + // + // Note: pointId is only used to determine whether we are at the start of a block. It is + // important that resetPosition is called prior to multiple calls to readKey. It is + // easy to get out of position. + private void readKey(long pointId, BytesRef key) + { + try + { + int prefixLength; + int suffixLength; + if ((pointId & blockMask) == 0L) + { + prefixLength = 0; + suffixLength = keysInput.readVInt(); + } + else + { + // Read the prefix and suffix lengths following the compression mechanism described + // in the KeyStoreWriterWriter. If the lengths contained in the starting byte are less + // than the 4-bit maximum, then nothing further is read. Otherwise, the lengths in the + // following vints are added. + int compressedLengths = Byte.toUnsignedInt(keysInput.readByte()); + prefixLength = compressedLengths & 0x0F; + suffixLength = compressedLengths >>> 4; + if (prefixLength == 15) + prefixLength += keysInput.readVInt(); + if (suffixLength == 15) + suffixLength += keysInput.readVInt(); + } + + assert prefixLength + suffixLength <= keyLookupMeta.maxKeyLength; + if (prefixLength + suffixLength > 0) + { + key.length = prefixLength + suffixLength; + // The currentKey is appended to as the suffix for the current key is + // added to the existing prefix. + keysInput.readBytes(key.bytes, prefixLength, suffixLength); + } + } + catch (IOException e) + { + throw Throwables.cleaned(e); + } + } + + private int compareKeys(BytesRef left, BytesRef right) + { + return FastByteOperations.compareUnsigned(left.bytes, left.offset, left.offset + left.length, + right.bytes, right.offset, right.offset + right.length); + } + + private BytesRef asBytesRef(ByteComparable source) + { + BytesRefBuilder builder = new BytesRefBuilder(); + + ByteSource byteSource = source.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION); + int val; + while ((val = byteSource.next()) != ByteSource.END_OF_STREAM) + builder.append((byte) val); + return builder.get(); + } + + /** + * Positions the cursor on the target point id and reads the key at the target to the current key buffer. + *

+ * It is allowed to position the cursor before the first item or after the last item; + * in these cases the internal buffer is cleared. + * + * @param target point id to lookup + * @return The {@link ByteComparable} containing the key + * @throws IndexOutOfBoundsException if the target point id is less than -1 or greater than the number of keys + */ + @Override + public @Nonnull ByteComparable seekToPointId(long target) + { + if (target <= -1 || target >= keyLookupMeta.keyCount) + throw new IndexOutOfBoundsException(String.format(INDEX_OUT_OF_BOUNDS, target, keyLookupMeta.keyCount)); + + if (target != currentPointId) + { + long blockIndex = target >>> blockShift; + // We need to reset the block if the block index has changed or the target < currentPointId. + // We can read forward in the same block without a reset, but we can't read backwards, and token + // collision can result in us moving backwards. + if (blockIndex != currentBlockIndex || target < currentPointId) + { + currentBlockIndex = blockIndex; + resetToCurrentBlock(); + } + + // Advance forward to the target position + while (currentPointId < target) + advanceToNextKey(); + } + + return ByteComparable.preencoded(TypeUtil.BYTE_COMPARABLE_VERSION, currentKey.bytes, currentKey.offset, currentKey.length); + } + + @Override + public void close() throws IOException + { + blockOffsets.close(); + keysInput.close(); + } + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookupMeta.java b/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookupMeta.java new file mode 100644 index 000000000000..ffd544c59470 --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookupMeta.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9.keystore; + +import java.io.IOException; + +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; + +/** + * Metadata produced by {@link KeyStoreWriter}, needed by {@link KeyLookup}. + */ +public class KeyLookupMeta +{ + public final long keyCount; + public final int maxKeyLength; + + public KeyLookupMeta(IndexInput input) throws IOException + { + this.keyCount = input.readLong(); + this.maxKeyLength = input.readInt(); + } + + public static void write(IndexOutput output, long keyCount, int maxKeyLength) throws IOException + { + output.writeLong(keyCount); + output.writeInt(maxKeyLength); + } +} diff --git a/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyStoreWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyStoreWriter.java new file mode 100644 index 000000000000..3c3fbd1c275a --- /dev/null +++ b/src/java/org/apache/cassandra/index/sai/disk/v9/keystore/KeyStoreWriter.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9.keystore; + +import java.io.Closeable; +import java.io.IOException; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; + +import io.micrometer.core.lang.NonNull; +import org.apache.cassandra.index.sai.disk.format.IndexComponent; +import org.apache.cassandra.index.sai.disk.io.IndexOutput; +import org.apache.cassandra.index.sai.disk.v1.MetadataWriter; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.utils.FastByteOperations; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.StringHelper; + +/** + * Writes a sequence of partition keys or clustering keys for use with {@link KeyLookup}. + *

+ * Partition keys are written unordered and clustering keys are written in ordered partitions determined by calls to + * {@link #startPartition()}. In either case keys can be of varying lengths. + *

+ * The {@link #blockShift} field is used to quickly determine the id of the current block + * based on a point id or to check if we are exactly at the beginning of the block. + *

+ * Keys are organized in blocks of (2 ^ {@link #blockShift}) keys. + *

+ * The blocks should not be too small because they allow prefix compression of the keys except the first key in a block. + *

+ * The blocks should not be too large because we can't just randomly jump to the key inside the block, but we have to + * iterate through all the keys from the start of the block. + * + * @see KeyLookup + */ +@NotThreadSafe +public class KeyStoreWriter implements Closeable +{ + private final int blockShift; + private final int blockMask; + private final boolean clustering; + private final IndexOutput keysOutput; + private final NumericValuesWriter offsetsWriter; + private final String componentName; + private final MetadataWriter metadataWriter; + private final long bytesStartFP; + private BytesRefBuilder prevKey = new BytesRefBuilder(); + private BytesRefBuilder tempKey = new BytesRefBuilder(); + private boolean inPartition = false; + private int maxKeyLength = -1; + private long pointId = 0; + + /** + * Creates a new writer. + *

+ * It does not own the components, so you must close the components by yourself + * after you're done with the writer. + * + * @param keysDataComponent component builder for the prefix-compressed keys data + * @param metadataWriter the {@link MetadataWriter} for storing the {@link KeyLookupMeta} + * @param keysBlockOffsets where to write the offsets of each block of keys + * @param blockShift the block shift that is used to determine the block size + * @param clustering determines whether the keys will be written as ordered partitions + */ + public KeyStoreWriter(@NonNull IndexComponent.ForWrite keysDataComponent, + @NonNull MetadataWriter metadataWriter, + @NonNull NumericValuesWriter keysBlockOffsets, + int blockShift, + boolean clustering) throws IOException + { + this.componentName = keysDataComponent.fileNamePart(); + this.metadataWriter = metadataWriter; + this.blockShift = blockShift; + this.blockMask = (1 << this.blockShift) - 1; + this.clustering = clustering; + this.keysOutput = keysDataComponent.openOutput(); + SAICodecUtils.writeHeader(this.keysOutput); + this.keysOutput.writeVInt(blockShift); + this.keysOutput.writeByte((byte) (clustering ? 1 : 0)); + this.bytesStartFP = keysOutput.getFilePointer(); + this.offsetsWriter = keysBlockOffsets; + } + + /** + * Identifies new partition for clustering keys, so clustering keys are written + * in order within a partition. + * It is not used for partition keys. + */ + public void startPartition() + { + assert clustering : "Cannot start a partition on a non-clustering key store"; + + inPartition = false; + } + + /** + * Appends a key at the end of the sequence. + * + * @throws IOException if write to disk fails + * @throws IllegalArgumentException if the key is not greater than the previous added key + */ + public void add(final @Nonnull ByteComparable key) throws IOException + { + tempKey.clear(); + copyBytes(key, tempKey); + + BytesRef keyRef = tempKey.get(); + + if (clustering && inPartition && compareKeys(keyRef, prevKey.get()) <= 0) + throw new IllegalArgumentException("Clustering keys must be in ascending lexicographical order"); + + inPartition = true; + + writeKey(keyRef); + + swapTempWithPrevious(); + maxKeyLength = Math.max(maxKeyLength, keyRef.length); + pointId++; + } + + private void writeKey(BytesRef key) throws IOException + { + if ((pointId & blockMask) == 0) + { + offsetsWriter.add(keysOutput.getFilePointer() - bytesStartFP); + + keysOutput.writeVInt(key.length); + keysOutput.writeBytes(key.bytes, key.offset, key.length); + } + else + { + int prefixLength = 0; + int suffixLength = 0; + + // If the key is the same as the previous key, then we use prefix and suffix lengths of 0. + // This means that we store a byte of 0 and don't write any data for the key. + if (compareKeys(prevKey.get(), key) != 0) + { + prefixLength = StringHelper.bytesDifference(prevKey.get(), key); + suffixLength = key.length - prefixLength; + } + // The prefix and suffix lengths are written as a byte followed by up to 2 vints. An attempt is + // made to compress the lengths into the byte (if prefix length < 15 and/or suffix length < 15). + // If either length exceeds the compressed byte maximum, it is written as a vint following the byte. + keysOutput.writeByte((byte) (Math.min(prefixLength, 15) | (Math.min(15, suffixLength) << 4))); + + if (prefixLength + suffixLength > 0) + { + if (prefixLength >= 15) + keysOutput.writeVInt(prefixLength - 15); + if (suffixLength >= 15) + keysOutput.writeVInt(suffixLength - 15); + + keysOutput.writeBytes(key.bytes, key.offset + prefixLength, key.length - prefixLength); + } + } + } + + /** + * Flushes any in-memory buffers to the output streams. + * Does not close the output streams. + * No more writes are allowed. + */ + @Override + public void close() throws IOException + { + try (IndexOutput output = metadataWriter.builder(componentName)) + { + SAICodecUtils.writeFooter(keysOutput); + KeyLookupMeta.write(output, pointId, maxKeyLength); + } + finally + { + FileUtils.close(offsetsWriter, keysOutput); + } + } + + private int compareKeys(BytesRef left, BytesRef right) + { + return FastByteOperations.compareUnsigned(left.bytes, left.offset, left.offset + left.length, + right.bytes, right.offset, right.offset + right.length); + } + + /** + * Copies bytes from source to dest. + */ + private void copyBytes(ByteComparable source, BytesRefBuilder dest) + { + ByteSource byteSource = source.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION); + int val; + while ((val = byteSource.next()) != ByteSource.END_OF_STREAM) + dest.append((byte) val); + } + + /** + * Swaps this.temp with this.previous. + * It is faster to swap the pointers instead of copying the data. + */ + private void swapTempWithPrevious() + { + BytesRefBuilder temp = this.tempKey; + this.tempKey = this.prevKey; + this.prevKey = temp; + } +} diff --git a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java index b0b646ab6487..2c0ab160b109 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java +++ b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java @@ -850,12 +850,12 @@ private ClusteringIndexFilter makeFilter(List keys) PrimaryKey firstKey = keys.get(0); assert !indexFeatureSet.isRowAware() || - cfs.metadata().comparator.size() == 0 && !firstKey.hasClustering() || - cfs.metadata().comparator.size() > 0 && (firstKey.hasClustering() || cfs.metadata().hasStaticColumns()) : + !cfs.metadata().hasClustering() && !firstKey.hasClustering() || + cfs.metadata().hasClustering() && (firstKey.hasClustering() || cfs.metadata().hasStaticColumns()) : "PrimaryKey " + firstKey + " clustering does not match table. There should be a clustering of size " + cfs.metadata().comparator.size(); ClusteringIndexFilter clusteringIndexFilter = command.clusteringIndexFilter(firstKey.partitionKey()); - if (cfs.metadata().comparator.size() == 0 || !firstKey.hasClustering()) + if (!cfs.metadata().hasClustering() || !firstKey.hasClustering()) { return clusteringIndexFilter; } diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java index 5aaa81dc4faa..86fd08d2d113 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKey.java @@ -26,8 +26,8 @@ import org.apache.cassandra.dht.Token; import org.apache.cassandra.index.sai.disk.format.IndexFeatureSet; import org.apache.cassandra.index.sai.disk.v1.PartitionAwarePrimaryKeyFactory; -import org.apache.cassandra.index.sai.disk.v2.RowAwarePrimaryKeyFactory; import org.apache.cassandra.index.sai.disk.v2.TokenOnlyPrimaryKey; +import org.apache.cassandra.index.sai.disk.v2.V2RowAwarePrimaryKeyFactory; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; @@ -40,7 +40,7 @@ * For the V2 on-disk format the {@link DecoratedKey} and {@link Clustering} are supported. * */ -public interface PrimaryKey extends Comparable, Accountable +public interface PrimaryKey extends Comparable, Accountable, ByteComparable { /** * A factory for creating {@link PrimaryKey} instances @@ -105,14 +105,14 @@ default PrimaryKey createPartitionKeyOnly(DecoratedKey partitionKey) * returned is based on the capabilities of the {@link IndexFeatureSet}. * * @param clusteringComparator the {@link ClusteringComparator} used by the - * {@link RowAwarePrimaryKeyFactory} for clustering comparisons + * {@link V2RowAwarePrimaryKeyFactory} for clustering comparisons * @param indexFeatureSet the {@link IndexFeatureSet} used to decide the type of * factory to use * @return a {@link Factory} for {@link PrimaryKey} creation */ static Factory factory(ClusteringComparator clusteringComparator, IndexFeatureSet indexFeatureSet) { - return indexFeatureSet.isRowAware() ? new RowAwarePrimaryKeyFactory(clusteringComparator) + return indexFeatureSet.isRowAware() ? new V2RowAwarePrimaryKeyFactory(clusteringComparator) : new PartitionAwarePrimaryKeyFactory(); } @@ -176,6 +176,7 @@ default boolean hasClustering() * @param version the {@link ByteComparable.Version} to use for the implementation * @return the {@code ByteSource} byte comparable. */ + @Override ByteSource asComparableBytes(ByteComparable.Version version); /** diff --git a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyListUtil.java b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyListUtil.java index 21032ebfd7a0..883a69fe8c0e 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyListUtil.java +++ b/src/java/org/apache/cassandra/index/sai/utils/PrimaryKeyListUtil.java @@ -37,7 +37,7 @@ private static int findBoundaryIndex(List keys, PrimaryKey key, bool int index = Collections.binarySearch(keys, key); if (index < 0) - return -index - 1; + return ~index; // When findMax is true, we are finding an exclusive upper bound, but binary search is inclusive, so we // increment by 1 to get the exclusive upper bound. diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java index d5e188549dfd..4b9f51bd7262 100644 --- a/src/java/org/apache/cassandra/schema/TableMetadata.java +++ b/src/java/org/apache/cassandra/schema/TableMetadata.java @@ -448,6 +448,14 @@ public ColumnMetadata getDroppedColumn(ByteBuffer name, boolean isStatic) return dropped.column; } + /** + * Determines whether the table has clustering. + */ + public boolean hasClustering() + { + return comparator.size() > 0; + } + public boolean hasStaticColumns() { return !staticColumns().isEmpty(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/sai/features/FeaturesVersionSupportGATest.java b/test/distributed/org/apache/cassandra/distributed/test/sai/features/FeaturesVersionSupportGATest.java new file mode 100644 index 000000000000..9dea038c6adf --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/sai/features/FeaturesVersionSupportGATest.java @@ -0,0 +1,35 @@ +/* + * Copyright IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.sai.features; + +import java.io.IOException; + +import org.junit.BeforeClass; + +import org.apache.cassandra.index.sai.disk.format.Version; + +/** + * {@link FeaturesVersionSupportTester} for {@link Version#GA}. + */ +public class FeaturesVersionSupportGATest extends FeaturesVersionSupportTester +{ + @BeforeClass + public static void setup() throws IOException + { + initCluster(Version.GA); + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/index/sai/QuerySelectivityBench.java b/test/microbench/org/apache/cassandra/test/microbench/index/sai/QuerySelectivityBench.java index 96d5f6076269..807693cb336a 100644 --- a/test/microbench/org/apache/cassandra/test/microbench/index/sai/QuerySelectivityBench.java +++ b/test/microbench/org/apache/cassandra/test/microbench/index/sai/QuerySelectivityBench.java @@ -59,7 +59,7 @@ public class QuerySelectivityBench extends CQLTester static final Random RANDOM = new Random(); /** The SAI index format version, {@code none} for no index. */ - @Param({ "aa", "ec", "none" }) + @Param({ "aa", "ec", "ga", "none" }) public String version; /** The number of partitions to be inserted. */ diff --git a/test/microbench/org/apache/cassandra/test/microbench/index/sai/v1/AbstractOnDiskBench.java b/test/microbench/org/apache/cassandra/test/microbench/index/sai/v1/AbstractOnDiskBench.java index 76bbedc8dffa..bd41c4baeb88 100644 --- a/test/microbench/org/apache/cassandra/test/microbench/index/sai/v1/AbstractOnDiskBench.java +++ b/test/microbench/org/apache/cassandra/test/microbench/index/sai/v1/AbstractOnDiskBench.java @@ -40,7 +40,7 @@ import org.apache.cassandra.index.sai.disk.v1.LongArray; import org.apache.cassandra.index.sai.disk.v1.MetadataSource; import org.apache.cassandra.index.sai.disk.io.IndexInput; -import org.apache.cassandra.index.sai.disk.v1.SSTableComponentsWriter; +import org.apache.cassandra.index.sai.disk.v1.V1SSTableComponentsWriter; import org.apache.cassandra.index.sai.disk.v1.bitpack.BlockPackedReader; import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader; @@ -183,7 +183,7 @@ protected final PostingsReader openPostingsReader() throws IOException private void writeSSTableComponents(int rows) throws IOException { - SSTableComponentsWriter writer = new SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); + V1SSTableComponentsWriter writer = new V1SSTableComponentsWriter(indexDescriptor.newPerSSTableComponentsForWrite()); for (int i = 0; i < rows; i++) writer.recordCurrentTokenOffset(toToken(i), toOffset(i)); diff --git a/test/microbench/org/apache/cassandra/test/microbench/index/sai/v9/keystore/KeyLookupBench.java b/test/microbench/org/apache/cassandra/test/microbench/index/sai/v9/keystore/KeyLookupBench.java new file mode 100644 index 000000000000..62abc68c9455 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/index/sai/v9/keystore/KeyLookupBench.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.test.microbench.index.sai.v9.keystore; + +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Stopwatch; +import org.junit.Assert; + +import org.apache.cassandra.Util; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.index.sai.SAIUtil; +import org.apache.cassandra.index.sai.disk.PerSSTableWriter; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v9.V9RowAwarePrimaryKeyFactory; +import org.apache.cassandra.index.sai.disk.v9.V9SSTableComponentsWriter; +import org.apache.cassandra.index.sai.disk.v9.WidePrimaryKeyMap; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.TableMetadata; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import static org.apache.cassandra.Util.makeKey; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@BenchmarkMode({ Mode.Throughput }) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 3, time = 5) +@Fork(value = 1, jvmArgsAppend = { +"-Xmx512M", +"--add-exports", "java.base/jdk.internal.ref=ALL-UNNAMED", +"--add-opens", "java.base/jdk.internal.ref=ALL-UNNAMED" +}) +@Threads(1) +@State(Scope.Benchmark) +public class KeyLookupBench +{ + private static final int rows = 1_000_000; + private static final CQLTester.Randomization random = new CQLTester.Randomization(); + + static + { + DatabaseDescriptor.toolInitialization(); + // Partitioner is not set in client mode. + if (DatabaseDescriptor.getPartitioner() == null) + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + } + + protected TableMetadata metadata; + protected IndexDescriptor indexDescriptor; + private PrimaryKeyMap primaryKeyMap; + private PrimaryKey primaryKey; + + @Param({ "3", "4", "5" }) + public int partitionBlockShift; + @Param({ "3", "4", "5" }) + public int clusteringBlockShift; + @Param({ "10", "100", "1000", "10000" }) + public int partitionSize; + @Param({ "true", "false" }) + public boolean randomClustering; + + @Setup(Level.Trial) + public void trialSetup() throws Exception + { + Version version = Version.LATEST.onOrAfter(Version.GA) ? Version.LATEST : Version.GA; + SAIUtil.setCurrentVersion(version); + String keyspaceName = "ks"; + String tableName = this.getClass().getSimpleName(); + metadata = TableMetadata + .builder(keyspaceName, tableName) + .partitioner(Murmur3Partitioner.instance) + .addPartitionKeyColumn("pk1", LongType.instance) + .addPartitionKeyColumn("pk2", LongType.instance) + .addClusteringColumn("ck1", UTF8Type.instance) + .addClusteringColumn("ck2", UTF8Type.instance) + .build(); + + Descriptor descriptor = new Descriptor(new File(Files.createTempDirectory("jmh").toFile()), + metadata.keyspace, + metadata.name, + Util.newUUIDGen().get()); + + indexDescriptor = IndexDescriptor.empty(descriptor, metadata.comparator); + + V9SSTableComponentsWriter.setPartitionBlockShift(partitionBlockShift); + V9SSTableComponentsWriter.setClusteringBlockShift(clusteringBlockShift); + Assert.assertTrue("Version must be at least GA", Version.current(keyspaceName).onOrAfter(Version.GA)); + PerSSTableWriter writer = Version.current(keyspaceName).onDiskFormat().newPerSSTableWriter(indexDescriptor); + V9RowAwarePrimaryKeyFactory factory = new V9RowAwarePrimaryKeyFactory(metadata.comparator); + + PrimaryKey[] primaryKeys = generatePrimaryKeys(factory); + Arrays.sort(primaryKeys); + + DecoratedKey lastKey = null; + for (PrimaryKey primaryKey : primaryKeys) + { + if (lastKey == null || lastKey.compareTo(primaryKey.partitionKey()) != 0) + { + lastKey = primaryKey.partitionKey(); + writer.startPartition(lastKey, -1); + } + writer.nextRow(primaryKey); + } + + writer.complete(Stopwatch.createStarted()); + + SSTableReader sstableReader = mock(SSTableReader.class); + when(sstableReader.metadata()).thenReturn(metadata); + + PrimaryKeyMap.Factory mapFactory = new WidePrimaryKeyMap.Factory(indexDescriptor.perSSTableComponents(), factory, sstableReader); + + primaryKeyMap = mapFactory.newPerSSTablePrimaryKeyMap(); + + primaryKey = primaryKeys[rows / 2]; + } + + @Benchmark + public long advanceToKey() + { + return primaryKeyMap.exactRowIdOrInvertedCeiling(primaryKey); + } + + private PrimaryKey[] generatePrimaryKeys(V9RowAwarePrimaryKeyFactory factory) + { + PrimaryKey[] primaryKeys = new PrimaryKey[rows]; + int partition = 0; + int partitionRowCounter = 0; + ClusteringStringGenerator clusteringStringGenerator = new ClusteringStringGenerator(metadata); + for (int index = 0; index < rows; index++) + { + primaryKeys[index] = factory.create(makeKey(metadata, (long) partition, (long) partition), + clusteringStringGenerator.nextClustering()); + partitionRowCounter++; + if (partitionRowCounter == partitionSize) + { + partition++; + partitionRowCounter = 0; + clusteringStringGenerator = new ClusteringStringGenerator(metadata); + } + } + return primaryKeys; + } + + /** + * Generates a sorted list of unique clustering strings during initialization, + * then returns them in lexicographical order on each call to nextClustering(). + */ + private class ClusteringStringGenerator + { + private final List sortedStrings; + private final TableMetadata table; + private int currentIndex; + + ClusteringStringGenerator(TableMetadata table) + { + // Use TreeSet to maintain sorted order and ensure uniqueness + TreeSet uniqueStrings = new TreeSet<>(); + this.table = table; + this.currentIndex = 0; + + sortedStrings = generateUniqueSortedStrings(uniqueStrings); + } + + private List generateUniqueSortedStrings(TreeSet uniqueStrings) + { + while (uniqueStrings.size() < partitionSize) + { + String candidate = makeClusteringString(); + uniqueStrings.add(candidate); + } + return new ArrayList<>(uniqueStrings); + } + + private String makeClusteringString() + { + if (randomClustering) + return random.nextTextString(10, 100); + else + return String.format("%08d", random.nextIntBetween(0, partitionSize)); + } + + Clustering nextClustering() + { + if (!table.hasClustering()) + return Clustering.EMPTY; + + ByteBuffer[] values = new ByteBuffer[table.comparator.size()]; + String nextString = sortedStrings.get(currentIndex++); + for (int index = 0; index < table.comparator.size(); index++) + values[index] = table.comparator.subtype(index).fromString(nextString); + return Clustering.make(values); + } + } +} diff --git a/test/unit/org/apache/cassandra/Util.java b/test/unit/org/apache/cassandra/Util.java index 25a95c07200c..c7e4c5c4bf08 100644 --- a/test/unit/org/apache/cassandra/Util.java +++ b/test/unit/org/apache/cassandra/Util.java @@ -413,7 +413,7 @@ public static AbstractReadCommandBuilder.PartitionRangeBuilder cmd(ColumnFamilyS return new AbstractReadCommandBuilder.PartitionRangeBuilder(cfs); } - static DecoratedKey makeKey(TableMetadata metadata, Object... partitionKey) + public static DecoratedKey makeKey(TableMetadata metadata, Object... partitionKey) { if (partitionKey.length == 1 && partitionKey[0] instanceof DecoratedKey) return (DecoratedKey)partitionKey[0]; diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java index 05c48278300b..df4d6480e8cf 100644 --- a/test/unit/org/apache/cassandra/cql3/CQLTester.java +++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java @@ -3434,7 +3434,7 @@ public static class Randomization private long seed; private Random random; - Randomization() + public Randomization() { if (random == null) { diff --git a/test/unit/org/apache/cassandra/index/sai/SAITester.java b/test/unit/org/apache/cassandra/index/sai/SAITester.java index 5fa930859902..827a9d8c380e 100644 --- a/test/unit/org/apache/cassandra/index/sai/SAITester.java +++ b/test/unit/org/apache/cassandra/index/sai/SAITester.java @@ -38,6 +38,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; + import javax.annotation.Nullable; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; @@ -165,6 +166,12 @@ protected static Injections.Counter.CounterBuilder addConditions(Injections.Coun public static final PrimaryKey.Factory TEST_FACTORY = Version.current(KEYSPACE).onDiskFormat().newPrimaryKeyFactory(EMPTY_COMPARATOR); + // Row-aware TEST_FACTORY that runs up to FA format version and does not use clustered row aware + public static final PrimaryKey.Factory ROW_AWARE_TEST_FACTORY = (Version.current(KEYSPACE).onOrAfter(Version.GA) + ? Version.FA + : Version.current(KEYSPACE)) + .onDiskFormat().newPrimaryKeyFactory(EMPTY_COMPARATOR); + static { Version.ALL.size(); @@ -740,7 +747,8 @@ protected void verifyIndexFiles(IndexContext numericIndexContext, { Set indexFiles = indexFiles(); - for (IndexComponentType indexComponentType : Version.current(KEYSPACE).onDiskFormat().perSSTableComponentTypes()) + for (IndexComponentType indexComponentType : Version.current(KEYSPACE).onDiskFormat() + .perSSTableComponentTypes(currentTableMetadata().hasClustering())) { Set tableFiles = componentFiles(indexFiles, new Component(SSTableFormat.Components.Types.CUSTOM, Version.current(KEYSPACE).fileNameFormatter().format(indexComponentType, (String)null, 0))); assertEquals(tableFiles.toString(), perSSTableFiles, tableFiles.size()); diff --git a/test/unit/org/apache/cassandra/index/sai/cql/NativeIndexDDLTest.java b/test/unit/org/apache/cassandra/index/sai/cql/NativeIndexDDLTest.java index a05ba9e0dbfc..c9c248d8cf3d 100644 --- a/test/unit/org/apache/cassandra/index/sai/cql/NativeIndexDDLTest.java +++ b/test/unit/org/apache/cassandra/index/sai/cql/NativeIndexDDLTest.java @@ -34,14 +34,16 @@ import java.util.stream.Collectors; import java.util.stream.LongStream; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.AppenderBase; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.exceptions.InvalidConfigurationInQueryException; import com.datastax.driver.core.exceptions.InvalidQueryException; @@ -89,11 +91,9 @@ import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Throwables; import org.mockito.Mockito; -import org.slf4j.LoggerFactory; import static java.util.Collections.singletonList; -import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_ENCRYPTION; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -1225,7 +1225,8 @@ private void verifyRebuildCorruptedFiles(String numericIndexName, IndexContext numericIndexContext = getIndexContext(numericIndexName); IndexContext stringIndexContext = getIndexContext(stringIndexName); - for (IndexComponentType component : Version.current(KEYSPACE).onDiskFormat().perSSTableComponentTypes()) + for (IndexComponentType component : Version.current(KEYSPACE).onDiskFormat() + .perSSTableComponentTypes(currentTableMetadata().hasClustering())) verifyRebuildIndexComponent(numericIndexContext, stringIndexContext, component, null, corruptionType, true, true, rebuild); for (IndexComponentType component : Version.current(KEYSPACE).onDiskFormat().perIndexComponentTypes(numericIndexContext)) @@ -1244,22 +1245,27 @@ private void verifyRebuildIndexComponent(IndexContext numericIndexContext, boolean failedNumericIndex, boolean rebuild) throws Throwable { - boolean encrypted = TEST_ENCRYPTION.getBoolean(); - - // The completion markers are valid if they exist on the file system so we only need to test + // The completion markers are valid if they exist on the file system, so we only need to test // their removal. If we are testing with encryption then we don't want to test any components // that are encryptable unless they have been removed because encrypted components aren't // checksum validated. + if (((component == IndexComponentType.GROUP_COMPLETION_MARKER) || + (component == IndexComponentType.COLUMN_COMPLETION_MARKER)) && + (corruptionType != CorruptionType.REMOVED)) + return; + // Skip per SSTable components for v2 primary key maps if (component == IndexComponentType.PRIMARY_KEY_TRIE || component == IndexComponentType.PRIMARY_KEY_BLOCKS || component == IndexComponentType.PRIMARY_KEY_BLOCK_OFFSETS) return; - if (((component == IndexComponentType.GROUP_COMPLETION_MARKER) || - (component == IndexComponentType.COLUMN_COMPLETION_MARKER)) && - (corruptionType != CorruptionType.REMOVED)) + // Skip per SSTables components for v9 primary key maps + if (component == IndexComponentType.ROW_TO_TOKEN || component == IndexComponentType.ROW_TO_PARTITION || + component == IndexComponentType.PARTITION_TO_SIZE || component == IndexComponentType.PARTITION_KEY_BLOCKS || + component == IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS || component == IndexComponentType.CLUSTERING_KEY_BLOCKS || + component == IndexComponentType.CLUSTERING_KEY_BLOCK_OFFSETS) return; - logger.info("CORRUPTING: " + component + ", corruption type = " + corruptionType); + logger.info("CORRUPTING: {}, corruption type = {}", component, corruptionType); int rowCount = 2; diff --git a/test/unit/org/apache/cassandra/index/sai/cql/TokenCollisionTest.java b/test/unit/org/apache/cassandra/index/sai/cql/TokenCollisionTest.java index 6debec7c021e..68ac4c5e04ef 100644 --- a/test/unit/org/apache/cassandra/index/sai/cql/TokenCollisionTest.java +++ b/test/unit/org/apache/cassandra/index/sai/cql/TokenCollisionTest.java @@ -18,6 +18,7 @@ package org.apache.cassandra.index.sai.cql; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.List; import org.junit.Before; @@ -61,4 +62,90 @@ public void testSkippingWhenTokensCollide() // we should match all the rows assertEquals(numRows, rows.size()); } + @Test + public void skinnyPartitionTest() + { + doSkinnyPartitionTest(10, 0); + } + + @Test + public void skinnyPartitionLastRowTest() + { + doSkinnyPartitionTest(49, 9); + } + + private void doSkinnyPartitionTest(int v1Match, int v2Match) + { + createTable("CREATE TABLE %s (pk blob, v1 int, v2 int, PRIMARY KEY (pk))"); + createIndex("CREATE CUSTOM INDEX ON %s(v1) USING 'StorageAttachedIndex'"); + createIndex("CREATE CUSTOM INDEX ON %s(v2) USING 'StorageAttachedIndex'"); + + ByteBuffer prefix = ByteBufferUtil.bytes("key"); + int numRows = 100; + int v1Count = 0; + int v2Count = 0; + List matchingPks = new ArrayList<>(); + for (int pkCount = 0; pkCount < numRows; pkCount++) + { + ByteBuffer pk = Util.generateMurmurCollision(prefix, (byte) (pkCount / 64), (byte) (pkCount % 64)); + if (v1Count == v1Match && v2Count == v2Match) + matchingPks.add(row(pk, v1Count, v2Count)); + execute("INSERT INTO %s (pk, v1, v2) VALUES (?, ?, ?)", pk, v1Count++, v2Count++); + if (v1Count == 50) + v1Count = 0; + if (v2Count == 10) + v2Count = 0; + } + assertEquals(2, matchingPks.size()); + flush(); + + assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE v1=" + v1Match + " AND v2=" + v2Match), matchingPks.get(0), matchingPks.get(1)); + } + + @Test + public void widePartitionTest() + { + doWidePartitionTest(100, 10, 0); + } + + @Test + public void widePartitionLastRowTest() + { + // Reduce the number of rows so the last row occurs at the first clustering value + doWidePartitionTest(97, 46, 6); + } + + private void doWidePartitionTest(int numRows, int v1Match, int v2Match) + { + createTable("CREATE TABLE %s (pk blob, ck int, v1 int, v2 int, PRIMARY KEY (pk, ck))"); + createIndex("CREATE CUSTOM INDEX ON %s(v1) USING 'StorageAttachedIndex'"); + createIndex("CREATE CUSTOM INDEX ON %s(v2) USING 'StorageAttachedIndex'"); + + ByteBuffer prefix = ByteBufferUtil.bytes("key"); + int pkCount = 0; + int ckCount = 0; + int v1Count = 0; + int v2Count = 0; + List matchingRows = new ArrayList<>(); + for (int i = 0; i < numRows; i++) + { + ByteBuffer pk = Util.generateMurmurCollision(prefix, (byte) (pkCount / 64), (byte) (pkCount % 64)); + if (v1Count == v1Match && v2Count == v2Match) + matchingRows.add(row(pk, ckCount, v1Count, v2Count)); + execute("INSERT INTO %s (pk, ck, v1, v2) VALUES (?, ?, ?, ?)", pk, ckCount++, v1Count++, v2Count++); + if (ckCount == 8) + { + ckCount = 0; + pkCount++; + } + if (v1Count == 50) + v1Count = 0; + if (v2Count == 10) + v2Count = 0; + } + assertEquals(2, matchingRows.size()); + flush(); + + assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE v1=" + v1Match + " AND v2=" + v2Match), matchingRows.get(0), matchingRows.get(1)); + } } diff --git a/test/unit/org/apache/cassandra/index/sai/cql/VectorCompactionTest.java b/test/unit/org/apache/cassandra/index/sai/cql/VectorCompactionTest.java index 5e41a52503c9..4b4f0be74dcf 100644 --- a/test/unit/org/apache/cassandra/index/sai/cql/VectorCompactionTest.java +++ b/test/unit/org/apache/cassandra/index/sai/cql/VectorCompactionTest.java @@ -412,7 +412,7 @@ else if (numRows >= MIN_PQ_ROWS) // With FA + fused PQ, PQ metadata is present and used by the graph, but there is no // standalone CompressedVectors instance to validate against. // TODO: further investigate what other checks are needed here - assertEquals("Expected fused PQ path only for FA+", Version.FA, version); + assertTrue("Expected fused PQ path only for FA+", version.onOrAfter(Version.FA)); assertNotNull("Expected PQ metadata for FA fused PQ", searcher.getPQ()); } } diff --git a/test/unit/org/apache/cassandra/index/sai/disk/NodeStartupTest.java b/test/unit/org/apache/cassandra/index/sai/disk/NodeStartupTest.java index 7739ce0f9c3a..e4b1c98a658d 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/NodeStartupTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/NodeStartupTest.java @@ -26,7 +26,7 @@ import java.util.stream.Stream; import com.google.common.collect.ObjectArrays; -import org.apache.cassandra.cql3.CQLTester; + import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -34,6 +34,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.index.SecondaryIndexManager; import org.apache.cassandra.index.sai.IndexContext; @@ -368,7 +369,9 @@ private void setState(IndexStateOnRestart state) case VALID: break; case ALL_EMPTY: - Version.current(KEYSPACE).onDiskFormat().perSSTableComponentTypes().forEach(this::remove); + Version.current(KEYSPACE).onDiskFormat() + .perSSTableComponentTypes(currentTableMetadata().hasClustering()) + .forEach(this::remove); Version.current(KEYSPACE).onDiskFormat().perIndexComponentTypes(indexContext).forEach(c -> remove(c, indexContext)); break; case PER_SSTABLE_INCOMPLETE: diff --git a/test/unit/org/apache/cassandra/index/sai/disk/RowAwarePrimaryKeyTest.java b/test/unit/org/apache/cassandra/index/sai/disk/RowAwarePrimaryKeyTest.java index 6b51f6289eae..a8a799f1a018 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/RowAwarePrimaryKeyTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/RowAwarePrimaryKeyTest.java @@ -38,7 +38,7 @@ import org.apache.cassandra.index.sai.SAITester; import org.apache.cassandra.index.sai.SAIUtil; import org.apache.cassandra.index.sai.disk.format.Version; -import org.apache.cassandra.index.sai.disk.v2.RowAwarePrimaryKeyFactory; +import org.apache.cassandra.index.sai.disk.v2.V2RowAwarePrimaryKeyFactory; import org.apache.cassandra.index.sai.utils.PrimaryKey; import static org.assertj.core.api.Assertions.assertThat; @@ -73,7 +73,7 @@ public void testHashCodeForDeferredPrimaryKey() PrimaryKey.Factory factory = version.onDiskFormat().newPrimaryKeyFactory(EMPTY_COMPARATOR); // Test relies on this implementation detail - assertTrue(factory instanceof RowAwarePrimaryKeyFactory); + assertTrue(factory instanceof V2RowAwarePrimaryKeyFactory); // Set up the primary key Token token = new Murmur3Partitioner.LongToken(1); @@ -100,7 +100,7 @@ public void testHashCodeForLoadedPrimaryKey() PrimaryKey.Factory factory = version.onDiskFormat().newPrimaryKeyFactory(EMPTY_COMPARATOR); // Test relies on this implementation detail - assertTrue(factory instanceof RowAwarePrimaryKeyFactory); + assertTrue(factory instanceof V2RowAwarePrimaryKeyFactory); // Set up the primary key Token token1 = new Murmur3Partitioner.LongToken(1); @@ -122,7 +122,7 @@ public void testHashCodeForDeferedPrimaryKeyWithClusteringColumns() PrimaryKey.Factory factory = version.onDiskFormat().newPrimaryKeyFactory(comparator); // Test relies on this implementation detail - assertTrue(factory instanceof RowAwarePrimaryKeyFactory); + assertTrue(factory instanceof V2RowAwarePrimaryKeyFactory); // Set up the primary key Token token1 = new Murmur3Partitioner.LongToken(1); @@ -143,7 +143,7 @@ public void testComparisonBetweenTokenOnlyAndFullKey() PrimaryKey.Factory factory = version.onDiskFormat().newPrimaryKeyFactory(EMPTY_COMPARATOR); // Test relies on this implementation detail - assertTrue(factory instanceof RowAwarePrimaryKeyFactory); + assertTrue(factory instanceof V2RowAwarePrimaryKeyFactory); // Create keys with the same token Token token = new Murmur3Partitioner.LongToken(100); @@ -153,7 +153,7 @@ public void testComparisonBetweenTokenOnlyAndFullKey() PrimaryKey fullKey = factory.create(decoratedKey, Clustering.EMPTY); // When tokens are equal, token-only key should compare equal to full key. - // This is the critical behavior tested in RowAwarePrimaryKeyFactory.compareTo. + // This is the critical behavior tested in V2RowAwarePrimaryKeyFactory.compareTo. assertEquals(0, tokenOnlyKey.compareTo(fullKey)); assertEquals(0, fullKey.compareTo(tokenOnlyKey)); @@ -168,7 +168,7 @@ public void testComparisonBetweenTokenOnlyKeysWithDifferentTokens() PrimaryKey.Factory factory = version.onDiskFormat().newPrimaryKeyFactory(EMPTY_COMPARATOR); // Test relies on this implementation detail - assertTrue(factory instanceof RowAwarePrimaryKeyFactory); + assertTrue(factory instanceof V2RowAwarePrimaryKeyFactory); // Create token-only keys with different tokens Token token1 = new Murmur3Partitioner.LongToken(50); @@ -192,7 +192,7 @@ public void testComparisonWithClusteringColumns() PrimaryKey.Factory factory = version.onDiskFormat().newPrimaryKeyFactory(comparator); // Test relies on this implementation detail - assertTrue(factory instanceof RowAwarePrimaryKeyFactory); + assertTrue(factory instanceof V2RowAwarePrimaryKeyFactory); // Create keys with the same token but different clustering Token token = new Murmur3Partitioner.LongToken(100); @@ -213,7 +213,7 @@ public void testTokenOnlyKeyHashCode() PrimaryKey.Factory factory = version.onDiskFormat().newPrimaryKeyFactory(EMPTY_COMPARATOR); // Test relies on this implementation detail - assertTrue(factory instanceof RowAwarePrimaryKeyFactory); + assertTrue(factory instanceof V2RowAwarePrimaryKeyFactory); // Create token-only keys with the same token Token token = new Murmur3Partitioner.LongToken(42); diff --git a/test/unit/org/apache/cassandra/index/sai/disk/RowAwareSkinnyPrimaryKeyMapTest.java b/test/unit/org/apache/cassandra/index/sai/disk/RowAwareSkinnyPrimaryKeyMapTest.java index ad108ddd8623..ea7742725ee0 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/RowAwareSkinnyPrimaryKeyMapTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/RowAwareSkinnyPrimaryKeyMapTest.java @@ -91,13 +91,13 @@ public void tearDown() throws Exception public void testExactRowIdOrInvertedCeiling() { assertThat(map.exactRowIdOrInvertedCeiling(beforeFirst(map))).as("before first expects the inverted first") - .isEqualTo(invert(0)); + .isEqualTo(~0); assertThat(map.exactRowIdOrInvertedCeiling(exactFirstRow(map))).as("exact first") .isEqualTo(0); assertThat(map.exactRowIdOrInvertedCeiling(betweenFirstAndSecond(map))).as("between first and second expects the inverted second") - .isEqualTo(invert(1)); + .isEqualTo(~1); assertThat(map.exactRowIdOrInvertedCeiling(exactLastRow(map))).as("exact last") .isEqualTo(map.count() - 1); @@ -179,9 +179,4 @@ private PrimaryKey afterLastToken(PrimaryKeyMap map) long lastToken = lastPk.token().getLongValue(); return pkFactory.createTokenOnly(partitioner.getTokenFactory().fromLongValue(lastToken + 1)); } - - private long invert(long rowId) - { - return -rowId - 1; - } } diff --git a/test/unit/org/apache/cassandra/index/sai/disk/RowAwareStaticClusteringPrimaryKeyMapTest.java b/test/unit/org/apache/cassandra/index/sai/disk/RowAwareStaticClusteringPrimaryKeyMapTest.java index 82e9890835df..cb3f2fe0b343 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/RowAwareStaticClusteringPrimaryKeyMapTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/RowAwareStaticClusteringPrimaryKeyMapTest.java @@ -33,6 +33,7 @@ import org.apache.cassandra.index.sai.SAITester; import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.sstable.format.SSTableReader; @@ -122,7 +123,7 @@ public void tearDown() throws Exception public void testExactRowIdOrInvertedCeiling() { assertThat(map.exactRowIdOrInvertedCeiling(beforeFirst(map))).as("before first expects the inverted first") - .isEqualTo(invert(0)); + .isEqualTo(~0); assertThat(map.exactRowIdOrInvertedCeiling(exactFirstRow(map))).as("exact first row") .isEqualTo(0); @@ -133,7 +134,7 @@ public void testExactRowIdOrInvertedCeiling() // Test between static and first clustering row assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1, 0))).as("between static and ck=1 expects inverted ck=1") - .isEqualTo(invert(idPk1Static + 1)); + .isEqualTo(~(idPk1Static + 1)); // Test regular clustering rows assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1, 1))).as("exact pk=1, ck=1, which is next after the static row") @@ -147,7 +148,7 @@ public void testExactRowIdOrInvertedCeiling() // Test after last clustering in partition assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1, Integer.MAX_VALUE))).as("after pk=1 ck=3 expects inverted next partition first row or out of range if the last partition") - .isEqualTo(idPk1Static < map.count() ? invert(idPk1Static + 4) : Long.MIN_VALUE); + .isEqualTo(idPk1Static < map.count() ? ~(idPk1Static + 4) : Long.MIN_VALUE); assertThat(map.exactRowIdOrInvertedCeiling(buildStaticPk(2))).as("exact pk=2 static row"). isEqualTo(idPk2Static); @@ -158,6 +159,10 @@ public void testExactRowIdOrInvertedCeiling() assertThat(map.exactRowIdOrInvertedCeiling(exactLastRow(map))).as("exact last row") .isEqualTo(map.count() - 1); + if (Version.current(KEYSPACE).onOrAfter(Version.GA)) // See CNDB-18024 + assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1000, 11))).as("after last row in last partition expects out of range") + .isEqualTo(Long.MIN_VALUE); + assertThat(map.exactRowIdOrInvertedCeiling(afterLastToken(map))).as("after last expects out of range") .isEqualTo(Long.MIN_VALUE); } @@ -196,6 +201,10 @@ public void testCeiling() assertThat(map.ceiling(exactLastRow(map))).as("exact last row") .isEqualTo(map.count() - 1); + if (Version.current(KEYSPACE).onOrAfter(Version.GA)) // See CNDB-18024 + assertThat(map.ceiling(buildPk(1000, 11))).as("after last row in last partition expects out of range") + .isEqualTo(-1); + assertThat(map.ceiling(afterLastToken(map))).as("after last expects out of range") .isEqualTo(-1); } @@ -230,6 +239,10 @@ public void testFloor() assertThat(map.floor(exactLastRow(map))).as("exact last row") .isEqualTo(map.count() - 1); + if (Version.current(KEYSPACE).onOrAfter(Version.GA)) // See CNDB-18024 + assertThat(map.floor(buildPk(1000, 11))).as("after last row in last partition expects the last row") + .isEqualTo(map.count() - 1); + assertThat(map.floor(afterLastToken(map))).as("after last expects the last row") .isEqualTo(map.count() - 1); } @@ -272,9 +285,4 @@ private PrimaryKey afterLastToken(PrimaryKeyMap map) long lastToken = lastPk.token().getLongValue(); return pkFactory.createTokenOnly(partitioner.getTokenFactory().fromLongValue(lastToken + 1)); } - - private long invert(long rowId) - { - return -rowId - 1; - } } diff --git a/test/unit/org/apache/cassandra/index/sai/disk/RowAwareWidePrimaryKeyMapTest.java b/test/unit/org/apache/cassandra/index/sai/disk/RowAwareWidePrimaryKeyMapTest.java index 02d8597e0fe4..a279e7bf2528 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/RowAwareWidePrimaryKeyMapTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/RowAwareWidePrimaryKeyMapTest.java @@ -33,6 +33,7 @@ import org.apache.cassandra.index.sai.SAITester; import org.apache.cassandra.index.sai.disk.format.IndexComponents; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.utils.PrimaryKey; import org.apache.cassandra.io.sstable.format.SSTableReader; @@ -114,7 +115,7 @@ public void tearDown() throws Exception public void testExactRowIdOrInvertedCeiling() { assertThat(map.exactRowIdOrInvertedCeiling(beforeFirst(map))).as("before first expects the inverted first") - .isEqualTo(invert(0)); + .isEqualTo(~0); assertThat(map.exactRowIdOrInvertedCeiling(exactFirstRow(map))).as("exact first row") .isEqualTo(0); @@ -129,19 +130,23 @@ public void testExactRowIdOrInvertedCeiling() .isEqualTo(idPk1Ck1 + 2); assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1, 4))).as("between pk=1 ck=3 and ck=10 expects inverted ck=10") - .isEqualTo(invert(idPk1Ck10)); + .isEqualTo(~idPk1Ck10); assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1, 10))).as("exact pk=1, ck=10 expects next after pk=1, ck=3") .isEqualTo(idPk1Ck1 + 3); assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1, Integer.MAX_VALUE))).as("after last ck in pk=1 expects inverted next partition first row or out of range if the last partition") .isEqualTo(idPk1Ck10 < map.count() - ? invert(idPk1Ck10 + 1) + ? ~(idPk1Ck10 + 1) : Integer.MAX_VALUE); assertThat(map.exactRowIdOrInvertedCeiling(exactLastRow(map))).as("exact last row") .isEqualTo(map.count() - 1); + if (Version.current(KEYSPACE).onOrAfter(Version.GA)) // See CNDB-18024 + assertThat(map.exactRowIdOrInvertedCeiling(buildPk(1000, 11))).as("after last row in last partition expects out of range") + .isEqualTo(Long.MIN_VALUE); + assertThat(map.exactRowIdOrInvertedCeiling(afterLastToken(map))).as("after last expects out of range") .isEqualTo(Long.MIN_VALUE); } @@ -178,6 +183,10 @@ public void testCeiling() assertThat(map.ceiling(exactLastRow(map))).as("exact last row") .isEqualTo(map.count() - 1); + if (Version.current(KEYSPACE).onOrAfter(Version.GA)) // See CNDB-18024 + assertThat(map.ceiling(buildPk(1000, 11))).as("after last row in last partition expects out of range") + .isEqualTo(-1); + assertThat(map.ceiling(afterLastToken(map))).as("after last expects out of range") .isEqualTo(-1); } @@ -215,6 +224,10 @@ public void testFloor() assertThat(map.floor(buildPk(1000, 11))).as("after last row in last partition expects the last") .isEqualTo(map.count() - 1); + if (Version.current(KEYSPACE).onOrAfter(Version.GA)) // See CNDB-18024 + assertThat(map.floor(buildPk(1000, 11))).as("after last row in last partition expects the last row") + .isEqualTo(map.count() - 1); + assertThat(map.floor(afterLastToken(map))).as("after last token expects the last") .isEqualTo(map.count() - 1); } @@ -250,9 +263,4 @@ private PrimaryKey afterLastToken(PrimaryKeyMap map) long lastToken = lastPk.token().getLongValue(); return pkFactory.createTokenOnly(partitioner.getTokenFactory().fromLongValue(lastToken + 1)); } - - private long invert(long rowId) - { - return -rowId - 1; - } } diff --git a/test/unit/org/apache/cassandra/index/sai/disk/format/IndexDescriptorTest.java b/test/unit/org/apache/cassandra/index/sai/disk/format/IndexDescriptorTest.java index 97033ab8c8e1..9a387812762a 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/format/IndexDescriptorTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/format/IndexDescriptorTest.java @@ -34,6 +34,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.SAITester; import org.apache.cassandra.index.sai.SAIUtil; @@ -66,6 +67,7 @@ public class IndexDescriptorTest public static void initialise() { DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); } @Before @@ -343,7 +345,7 @@ static void createFakePerSSTableComponents(Descriptor descriptor, Version versio static void createFakePerSSTableComponents(Descriptor descriptor, Version version, int generation, int sizeInBytes) throws IOException { - for (IndexComponentType type : version.onDiskFormat().perSSTableComponentTypes()) + for (IndexComponentType type : version.onDiskFormat().perSSTableComponentTypes(false)) createileOnDisk(descriptor, version.fileNameFormatter().format(type, (String)null, generation), sizeInBytes); } diff --git a/test/unit/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsTest.java b/test/unit/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsTest.java index 53a7a0d3fa31..b80198b44ed5 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/v2/sortedterms/SortedTermsTest.java @@ -95,7 +95,7 @@ public void testFileValidation() throws Exception { ByteBuffer buffer = UTF8Type.instance.decompose(Integer.toString(x)); DecoratedKey partitionKey = Murmur3Partitioner.instance.decorateKey(buffer); - PrimaryKey primaryKey = SAITester.TEST_FACTORY.create(partitionKey, Clustering.EMPTY); + PrimaryKey primaryKey = SAITester.ROW_AWARE_TEST_FACTORY.create(partitionKey, Clustering.EMPTY); primaryKeys.add(primaryKey); } diff --git a/test/unit/org/apache/cassandra/index/sai/disk/v9/WideRowPrimaryKeyTest.java b/test/unit/org/apache/cassandra/index/sai/disk/v9/WideRowPrimaryKeyTest.java new file mode 100644 index 000000000000..8a333c8383a6 --- /dev/null +++ b/test/unit/org/apache/cassandra/index/sai/disk/v9/WideRowPrimaryKeyTest.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9; + +import java.util.Arrays; + +import com.google.common.base.Stopwatch; +import org.junit.Test; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.index.sai.disk.PrimaryKeyMap; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.utils.AbstractPrimaryKeyTest; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class WideRowPrimaryKeyTest extends AbstractPrimaryKeyTest +{ + @Test + public void testRandomly() throws Throwable + { + IndexDescriptor indexDescriptor = newClusteringIndexDescriptor(compositePartitionMultipleClusteringAsc); + IndexComponents.ForWrite components = indexDescriptor.newPerSSTableComponentsForWrite(); + + V9SSTableComponentsWriter writer = new V9SSTableComponentsWriter(components); + + V9RowAwarePrimaryKeyFactory factory = new V9RowAwarePrimaryKeyFactory(compositePartitionMultipleClusteringAsc.comparator); + + int rows = nextInt(1000, 10000); + PrimaryKey[] keys = new PrimaryKey[rows]; + int partition = 0; + int partitionSize = nextInt(5, 500); + int partitionCounter = 0; + for (int index = 0; index < rows; index++) + { + keys[index] = factory.create(makeKey(compositePartitionMultipleClusteringAsc, partition, partition), + makeClustering(compositePartitionMultipleClusteringAsc, + randomSimpleString(10, 100), + randomSimpleString(10, 100))); + partitionCounter++; + if (partitionCounter == partitionSize) + { + partition++; + partitionCounter = 0; + partitionSize = nextInt(5, 500); + } + } + + Arrays.sort(keys); + + DecoratedKey lastKey = null; + for (PrimaryKey primaryKey : keys) + { + if (lastKey == null || lastKey.compareTo(primaryKey.partitionKey()) < 0) + { + lastKey = primaryKey.partitionKey(); + writer.startPartition(lastKey, -1); + } + writer.nextRow(primaryKey); + } + + writer.complete(Stopwatch.createStarted()); + + SSTableReader sstableReader = mock(SSTableReader.class); + when(sstableReader.metadata()).thenReturn(compositePartitionMultipleClusteringAsc); + + try (PrimaryKeyMap.Factory mapFactory = new WidePrimaryKeyMap.Factory(components, factory, sstableReader); + PrimaryKeyMap primaryKeyMap = mapFactory.newPerSSTablePrimaryKeyMap()) + { + for (int key = 0; key < rows; key++) + { + PrimaryKey test = factory.create(keys[key].partitionKey(), + makeClustering(compositePartitionMultipleClusteringAsc, + randomSimpleString(10, 100), + randomSimpleString(10, 100))); + + long rowId = primaryKeyMap.ceiling(test); + + if (rowId >= 0) + { + PrimaryKey found = keys[(int) rowId]; + + assertTrue(found.compareTo(test) >= 0); + + if (rowId > 0) + assertTrue(keys[(int) rowId - 1].compareTo(test) < 0); + } + else + { + assertTrue(test.compareTo(keys[keys.length - 1]) > 0); + } + } + } + } +} diff --git a/test/unit/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookupTest.java b/test/unit/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookupTest.java new file mode 100644 index 000000000000..7761b948431e --- /dev/null +++ b/test/unit/org/apache/cassandra/index/sai/disk/v9/keystore/KeyLookupTest.java @@ -0,0 +1,919 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.index.sai.disk.v9.keystore; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.index.sai.SAITester; +import org.apache.cassandra.index.sai.SAIUtil; +import org.apache.cassandra.index.sai.disk.format.IndexComponentType; +import org.apache.cassandra.index.sai.disk.format.IndexComponents; +import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; +import org.apache.cassandra.index.sai.disk.format.Version; +import org.apache.cassandra.index.sai.disk.v1.MetadataSource; +import org.apache.cassandra.index.sai.disk.v1.MetadataWriter; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesMeta; +import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter; +import org.apache.cassandra.index.sai.utils.PrimaryKey; +import org.apache.cassandra.index.sai.utils.SAICodecUtils; +import org.apache.cassandra.index.sai.utils.SaiRandomizedTest; +import org.apache.cassandra.index.sai.utils.TypeUtil; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.utils.bytecomparable.ByteComparable; +import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse; +import org.apache.lucene.store.IndexInput; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class KeyLookupTest extends SaiRandomizedTest +{ + public static final ByteComparable.Version VERSION = TypeUtil.BYTE_COMPARABLE_VERSION; + private static final int BLOCK_SIZE = 4; + protected IndexDescriptor indexDescriptor; + + @Before + public void setup() throws Exception + { + SAIUtil.setCurrentVersion(Version.GA); + indexDescriptor = newIndexDescriptor(); + } + + @Test + public void testLexicographicException() throws Exception + { + IndexComponents.ForWrite components = indexDescriptor.newPerSSTableComponentsForWrite(); + try (MetadataWriter metadataWriter = new MetadataWriter(components)) + { + NumericValuesWriter blockFPWriter = new NumericValuesWriter(components.addOrGet(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS), + metadataWriter, true); + try (KeyStoreWriter writer = new KeyStoreWriter(components.addOrGet(IndexComponentType.PARTITION_KEY_BLOCKS), + metadataWriter, + blockFPWriter, + 4, + true)) + { + // Start the first partition + writer.startPartition(); + ByteBuffer buffer = Int32Type.instance.decompose(99999); + ByteSource byteSource = Int32Type.instance.asComparableBytes(buffer, VERSION); + byte[] bytes1 = ByteSourceInverse.readBytes(byteSource); + + writer.add(ByteComparable.preencoded(VERSION, bytes1)); + + buffer = Int32Type.instance.decompose(444); + byteSource = Int32Type.instance.asComparableBytes(buffer, VERSION); + byte[] bytes2 = ByteSourceInverse.readBytes(byteSource); + + // Within the same partition, keys must be in ascending lexicographic order + assertThrows(IllegalArgumentException.class, () -> writer.add(ByteComparable.preencoded(VERSION, bytes2))); + + // Start a new partition - now we can add a smaller key because it's a different partition + writer.startPartition(); + writer.add(ByteComparable.preencoded(VERSION, bytes2)); + } + } + } + + @Test + public void testFileValidation() throws Exception + { + List primaryKeys = new ArrayList<>(); + + for (int x = 0; x < 11; x++) + { + ByteBuffer buffer = UTF8Type.instance.decompose(Integer.toString(x)); + DecoratedKey partitionKey = Murmur3Partitioner.instance.decorateKey(buffer); + PrimaryKey primaryKey = SAITester.TEST_FACTORY.create(partitionKey, Clustering.EMPTY); + primaryKeys.add(primaryKey); + } + + primaryKeys.sort(PrimaryKey::compareTo); + IndexComponents.ForWrite components = indexDescriptor.newPerSSTableComponentsForWrite(); + + try (MetadataWriter metadataWriter = new MetadataWriter(components)) + { + NumericValuesWriter blockFPWriter = new NumericValuesWriter(components.addOrGet(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS), + metadataWriter, true); + try (KeyStoreWriter writer = new KeyStoreWriter(components.addOrGet(IndexComponentType.PARTITION_KEY_BLOCKS), + metadataWriter, + blockFPWriter, + 4, + false)) + { + primaryKeys.forEach(primaryKey -> { + try + { + writer.add(primaryKey); + } + catch (IOException e) + { + e.printStackTrace(); + } + }); + } + } + assertTrue(validateComponent(components, IndexComponentType.PARTITION_KEY_BLOCKS, true)); + assertTrue(validateComponent(components, IndexComponentType.PARTITION_KEY_BLOCKS, false)); + assertTrue(validateComponent(components, IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS, true)); + assertTrue(validateComponent(components, IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS, false)); + } + + @Test + public void testSeekToTerm() throws Exception + { + List keys = new ArrayList<>(); + writeTerms(keys); + + // iterate on keys ascending + withKeyLookup(reader -> + { + for (int x = 0; x < keys.size(); x++) + { + try (KeyLookup.Cursor cursor = reader.openCursor()) + { + ByteComparable key = cursor.seekToPointId(x); + + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(VERSION)); + + assertArrayEquals(keys.get(x), bytes); + } + } + }); + + // iterate on keys descending + withKeyLookup(reader -> + { + for (int x = keys.size() - 1; x >= 0; x--) + { + try (KeyLookup.Cursor cursor = reader.openCursor()) + { + ByteComparable key = cursor.seekToPointId(x); + + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(VERSION)); + + assertArrayEquals(keys.get(x), bytes); + } + } + }); + + // iterate randomly + withKeyLookup(reader -> + { + for (int x = 0; x < keys.size(); x++) + { + int target = nextInt(0, keys.size()); + + try (KeyLookup.Cursor cursor = reader.openCursor()) + { + ByteComparable key = cursor.seekToPointId(target); + + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(VERSION)); + + assertArrayEquals(keys.get(target), bytes); + } + } + }); + } + + @Test + public void testLongPrefixesAndSuffixes() throws Exception + { + List keys = new ArrayList<>(); + writeKeys(writer -> { + // The following writes a set of keys that cover the following conditions: + + // Start value 0 + byte[] bytes = new byte[20]; + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + // prefix > 15 + bytes = new byte[20]; + Arrays.fill(bytes, 16, 20, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + // prefix == 15 + bytes = new byte[20]; + Arrays.fill(bytes, 15, 20, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + // prefix < 15 + bytes = new byte[20]; + Arrays.fill(bytes, 14, 20, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + // suffix > 16 + bytes = new byte[20]; + Arrays.fill(bytes, 0, 4, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + // suffix == 16 + bytes = new byte[20]; + Arrays.fill(bytes, 0, 5, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + // suffix < 16 + bytes = new byte[20]; + Arrays.fill(bytes, 0, 6, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + + bytes = new byte[32]; + Arrays.fill(bytes, 0, 16, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + // prefix >= 15 && suffix >= 16 + bytes = new byte[32]; + Arrays.fill(bytes, 0, 32, (byte) 1); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + }, false); + + doTestKeyLookup(keys); + } + + @Test + public void testNonUniqueKeys() throws Exception + { + List keys = new ArrayList<>(); + + writeKeys(writer -> { + for (int x = 0; x < 4000; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(5000)); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + doTestKeyLookup(keys); + } + + @Test + public void testSeekToPointId() throws Exception + { + List keys = new ArrayList<>(); + + writeKeys(writer -> { + for (int x = 0; x < 4000; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + doTestKeyLookup(keys); + } + + @Test + public void testSeekToPointIdCC() throws Exception + { + List terms = new ArrayList<>(); + writeTerms(terms); + + // iterate ascending + withKeyLookupCursor(cursor -> + { + for (int x = 0; x < terms.size(); x++) + { + ByteComparable term = cursor.seekToPointId(x); + + byte[] bytes = ByteSourceInverse.readBytes(term.asComparableBytes(VERSION)); + assertArrayEquals(terms.get(x), bytes); + } + }); + + // iterate descending + withKeyLookupCursor(cursor -> + { + for (int x = terms.size() - 1; x >= 0; x--) + { + ByteComparable term = cursor.seekToPointId(x); + + byte[] bytes = ByteSourceInverse.readBytes(term.asComparableBytes(VERSION)); + assertArrayEquals(terms.get(x), bytes); + } + }); + + // iterate randomly + withKeyLookupCursor(cursor -> + { + for (int x = 0; x < terms.size(); x++) + { + int target = nextInt(0, terms.size()); + ByteComparable term = cursor.seekToPointId(target); + + byte[] bytes = ByteSourceInverse.readBytes(term.asComparableBytes(VERSION)); + assertArrayEquals(terms.get(target), bytes); + } + }); + } + + @Test + public void testSeekToPointIdOutOfRange() throws Exception + { + writeKeys(writer -> { + for (int x = 0; x < 4000; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + withKeyLookupCursor(cursor -> { + assertThatThrownBy(() -> cursor.seekToPointId(-2)).isInstanceOf(IndexOutOfBoundsException.class) + .hasMessage(String.format(KeyLookup.INDEX_OUT_OF_BOUNDS, -2, 4000)); + assertThatThrownBy(() -> cursor.seekToPointId(Long.MAX_VALUE)).isInstanceOf(IndexOutOfBoundsException.class) + .hasMessage(String.format(KeyLookup.INDEX_OUT_OF_BOUNDS, Long.MAX_VALUE, 4000)); + assertThatThrownBy(() -> cursor.seekToPointId(4000)).isInstanceOf(IndexOutOfBoundsException.class) + .hasMessage(String.format(KeyLookup.INDEX_OUT_OF_BOUNDS, 4000, 4000)); + }); + } + + @Test + public void testSeekToKey() throws Exception + { + Map keys = new HashMap<>(); + + writeKeys(writer -> { + long pointId = 0; + for (int x = 0; x < 4000; x += 4) + { + byte[] key = makeKey(x); + keys.put(pointId++, key); + + writer.add(ByteComparable.preencoded(VERSION, key)); + } + }, true); + + withKeyLookupCursor(cursor -> { + assertEquals(0L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(0L)), 0L, 10L)); + assertEquals(160L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(160L)), 160L, 170L)); + assertEquals(165L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(165L)), 160L, 170L)); + assertEquals(175L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(175L)), 160L, 176L)); + assertEquals(176L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(176L)), 160L, 177L)); + assertEquals(176L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(176L)), 175L, 177L)); + assertEquals(176L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(701)), 160L, 177L)); + assertEquals(504L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(504L)), 200L, 600L)); + assertEquals(-1L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(4000)), 0L, 1000L)); + assertEquals(-1L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(4000)), 999L, 1000L)); + assertEquals(999L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(999L)), 0L, 1000L)); + }); + } + + @Test + public void testSeekToKeyOnNonPartitioned() throws Throwable + { + Map keys = new HashMap<>(); + + writeKeys(writer -> { + long pointId = 0; + for (int x = 0; x < 16; x += 4) + { + byte[] key = makeKey(x); + keys.put(pointId++, key); + + writer.add(ByteComparable.preencoded(VERSION, key)); + } + }, false); + + withKeyLookupCursor(cursor -> assertThatThrownBy(() -> cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(0L)), + 0L, 10L)) + .isInstanceOf(AssertionError.class)); + } + + @Test + public void partitionedKeysMustBeInOrderInPartitions() throws Throwable + { + writeKeys(writer -> { + writer.startPartition(); + writer.add(ByteComparable.preencoded(VERSION, makeKey(0))); + writer.add(ByteComparable.preencoded(VERSION, makeKey(10))); + assertThatThrownBy(() -> writer.add(ByteComparable.preencoded(VERSION, makeKey(9)))).isInstanceOf(IllegalArgumentException.class); + writer.startPartition(); + writer.add(ByteComparable.preencoded(VERSION, makeKey(9))); + }, true); + } + + @Test + public void testEmptyCursor() throws Exception + { + // Write an empty key store (keyCount = 0) + writeKeys(writer -> { + }, false); + + withKeyLookupCursor(cursor -> { + assertEquals(-1L, cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(0)), 0L, 10L)); + + assertThatThrownBy(() -> cursor.seekToPointId(0)) + .isInstanceOf(IndexOutOfBoundsException.class) + .hasMessage(String.format(KeyLookup.INDEX_OUT_OF_BOUNDS, 0, 0)); + assertThatThrownBy(() -> cursor.seekToPointId(-1)) + .isInstanceOf(IndexOutOfBoundsException.class) + .hasMessage(String.format(KeyLookup.INDEX_OUT_OF_BOUNDS, -1, 0)); + + // Test close should not throw + cursor.close(); + }); + } + + @Test + public void testSeekToSamePointId() throws Exception + { + List keys = new ArrayList<>(); + writeKeys(writer -> { + for (int x = 0; x < 100; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + withKeyLookupCursor(cursor -> { + // Seek to point 50 + ByteComparable key1 = cursor.seekToPointId(50); + byte[] bytes1 = ByteSourceInverse.readBytes(key1.asComparableBytes(VERSION)); + assertArrayEquals(keys.get(50), bytes1); + + // Seek to the same point again (target == currentPointId) + ByteComparable key2 = cursor.seekToPointId(50); + byte[] bytes2 = ByteSourceInverse.readBytes(key2.asComparableBytes(VERSION)); + assertArrayEquals(keys.get(50), bytes2); + }); + } + + @Test + public void testSeekBackwardsInSameBlock() throws Exception + { + List keys = new ArrayList<>(); + writeKeys(writer -> { + for (int x = 0; x < 20; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + withKeyLookupCursor(cursor -> { + int pointInBlock1 = BLOCK_SIZE + 2; + ByteComparable key1 = cursor.seekToPointId(pointInBlock1); + byte[] bytes1 = ByteSourceInverse.readBytes(key1.asComparableBytes(VERSION)); + assertArrayEquals(keys.get(pointInBlock1), bytes1); + + int earlierPointInBlock1 = BLOCK_SIZE + 1; + ByteComparable key2 = cursor.seekToPointId(earlierPointInBlock1); + byte[] bytes2 = ByteSourceInverse.readBytes(key2.asComparableBytes(VERSION)); + assertArrayEquals(keys.get(earlierPointInBlock1), bytes2); + }); + } + + @Test + public void testSeekForwardWithinBlock() throws Exception + { + List keys = new ArrayList<>(); + writeKeys(writer -> { + for (int x = 0; x < 20; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + withKeyLookupCursor(cursor -> { + // Seek to start of block 1 + int blockStart = BLOCK_SIZE; + cursor.seekToPointId(blockStart); + + // Seek forward within the same block (no block reset needed) + int pointInSameBlock = blockStart + 2; + ByteComparable key = cursor.seekToPointId(pointInSameBlock); + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(VERSION)); + assertArrayEquals(keys.get(pointInSameBlock), bytes); + }); + } + + @Test + public void testClusteredSeekWithMatchAtCurrentPosition() throws Exception + { + Map keys = new HashMap<>(); + writeKeys(writer -> { + writer.startPartition(); + for (long pointId = 0; pointId < 10; pointId++) + { + byte[] key = makeKey((int) pointId * 4); + keys.put(pointId, key); + writer.add(ByteComparable.preencoded(VERSION, key)); + } + + writer.startPartition(); + for (long pointId = 10; pointId < 20; pointId++) + { + byte[] key = makeKey((int) pointId * 4); + keys.put(pointId, key); + writer.add(ByteComparable.preencoded(VERSION, key)); + } + + writer.startPartition(); + for (long pointId = 20; pointId < 30; pointId++) + { + byte[] key = makeKey((int) pointId * 4); + keys.put(pointId, key); + writer.add(ByteComparable.preencoded(VERSION, key)); + } + }, true); + + withKeyLookupCursor(cursor -> { + // Position cursor at the start of partition 2 (point id 10) + cursor.seekToPointId(10); + + // Now do a clustered seek within partition 2 for the key at position 10 + // Since cursor is already at the correct position and the key matches, + // this should hit the early return. + long result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(10L)), 10L, 20L); + assertEquals(10L, result); + }); + } + + @Test + public void testClusteredSeekAtEndOfKeyCount() throws Exception + { + int partitionSize = 25; + writeKeys(writer -> { + writer.startPartition(); + for (long pointId = 0; pointId < partitionSize; pointId++) + { + byte[] key = makeKey((int) pointId * 4); + writer.add(ByteComparable.preencoded(VERSION, key)); + } + }, true); + + withKeyLookupCursor(cursor -> { + // Search for a key that's beyond all keys in the partition, with endingPointId == keyCount + long result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(10000)), 0L, partitionSize); + assertEquals(-1L, result); + }); + } + + @Test + public void testClusteredSeekInLastBlock() throws Exception + { + Map keys = new HashMap<>(); + int numBlocks = 3; + int totalKeys = numBlocks * BLOCK_SIZE; + writeKeys(writer -> { + // Create a single partition with exactly totalKeys clustering keys (numBlocks blocks) + // This ensures all point ids are in the same partition + writer.startPartition(); + for (long pointId = 0; pointId < totalKeys; pointId++) + { + byte[] key = makeKey((int) pointId * 4); + keys.put(pointId, key); + writer.add(ByteComparable.preencoded(VERSION, key)); + } + }, true); + + withKeyLookupCursor(cursor -> { + // Search in the last block within the partition + // This tests the last block logic in moveToBlockAndCompareTo (lines 335-336) + long lastBlockStart = (numBlocks - 1) * BLOCK_SIZE; + long result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(lastBlockStart)), + lastBlockStart, totalKeys); + assertEquals(lastBlockStart, result); + }); + } + + @Test + public void testCursorReset() throws Exception + { + List keys = new ArrayList<>(); + writeKeys(writer -> { + for (int x = 0; x < 50; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + withKeyLookupCursor(cursor -> { + cursor.seekToPointId(25); + + ByteComparable key = cursor.seekToPointId(0); + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(VERSION)); + assertArrayEquals(keys.get(0), bytes); + }); + } + + @Test + public void testCursorResetFromSecondPartition() throws Exception + { + writeKeys(writer -> { + writer.startPartition(); + for (int pointId = 0; pointId < 8; pointId++) + { + writer.add(ByteComparable.preencoded(VERSION, ByteSourceInverse.readBytes(intByteSource(pointId)))); + } + writer.startPartition(); + for (int pointId = 8; pointId < 16; pointId++) + { + writer.add(ByteComparable.preencoded(VERSION, ByteSourceInverse.readBytes(intByteSource(pointId)))); + } + }, true); + + withKeyLookupCursor(cursor -> { + ByteComparable key = cursor.seekToPointId(10); + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + assertArrayEquals(ByteSourceInverse.readBytes(intByteSource(10)), bytes); + + key = cursor.seekToPointId(0); + bytes = ByteSourceInverse.readBytes(key.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + assertArrayEquals(ByteSourceInverse.readBytes(intByteSource(0)), bytes); + + key = cursor.seekToPointId(12); + bytes = ByteSourceInverse.readBytes(key.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + assertArrayEquals(ByteSourceInverse.readBytes(intByteSource(12)), bytes); + }); + } + + @Test + public void testMultipleCursorInstances() throws Exception + { + List keys = new ArrayList<>(); + writeKeys(writer -> { + for (int x = 0; x < 50; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + keys.add(bytes); + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + + withKeyLookup(reader -> { + // Open multiple cursors and verify they work independently + try (KeyLookup.Cursor cursor1 = reader.openCursor(); + KeyLookup.Cursor cursor2 = reader.openCursor()) + { + ByteComparable key1 = cursor1.seekToPointId(10); + ByteComparable key2 = cursor2.seekToPointId(20); + + byte[] bytes1 = ByteSourceInverse.readBytes(key1.asComparableBytes(VERSION)); + byte[] bytes2 = ByteSourceInverse.readBytes(key2.asComparableBytes(VERSION)); + + assertArrayEquals(keys.get(10), bytes1); + assertArrayEquals(keys.get(20), bytes2); + } + }); + } + + @Test + public void testClusteredSeekBinarySearchPath() throws Exception + { + Map keys = new HashMap<>(); + writeKeys(writer -> { + // Create enough keys to trigger binary search (multiple blocks) + for (int x = 0; x < 200; x += 2) + { + byte[] key = makeKey(x); + keys.put((long) x / 2, key); + writer.add(ByteComparable.preencoded(VERSION, key)); + } + }, true); + + withKeyLookupCursor(cursor -> { + // Search for a key in the middle, forcing binary search + // pointId 50 corresponds to value 100 (since we increment by 2) + long result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, keys.get(50L)), 0L, 100L); + assertEquals(50L, result); + + // Search for a key that doesn't exist but falls between existing keys + // Key value 51 doesn't exist, next highest is 52 at position 26 + long result2 = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(51)), 0L, 100L); + assertEquals(26L, result2); + }); + } + + private byte[] makeKey(int value) + { + return ByteSourceInverse.readBytes(intByteSource(value)); + } + + private void doTestKeyLookup(List keys) throws Exception + { + // iterate ascending + withKeyLookupCursor(cursor -> { + for (int x = 0; x < keys.size(); x++) + { + ByteComparable key = cursor.seekToPointId(x); + + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + + assertArrayEquals(keys.get(x), bytes); + } + }); + + // iterate ascending skipping blocks + withKeyLookupCursor(cursor -> { + for (int x = 0; x < keys.size(); x += 17) + { + ByteComparable key = cursor.seekToPointId(x); + + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + + assertArrayEquals(keys.get(x), bytes); + } + }); + + withKeyLookupCursor(cursor -> { + ByteComparable key = cursor.seekToPointId(7); + byte[] bytes = ByteSourceInverse.readBytes(key.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + assertArrayEquals(keys.get(7), bytes); + + key = cursor.seekToPointId(7); + bytes = ByteSourceInverse.readBytes(key.asComparableBytes(TypeUtil.BYTE_COMPARABLE_VERSION)); + assertArrayEquals(keys.get(7), bytes); + }); + } + + private void writeTerms(List terms) throws Exception + { + writeKeys(writer -> { + + for (int x = 0; x < 1000 * 4; x++) + { + byte[] bytes = ByteSourceInverse.readBytes(intByteSource(x)); + terms.add(bytes); + + writer.add(ByteComparable.preencoded(VERSION, bytes)); + } + }, false); + } + + @Test + public void testClusteredSeekKeyBeforePartition() throws Exception + { + writeKeys(writer -> { + writer.startPartition(); + for (int x = 10; x < 26; x += 2) + { + writer.add(ByteComparable.preencoded(VERSION, ByteSourceInverse.readBytes(intByteSource(x)))); + } + }, true); + + withKeyLookupCursor(cursor -> { + long result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(5)), 0L, 8L); + assertEquals(0L, result); + + result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(11)), 0L, 8L); + assertEquals(1L, result); + }); + } + + @Test + public void testClusteredSeekKeyOutsidePartition() throws Exception + { + writeKeys(writer -> { + writer.startPartition(); + for (int pointId = 0; pointId < 8; pointId++) + { + writer.add(ByteComparable.preencoded(VERSION, ByteSourceInverse.readBytes(intByteSource(pointId * 2)))); + } + writer.startPartition(); + for (int pointId = 8; pointId < 16; pointId++) + { + writer.add(ByteComparable.preencoded(VERSION, ByteSourceInverse.readBytes(intByteSource(pointId * 2)))); + } + writer.startPartition(); + for (int pointId = 16; pointId < 24; pointId++) + { + writer.add(ByteComparable.preencoded(VERSION, ByteSourceInverse.readBytes(intByteSource(pointId * 2)))); + } + }, true); + + withKeyLookupCursor(cursor -> { + // Search for a key (value 4) that exists in partition 0, but we're searching in partition 1 (pointIds 8-15) + // Should return the startingPointId (8) since the search key is before all keys in the search range + long result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(4)), 8L, 16L); + assertEquals(8L, result); + + // Search for a key (value 10) from partition 0, searching in partition 1 + // Should also return startingPointId (8) + result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(10)), 8L, 16L); + assertEquals(8L, result); + + // Search for a key from partition 1 while searching in partition 0 + // Should return startingPointId (8) of next partition, i.e., partition 1 + result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(20)), 0L, 8L); + assertEquals(8L, result); + + // Search for a key from partition 2 while searching in partition 0 + // Should return startingPointId (8) of next partition, i.e., partition 1 + result = cursor.clusteredSeekToKey(ByteComparable.preencoded(VERSION, makeKey(34)), 0L, 8L); + assertEquals(8L, result); + }); + } + + private ByteSource intByteSource(int value) + { + ByteBuffer buffer = Int32Type.instance.decompose(value); + return Int32Type.instance.asComparableBytes(buffer, VERSION); + } + + protected void writeKeys(ThrowingConsumer testCode, boolean clustering) throws Exception + { + IndexComponents.ForWrite components = indexDescriptor.newPerSSTableComponentsForWrite(); + try (MetadataWriter metadataWriter = new MetadataWriter(components)) + { + NumericValuesWriter blockFPWriter = new NumericValuesWriter(components.addOrGet(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS), + metadataWriter, true); + try (KeyStoreWriter writer = new KeyStoreWriter(components.addOrGet(IndexComponentType.PARTITION_KEY_BLOCKS), + metadataWriter, + blockFPWriter, + BLOCK_SIZE, + clustering)) + { + testCode.accept(writer); + } + } + components.markComplete(); + } + + private void withKeyLookup(ThrowingConsumer testCode) throws Exception + { + IndexComponents.ForRead components = indexDescriptor.perSSTableComponents(); + MetadataSource metadataSource = MetadataSource.loadMetadata(components); + NumericValuesMeta blockPointersMeta = new NumericValuesMeta(metadataSource.get(components.get(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS))); + KeyLookupMeta keyLookupMeta = new KeyLookupMeta(metadataSource.get(components.get(IndexComponentType.PARTITION_KEY_BLOCKS))); + try (FileHandle keysData = components.get(IndexComponentType.PARTITION_KEY_BLOCKS).createFileHandle(); + FileHandle blockOffsets = components.get(IndexComponentType.PARTITION_KEY_BLOCK_OFFSETS).createFileHandle()) + { + KeyLookup reader = new KeyLookup(keysData, blockOffsets, keyLookupMeta, blockPointersMeta); + testCode.accept(reader); + } + } + + private void withKeyLookupCursor(ThrowingConsumer testCode) throws Exception + { + withKeyLookup(reader -> { + try (KeyLookup.Cursor cursor = reader.openCursor()) + { + testCode.accept(cursor); + } + }); + } + + private boolean validateComponent(IndexComponents.ForRead components, IndexComponentType indexComponentType, boolean checksum) + { + try (IndexInput input = components.get(indexComponentType).openInput()) + { + if (checksum) + SAICodecUtils.validateChecksum(input, Version.GA); + else + SAICodecUtils.validate(input, Version.GA); + return true; + } + catch (Throwable ignored) + { + return false; + } + } + + @FunctionalInterface + public interface ThrowingConsumer + { + void accept(T t) throws Exception; + } +} diff --git a/test/unit/org/apache/cassandra/index/sai/functional/GroupComponentsTest.java b/test/unit/org/apache/cassandra/index/sai/functional/GroupComponentsTest.java index e45555fca67e..ff1fecab3461 100644 --- a/test/unit/org/apache/cassandra/index/sai/functional/GroupComponentsTest.java +++ b/test/unit/org/apache/cassandra/index/sai/functional/GroupComponentsTest.java @@ -21,6 +21,7 @@ import java.util.Set; import com.google.common.collect.Iterables; + import org.junit.Assert; import org.junit.Test; @@ -55,7 +56,10 @@ public void testInvalidateWithoutObsolete() SSTableReader sstable = Iterables.getOnlyElement(cfs.getLiveSSTables()); Set components = group.activeComponents(sstable); - assertEquals(Version.current(KEYSPACE).onDiskFormat().perSSTableComponentTypes().size() + 1, components.size()); + assertEquals(Version.current(KEYSPACE).onDiskFormat() + .perSSTableComponentTypes(currentTableMetadata().hasClustering()) + .size() + 1, + components.size()); // index files are released but not removed cfs.invalidate(true, false); @@ -81,7 +85,10 @@ public void getLiveComponentsForEmptyIndex() Set components = group.activeComponents(sstables.iterator().next()); - assertEquals(Version.current(KEYSPACE).onDiskFormat().perSSTableComponentTypes().size() + 1, components.size()); + assertEquals(Version.current(KEYSPACE).onDiskFormat() + .perSSTableComponentTypes(currentTableMetadata().hasClustering()) + .size() + 1, + components.size()); } @Test @@ -101,8 +108,10 @@ public void getLiveComponentsForPopulatedIndex() Set components = group.activeComponents(sstables.iterator().next()); - assertEquals(Version.current(KEYSPACE).onDiskFormat().perSSTableComponentTypes().size() + - Version.current(KEYSPACE).onDiskFormat().perIndexComponentTypes(indexContext).size(), + assertEquals(Version.current(KEYSPACE).onDiskFormat() + .perSSTableComponentTypes(currentTableMetadata().hasClustering()) + .size() + + Version.current(KEYSPACE).onDiskFormat().perIndexComponentTypes(indexContext).size(), components.size()); } diff --git a/test/unit/org/apache/cassandra/index/sai/functional/IndexBuildDeciderTest.java b/test/unit/org/apache/cassandra/index/sai/functional/IndexBuildDeciderTest.java index b5c76ce897be..27a034da0b21 100644 --- a/test/unit/org/apache/cassandra/index/sai/functional/IndexBuildDeciderTest.java +++ b/test/unit/org/apache/cassandra/index/sai/functional/IndexBuildDeciderTest.java @@ -40,8 +40,8 @@ import org.apache.cassandra.index.sai.SSTableContextManager; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; +import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.disk.v1.MemtableIndexWriter; -import org.apache.cassandra.index.sai.disk.v2.V2OnDiskFormat; import org.apache.cassandra.inject.Injections; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.FileUtils; @@ -158,8 +158,10 @@ private int sstableFileCount(SSTableReader secondSSTable) private int numericIndexFileCount() { IndexContext context = createIndexContext("v1", Int32Type.instance); - return V2OnDiskFormat.instance.perIndexComponentTypes(context).size() - + V2OnDiskFormat.instance.perSSTableComponentTypes().size(); + return Version.current(KEYSPACE).onDiskFormat().perIndexComponentTypes(context).size() + + Version.current(KEYSPACE).onDiskFormat() + .perSSTableComponentTypes(currentTableMetadata().hasClustering()) + .size(); } public static class IndexBuildDeciderWithoutInitialBuild implements IndexBuildDecider diff --git a/test/unit/org/apache/cassandra/index/sai/functional/SaiDiskSizeTest.java b/test/unit/org/apache/cassandra/index/sai/functional/SaiDiskSizeTest.java index 5072bf583fa4..33d0a2953628 100644 --- a/test/unit/org/apache/cassandra/index/sai/functional/SaiDiskSizeTest.java +++ b/test/unit/org/apache/cassandra/index/sai/functional/SaiDiskSizeTest.java @@ -30,7 +30,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,12 +66,11 @@ public class SaiDiskSizeTest extends SAITester /** * The expected sizes were determined empirically to satisfy the result of both flush and compaction. * To understand the difference check {@link Version} and on disk components. - * There are no vectors involved, thus the expected sizes are not affected by chenges to Vector format. + * There are no vectors involved, thus the expected sizes are not affected by changes to Vector format. * * @return a collection of parameters to test */ - @Parameterized.Parameters(name = "saiFormat={0}, expectedDiskSizeFor2SSTables={1}, " + - "expectedDiskSizeForCompactedSSTable={2}, pkColumns={3}, rowsPerPartition={4}") + @Parameterized.Parameters(name = "saiFormat={0}, rowsPerPartition={4}") public static Collection generateParameters() { return Version.ALL.stream() @@ -81,7 +79,7 @@ public static Collection generateParameters() { case "aa": return Stream.of( - new Object[]{ v, 24766, 24989, "pk", 1 }, + new Object[]{ v, 24989, 24989, "pk", 1 }, new Object[]{ v, 26026, 26181, "pk, v_int", 2 }, new Object[]{ v, 28526, 26603, "pk, v_int", 100 }); case "ba": @@ -100,11 +98,18 @@ public static Collection generateParameters() new Object[]{ v, 59133, 57249, "pk, v_int", 100 }); case "ed": case "fa": - default: + case "fb": return Stream.of( new Object[]{ v, 134777, 132849, "pk", 1 }, new Object[]{ v, 118465, 116546, "pk, v_int", 2 }, new Object[]{ v, 59149, 57257, "pk, v_int", 100 }); + case "ga": + default: + return // A new version assumes the latest size by default + Stream.of( + new Object[]{ v, 34901, 34689, "pk", 1 }, + new Object[]{ v, 39313, 39313, "pk, v_int", 2 }, + new Object[]{ v, 33549, 31315, "pk, v_int", 100 }); } }) .collect(Collectors.toList()); @@ -159,15 +164,16 @@ public void testIndexDiskSizeAcrossVersions() throws UnknownHostException assertThat(diskSize) .as("Disk size for SAI version %s before compaction", saiFormat) .isLessThanOrEqualTo(expectedDiskSizeFor2SSTables) - .isGreaterThan((long) (expectedDiskSizeFor2SSTables * 0.95)); + .isGreaterThan((long) (expectedDiskSizeFor2SSTables * 0.92)); compact(); diskSize = indexDiskSpaceUse(); + logger.info("Disk size for SAI version {}: {}", saiFormat, diskSize); assertThat(diskSize) .as("Disk size for SAI version %s after compaction", saiFormat) .isLessThanOrEqualTo(expectedDiskSizeForCompactedSSTable) - .isGreaterThan((long) (expectedDiskSizeForCompactedSSTable * 0.95)); + .isGreaterThan((long) (expectedDiskSizeForCompactedSSTable * 0.92)); } private void insertRowsIntoOneSegment(int nrRows, int startRow) throws UnknownHostException diff --git a/test/unit/org/apache/cassandra/index/sai/metrics/IndexGroupMetricsTest.java b/test/unit/org/apache/cassandra/index/sai/metrics/IndexGroupMetricsTest.java index 48357ecdfdd3..91f78e3f8480 100644 --- a/test/unit/org/apache/cassandra/index/sai/metrics/IndexGroupMetricsTest.java +++ b/test/unit/org/apache/cassandra/index/sai/metrics/IndexGroupMetricsTest.java @@ -65,7 +65,7 @@ public void verifyIndexGroupMetrics() // with 10 sstable int indexopenFileCountWithOnlyNumeric = getOpenIndexFiles(); - assertEquals(sstables * (Version.current(KEYSPACE).onDiskFormat().openFilesPerSSTable() + + assertEquals(sstables * (Version.current(KEYSPACE).onDiskFormat().openFilesPerSSTable(false) + Version.current(KEYSPACE).onDiskFormat().openFilesPerIndex(v1IndexContext)), indexopenFileCountWithOnlyNumeric); @@ -88,14 +88,14 @@ public void verifyIndexGroupMetrics() compact(); long perSSTableFileDiskUsage = getDiskUsage(); - assertEquals(Version.current(KEYSPACE).onDiskFormat().openFilesPerSSTable() + + assertEquals(Version.current(KEYSPACE).onDiskFormat().openFilesPerSSTable(false) + Version.current(KEYSPACE).onDiskFormat().openFilesPerIndex(v2IndexContext) + Version.current(KEYSPACE).onDiskFormat().openFilesPerIndex(v1IndexContext), getOpenIndexFiles()); // drop string index, reduce open string index files, per-sstable file disk usage remains the same dropIndex("DROP INDEX %s." + v2IndexName); - assertEquals(Version.current(KEYSPACE).onDiskFormat().openFilesPerSSTable() + + assertEquals(Version.current(KEYSPACE).onDiskFormat().openFilesPerSSTable(false) + Version.current(KEYSPACE).onDiskFormat().openFilesPerIndex(v1IndexContext), getOpenIndexFiles()); assertEquals(perSSTableFileDiskUsage, getDiskUsage()); diff --git a/test/unit/org/apache/cassandra/index/sai/metrics/SegmentFlushingFailureTest.java b/test/unit/org/apache/cassandra/index/sai/metrics/SegmentFlushingFailureTest.java index 33121881aad3..55c7f118bdb0 100644 --- a/test/unit/org/apache/cassandra/index/sai/metrics/SegmentFlushingFailureTest.java +++ b/test/unit/org/apache/cassandra/index/sai/metrics/SegmentFlushingFailureTest.java @@ -33,6 +33,9 @@ import org.apache.cassandra.index.sai.disk.format.Version; import org.apache.cassandra.index.sai.disk.v1.SSTableIndexWriter; import org.apache.cassandra.index.sai.disk.v1.SegmentBuilder; +import org.apache.cassandra.index.sai.disk.v1.V1SSTableComponentsWriter; +import org.apache.cassandra.index.sai.disk.v2.V2SSTableComponentsWriter; +import org.apache.cassandra.index.sai.disk.v9.V9SSTableComponentsWriter; import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter; import org.apache.cassandra.inject.Injection; import org.apache.cassandra.inject.Injections; @@ -77,13 +80,19 @@ public void initialize() throws Throwable private static final Injection v1sstableComponentsWriterFailure = newFailureOnEntry("sstableComponentsWriterFailure", - org.apache.cassandra.index.sai.disk.v1.SSTableComponentsWriter.class, + V1SSTableComponentsWriter.class, "complete", RuntimeException.class); private static final Injection v2sstableComponentsWriterFailure = newFailureOnEntry("sstableComponentsWriterFailure", - org.apache.cassandra.index.sai.disk.v2.SSTableComponentsWriter.class, + V2SSTableComponentsWriter.class, + "complete", + RuntimeException.class); + + private static final Injection v9sstableComponentsWriterFailure = + newFailureOnEntry("sstableComponentsWriterFailure", + V9SSTableComponentsWriter.class, "complete", RuntimeException.class); @@ -141,9 +150,21 @@ public void testSegmentMemoryTrackerLifecycle() throws Throwable @Test public void shouldZeroMemoryTrackerOnOffsetsRuntimeFailure() throws Throwable { - shouldZeroMemoryTrackerOnFailure(Version.current(KEYSPACE) == Version.AA ? v1sstableComponentsWriterFailure : v2sstableComponentsWriterFailure, "v1"); + shouldZeroMemoryTrackerOnFailure(getSstableComponentsWriterFailure(), + "v1"); resetCounters(); - shouldZeroMemoryTrackerOnFailure(Version.current(KEYSPACE) == Version.AA ? v1sstableComponentsWriterFailure : v2sstableComponentsWriterFailure, "v2"); + shouldZeroMemoryTrackerOnFailure(getSstableComponentsWriterFailure(), + "v2"); + } + + private static Injection getSstableComponentsWriterFailure() + { + if (Version.current(KEYSPACE).onOrAfter(Version.GA)) + return v9sstableComponentsWriterFailure; + else if (Version.current(KEYSPACE).onOrAfter(Version.BA)) + return v2sstableComponentsWriterFailure; + else + return v1sstableComponentsWriterFailure; } @Test diff --git a/test/unit/org/apache/cassandra/index/sai/utils/AbstractPrimaryKeyTest.java b/test/unit/org/apache/cassandra/index/sai/utils/AbstractPrimaryKeyTest.java index e57476d0b3eb..ef10715a94c9 100644 --- a/test/unit/org/apache/cassandra/index/sai/utils/AbstractPrimaryKeyTest.java +++ b/test/unit/org/apache/cassandra/index/sai/utils/AbstractPrimaryKeyTest.java @@ -83,13 +83,13 @@ public class AbstractPrimaryKeyTest extends SaiRandomizedTest .addClusteringColumn("ck1", UTF8Type.instance) .build(); - static TableMetadata compositePartitionMultipleClusteringAsc = TableMetadata.builder("test", "test") - .partitioner(Murmur3Partitioner.instance) - .addPartitionKeyColumn("pk1", Int32Type.instance) - .addPartitionKeyColumn("pk2", Int32Type.instance) - .addClusteringColumn("ck1", UTF8Type.instance) - .addClusteringColumn("ck2", UTF8Type.instance) - .build(); + protected static TableMetadata compositePartitionMultipleClusteringAsc = TableMetadata.builder("test", "test") + .partitioner(Murmur3Partitioner.instance) + .addPartitionKeyColumn("pk1", Int32Type.instance) + .addPartitionKeyColumn("pk2", Int32Type.instance) + .addClusteringColumn("ck1", UTF8Type.instance) + .addClusteringColumn("ck2", UTF8Type.instance) + .build(); static TableMetadata compositePartitionSingleClusteringDesc = TableMetadata.builder("test", "test") .partitioner(Murmur3Partitioner.instance) @@ -128,7 +128,7 @@ void assertByteComparison(PrimaryKey a, PrimaryKey b, int expected) TypeUtil.BYTE_COMPARABLE_VERSION)); } - void assertCompareToAndEquals(PrimaryKey a, PrimaryKey b, int expected) + protected void assertCompareToAndEquals(PrimaryKey a, PrimaryKey b, int expected) { if (expected > 0) { @@ -147,7 +147,7 @@ else if (expected < 0) } } - DecoratedKey makeKey(TableMetadata table, Object...partitionKeys) + protected DecoratedKey makeKey(TableMetadata table, Object... partitionKeys) { ByteBuffer key; if (TypeUtil.isComposite(table.partitionKeyType)) @@ -157,7 +157,7 @@ DecoratedKey makeKey(TableMetadata table, Object...partitionKeys) return table.partitioner.decorateKey(key); } - Clustering makeClustering(TableMetadata table, String...clusteringKeys) + protected Clustering makeClustering(TableMetadata table, String... clusteringKeys) { Clustering clustering; if (table.comparator.size() == 0) diff --git a/test/unit/org/apache/cassandra/index/sai/utils/IndexInputLeakDetector.java b/test/unit/org/apache/cassandra/index/sai/utils/IndexInputLeakDetector.java index d17198cc8f1a..b55773689138 100644 --- a/test/unit/org/apache/cassandra/index/sai/utils/IndexInputLeakDetector.java +++ b/test/unit/org/apache/cassandra/index/sai/utils/IndexInputLeakDetector.java @@ -24,6 +24,7 @@ import java.util.Set; import com.carrotsearch.randomizedtesting.rules.TestRuleAdapter; +import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; import org.apache.cassandra.index.sai.disk.io.IndexInput; @@ -38,11 +39,16 @@ public class IndexInputLeakDetector extends TestRuleAdapter private final static Set trackedIndexFileUtils = Collections.synchronizedSet(new HashSet<>()); public IndexDescriptor newIndexDescriptor(Descriptor descriptor, SequentialWriterOption sequentialWriterOption) + { + return newIndexDescriptor(descriptor, sequentialWriterOption, new ClusteringComparator()); + } + + public IndexDescriptor newIndexDescriptor(Descriptor descriptor, SequentialWriterOption sequentialWriterOption, ClusteringComparator comparator) { TrackingIndexFileUtils trackingIndexFileUtils = new TrackingIndexFileUtils(sequentialWriterOption); trackedIndexFileUtils.add(trackingIndexFileUtils); IndexFileUtils.setOverrideInstance(trackingIndexFileUtils); - return IndexDescriptor.empty(descriptor); + return IndexDescriptor.empty(descriptor, comparator); } @Override diff --git a/test/unit/org/apache/cassandra/index/sai/utils/SaiRandomizedTest.java b/test/unit/org/apache/cassandra/index/sai/utils/SaiRandomizedTest.java index b2b7ace5de8f..1823565d5603 100644 --- a/test/unit/org/apache/cassandra/index/sai/utils/SaiRandomizedTest.java +++ b/test/unit/org/apache/cassandra/index/sai/utils/SaiRandomizedTest.java @@ -22,6 +22,8 @@ import java.util.Random; import com.google.common.base.Preconditions; + +import org.apache.cassandra.index.sai.disk.io.IndexFileUtils; import org.apache.cassandra.io.util.FileUtils; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -40,6 +42,7 @@ import org.apache.cassandra.io.sstable.SequenceBasedSSTableId; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.SequentialWriterOption; +import org.apache.cassandra.schema.TableMetadata; @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class SaiRandomizedTest extends RandomizedTest @@ -97,6 +100,16 @@ public IndexDescriptor newIndexDescriptor() throws IOException .build()); } + public static IndexDescriptor newClusteringIndexDescriptor(TableMetadata metadata) throws IOException + { + return indexInputLeakDetector.newIndexDescriptor(new Descriptor(new File(temporaryFolder.newFolder()), + randomSimpleString(5, 13), + randomSimpleString(3, 17), + new SequenceBasedSSTableId(randomIntBetween(0, 128))), + IndexFileUtils.DEFAULT_WRITER_OPTION, + metadata.comparator); + } + public String newIndex() { return randomSimpleString(2, 29); diff --git a/test/unit/org/apache/cassandra/index/sai/utils/RowAwarePrimaryKeyFactoryTest.java b/test/unit/org/apache/cassandra/index/sai/utils/V2RowAwarePrimaryKeyFactoryTest.java similarity index 89% rename from test/unit/org/apache/cassandra/index/sai/utils/RowAwarePrimaryKeyFactoryTest.java rename to test/unit/org/apache/cassandra/index/sai/utils/V2RowAwarePrimaryKeyFactoryTest.java index 0f8cf4371b3d..d9d5898e3b5d 100644 --- a/test/unit/org/apache/cassandra/index/sai/utils/RowAwarePrimaryKeyFactoryTest.java +++ b/test/unit/org/apache/cassandra/index/sai/utils/V2RowAwarePrimaryKeyFactoryTest.java @@ -23,14 +23,14 @@ import org.junit.Test; import org.apache.cassandra.db.Clustering; -import org.apache.cassandra.index.sai.disk.v2.RowAwarePrimaryKeyFactory; +import org.apache.cassandra.index.sai.disk.v2.V2RowAwarePrimaryKeyFactory; -public class RowAwarePrimaryKeyFactoryTest extends AbstractPrimaryKeyTest +public class V2RowAwarePrimaryKeyFactoryTest extends AbstractPrimaryKeyTest { @Test public void singlePartitionTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(simplePartition.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(simplePartition.comparator); int rows = nextInt(10, 100); PrimaryKey[] keys = new PrimaryKey[rows]; for (int index = 0; index < rows; index++) @@ -45,7 +45,7 @@ public void singlePartitionTest() @Test public void compositePartitionTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(compositePartition.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(compositePartition.comparator); int rows = nextInt(10, 100); PrimaryKey[] keys = new PrimaryKey[rows]; for (int index = 0; index < rows; index++) @@ -60,7 +60,7 @@ public void compositePartitionTest() @Test public void simplePartitonSingleClusteringAscTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(simplePartitionSingleClusteringAsc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(simplePartitionSingleClusteringAsc.comparator); int rows = nextInt(10, 100); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -85,7 +85,7 @@ public void simplePartitonSingleClusteringAscTest() @Test public void simplePartitionMultipleClusteringAscTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(simplePartitionMultipleClusteringAsc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(simplePartitionMultipleClusteringAsc.comparator); int rows = nextInt(100, 1000); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -116,7 +116,7 @@ public void simplePartitionMultipleClusteringAscTest() @Test public void simplePartitonSingleClusteringDescTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(simplePartitionSingleClusteringDesc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(simplePartitionSingleClusteringDesc.comparator); int rows = nextInt(10, 100); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -141,7 +141,7 @@ public void simplePartitonSingleClusteringDescTest() @Test public void simplePartitionMultipleClusteringDescTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(simplePartitionMultipleClusteringDesc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(simplePartitionMultipleClusteringDesc.comparator); int rows = nextInt(100, 1000); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -172,7 +172,7 @@ public void simplePartitionMultipleClusteringDescTest() @Test public void compositePartitionSingleClusteringAscTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(compositePartitionSingleClusteringAsc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(compositePartitionSingleClusteringAsc.comparator); int rows = nextInt(10, 100); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -197,7 +197,7 @@ public void compositePartitionSingleClusteringAscTest() @Test public void compositePartitionMultipleClusteringAscTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(compositePartitionMultipleClusteringAsc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(compositePartitionMultipleClusteringAsc.comparator); int rows = nextInt(100, 1000); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -228,7 +228,7 @@ public void compositePartitionMultipleClusteringAscTest() @Test public void compositePartitionSingleClusteringDescTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(compositePartitionSingleClusteringDesc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(compositePartitionSingleClusteringDesc.comparator); int rows = nextInt(10, 100); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -253,7 +253,7 @@ public void compositePartitionSingleClusteringDescTest() @Test public void compositePartitionMultipleClusteringDescTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(compositePartitionMultipleClusteringDesc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(compositePartitionMultipleClusteringDesc.comparator); int rows = nextInt(100, 1000); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -284,7 +284,7 @@ public void compositePartitionMultipleClusteringDescTest() @Test public void simplePartitionMultipleClusteringMixedTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(simplePartitionMultipleClusteringMixed.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(simplePartitionMultipleClusteringMixed.comparator); int rows = nextInt(100, 1000); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -315,7 +315,7 @@ public void simplePartitionMultipleClusteringMixedTest() @Test public void compositePartitionMultipleClusteringMixedTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(compositePartitionMultipleClusteringMixed.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(compositePartitionMultipleClusteringMixed.comparator); int rows = nextInt(100, 1000); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0; @@ -346,7 +346,7 @@ public void compositePartitionMultipleClusteringMixedTest() @Test public void simplePartitonStaticAndSingleClusteringAscTest() { - PrimaryKey.Factory factory = new RowAwarePrimaryKeyFactory(simplePartitionStaticAndSingleClusteringAsc.comparator); + PrimaryKey.Factory factory = new V2RowAwarePrimaryKeyFactory(simplePartitionStaticAndSingleClusteringAsc.comparator); int rows = nextInt(10, 100); PrimaryKey[] keys = new PrimaryKey[rows]; int partition = 0;