arjunashok commented on code in PR #254:
URL:
https://github.com/apache/cassandra-analytics/pull/254#discussion_r4342706583
##########
cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/data/DataLayer.java:
##########
@@ -267,13 +274,24 @@ public boolean useIncrementalRepair()
return true;
}
+ public boolean saiFilteringEnabled()
+ {
+ return ClientConfig.DEFAULT_SAI_FILTERING_ENABLED;
+ }
+
+ public int saiMaxCandidateTokens()
+ {
+ return ClientConfig.DEFAULT_SAI_MAX_CANDIDATE_TOKENS;
+ }
+
/**
* @return CompactionScanner for iterating over one or more SSTables,
compacting data and purging tombstones
*/
public StreamScanner<RowData> openCompactionScanner(int partitionId,
List<PartitionKeyFilter> partitionKeyFilters,
SSTableTimeRangeFilter
sstableTimeRangeFilter,
- @Nullable
PruneColumnFilter columnFilter)
+ @Nullable
PruneColumnFilter columnFilter,
+ @NotNull
List<SaiFilter> saiFilters)
Review Comment:
We're changing the public signature here, potentially impacting subclasses
overriding it. Consider either keeping the 4-arg method for compatibility or
document this change in CHANGES.txt
##########
cassandra-six-zero-bridge/src/main/java/org/apache/cassandra/bridge/CassandraBridgeImplementation.java:
##########
@@ -227,6 +232,186 @@ public StreamScanner<RowData>
getCompactionScanner(@NotNull CqlTable table,
}));
}
+ @Override
+ public StreamScanner<RowData> getCompactionScanner(@NotNull CqlTable table,
+ @NotNull Partitioner
partitioner,
+ @NotNull
SSTablesSupplier ssTables,
+ @Nullable
SparkRangeFilter sparkRangeFilter,
+ @NotNull
Collection<PartitionKeyFilter> partitionKeyFilters,
+ @NotNull
SSTableTimeRangeFilter sstableTimeRangeFilter,
+ @Nullable
PruneColumnFilter columnFilter,
+ @NotNull TimeProvider
timeProvider,
+ boolean readIndexOffset,
+ boolean
useIncrementalRepair,
+ @NotNull Stats stats,
+ @NotNull
List<SaiFilter> saiFilters,
+ int
saiMaxCandidateTokens)
+ {
+ // A complete partition-key lookup is already more selective and
cheaper than consulting SAI.
+ if (!partitionKeyFilters.isEmpty() || saiFilters.isEmpty())
+ {
+ return getCompactionScanner(table, partitioner, ssTables,
sparkRangeFilter, partitionKeyFilters,
+ sstableTimeRangeFilter, columnFilter,
timeProvider, readIndexOffset,
+ useIncrementalRepair, stats);
+ }
+
+ // NOTE: Need to use SchemaBuilder to init keyspace if not already set
in Cassandra Schema instance
+ SchemaBuilder schemaBuilder = new SchemaBuilder(table, partitioner);
+ TableMetadata metadata = schemaBuilder.tableMetaData();
+
+ // Resolve each sstable token span before opening any SAI resources so
data files outside this
+ // Spark worker's token range are ignored.
+ Set<SaiSSTableReference> references = ssTables.openAll((sstable,
isRepairPrimary) -> {
+ if (sparkRangeFilter == null)
+ {
+ return new SaiSSTableReference(sstable, isRepairPrimary);
+ }
+ Pair<BigInteger, BigInteger> tokens = null;
+ if (sstable.isBigFormat())
+ {
+ SSTableSummary summary =
getSSTableSummary(metadata.partitioner,
+ sstable,
+
metadata.params.minIndexInterval,
+
metadata.params.maxIndexInterval);
Review Comment:
Could we use `SSTableCache` to prevent the network read ? If so, we might
not want to close the summary (done in `getSSTableSummary()` as it gets freed)
##########
cassandra-five-zero-bridge/src/main/java/org/apache/cassandra/spark/reader/sai/SaiIndexReader.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cassandra.spark.reader.sai;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.bridge.TokenRange;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.PartitionRangeReadCommand;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.dht.Bounds;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+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.SSTableIndexFileAccess;
+import org.apache.cassandra.index.sai.disk.v1.MetadataSource;
+import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
+import org.apache.cassandra.index.sai.disk.v1.segment.IndexSegmentSearcher;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIntersectionIterator;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator;
+import org.apache.cassandra.index.sai.plan.SaiPlanAccessor;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.plan.QueryController;
+import org.apache.cassandra.index.sai.plan.SaiQueryPlanner;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.spark.data.SSTable;
+import org.apache.cassandra.spark.sparksql.filters.SaiFilter;
+import org.apache.cassandra.spark.sparksql.filters.SparkRangeFilter;
+import org.apache.cassandra.spark.utils.Preconditions;
+import org.apache.cassandra.utils.TokenUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Opens Cassandra 5 SAI components, executes the query plan once, and
materializes the resulting candidate tokens
+ * as an exact compact token set.
+ *
+ * <p>Cassandra's native SAI planner analyzes the conjunctive query, including
same-column range folding and
+ * intersection. For every planned expression, matches are first UNIONed
across all SSTables before Cassandra
+ * intersects different expressions. This ordering is important for
correctness: values contributing to a logically
+ * reconciled row may live in different SSTables. Applying conjunctions inside
each SSTable could therefore create
+ * false negatives.</p>
+ *
+ * <p>SAI is a read-planning phase only. The native iterator is fully consumed
before {@code Data.db} scanning starts,
+ * then all SAI resources are closed. The resulting exact tokens are shared by
every participating SSTable so the normal
+ * Cassandra compaction / reconciliation path can still apply newer values,
TTLs and tombstones. Spark keeps the
+ * original predicates as residual filters, so stale index entries can only
create false positives.</p>
+ */
+public final class SaiIndexReader
+{
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SaiIndexReader.class);
+
+ private SaiIndexReader()
+ {
+ throw new IllegalStateException("Static utility class");
+ }
+
+ /**
+ * Executes all SAI predicates once and returns the sorted candidate
tokens as an exact compact token set.
+ *
+ * @return empty {@link Optional} when SAI cannot safely be used and the
caller should fall back to the ordinary SSTable
+ * scan; an empty {@link CandidateTokens} means SAI executed
successfully and found no candidates
+ */
+ @NotNull
+ public static Optional<CandidateTokens> findCandidateTokens(@NotNull
TableMetadata metadata,
+ @NotNull
Set<SSTable> sstables,
+ @NotNull
List<SaiFilter> filters,
+ @Nullable
SparkRangeFilter sparkRangeFilter,
+ int
maxCandidateTokens)
+ {
+ if (sstables.isEmpty() || filters.isEmpty())
+ {
+ return Optional.of(CandidateTokens.empty());
+ }
+
+ ResourceGroup resources = new ResourceGroup();
+ try
+ {
+ SaiQueryPlanner.Plan plan = SaiQueryPlanner.plan(metadata,
filters);
+ if (plan == null)
+ {
+ return Optional.empty();
+ }
+
+ List<StorageAttachedIndex> resourceIndexes =
plan.resourceIndexes();
+
+ List<SSTableResources> sstableResources = new
ArrayList<>(sstables.size());
+ for (SSTable sstable : sstables)
+ {
+ SSTableResources opened = SSTableResources.open(metadata,
sstable, resourceIndexes);
+ resources.add(opened);
+ sstableResources.add(opened);
+ }
+
+ QueryContext queryContext = new QueryContext(null, Long.MAX_VALUE);
+ try (KeyRangeIterator finalMatches = executePlan(metadata,
+ plan,
+ sstableResources,
+ queryContext,
+ sparkRangeFilter))
+ {
+ finalMatches.setOnClose(resources::close);
Review Comment:
Worth evaluating if implementations of `KeyRangeIterator` honor the
`close()` operation as expected here (eg. `EmptyRangeIterator `,
`PostingListRangeIterator `). We might want to explicitly close the resources
in a `finally` in the method that opened them instead.
##########
cassandra-six-zero-bridge/src/main/java/org/apache/cassandra/bridge/CassandraBridgeImplementation.java:
##########
@@ -227,6 +232,186 @@ public StreamScanner<RowData>
getCompactionScanner(@NotNull CqlTable table,
}));
}
+ @Override
+ public StreamScanner<RowData> getCompactionScanner(@NotNull CqlTable table,
+ @NotNull Partitioner
partitioner,
+ @NotNull
SSTablesSupplier ssTables,
+ @Nullable
SparkRangeFilter sparkRangeFilter,
+ @NotNull
Collection<PartitionKeyFilter> partitionKeyFilters,
+ @NotNull
SSTableTimeRangeFilter sstableTimeRangeFilter,
+ @Nullable
PruneColumnFilter columnFilter,
+ @NotNull TimeProvider
timeProvider,
+ boolean readIndexOffset,
+ boolean
useIncrementalRepair,
+ @NotNull Stats stats,
+ @NotNull
List<SaiFilter> saiFilters,
+ int
saiMaxCandidateTokens)
+ {
+ // A complete partition-key lookup is already more selective and
cheaper than consulting SAI.
+ if (!partitionKeyFilters.isEmpty() || saiFilters.isEmpty())
+ {
+ return getCompactionScanner(table, partitioner, ssTables,
sparkRangeFilter, partitionKeyFilters,
+ sstableTimeRangeFilter, columnFilter,
timeProvider, readIndexOffset,
+ useIncrementalRepair, stats);
+ }
+
+ // NOTE: Need to use SchemaBuilder to init keyspace if not already set
in Cassandra Schema instance
+ SchemaBuilder schemaBuilder = new SchemaBuilder(table, partitioner);
+ TableMetadata metadata = schemaBuilder.tableMetaData();
+
+ // Resolve each sstable token span before opening any SAI resources so
data files outside this
+ // Spark worker's token range are ignored.
+ Set<SaiSSTableReference> references = ssTables.openAll((sstable,
isRepairPrimary) -> {
+ if (sparkRangeFilter == null)
+ {
+ return new SaiSSTableReference(sstable, isRepairPrimary);
+ }
+ Pair<BigInteger, BigInteger> tokens = null;
+ if (sstable.isBigFormat())
+ {
+ SSTableSummary summary =
getSSTableSummary(metadata.partitioner,
+ sstable,
+
metadata.params.minIndexInterval,
+
metadata.params.maxIndexInterval);
+ tokens = Pair.of(summary.firstToken, summary.lastToken);
+ }
+ else
+ {
+ Pair<DecoratedKey, DecoratedKey> keys =
ReaderUtils.keysFromIndex(metadata.partitioner, sstable);
+ if (keys != null)
+ {
+ tokens =
Pair.of(ReaderUtils.tokenToBigInteger(keys.left.getToken()),
+
ReaderUtils.tokenToBigInteger(keys.right.getToken()));
+ }
+ }
+ if (tokens == null)
+ {
+ return new SaiSSTableReference(sstable, isRepairPrimary);
+ }
+ return new SaiSSTableReference(sstable,
+ isRepairPrimary,
+ tokens.left,
+ tokens.right,
+ sparkRangeFilter);
+ });
+ if (references.isEmpty())
+ {
+ return EmptyStreamScanner.INSTANCE;
+ }
+
+ Set<SSTable> sstables = references.stream()
+ .filter(p -> !p.ignore)
+ .map(reference -> reference.sstable)
+ .collect(Collectors.toSet());
+ Optional<CandidateTokens> candidates =
SaiIndexReader.findCandidateTokens(metadata,
+
sstables,
+
saiFilters,
+
sparkRangeFilter,
+
saiMaxCandidateTokens);
+ if (candidates.isEmpty())
+ {
+ // No candidate token ranges selected form SAI filter, use
standard full-table scan.
+ return openCompactionScanner(metadata, partitioner, timeProvider,
references, sparkRangeFilter,
+ Collections.emptyList(), null,
sstableTimeRangeFilter, columnFilter,
+ readIndexOffset,
useIncrementalRepair, stats);
+ }
+
+ CandidateTokens candidateTokens = candidates.get();
+ if (candidateTokens.isEmpty())
+ {
+ return EmptyStreamScanner.INSTANCE;
+ }
+
+ return openCompactionScanner(metadata, partitioner, timeProvider,
references, sparkRangeFilter,
+ Collections.emptyList(), candidateTokens,
sstableTimeRangeFilter,
+ columnFilter, readIndexOffset,
useIncrementalRepair, stats);
+ }
+
+ @NotNull
+ private static StreamScanner<RowData> openCompactionScanner(@NotNull
TableMetadata metadata,
+ @NotNull
Partitioner partitioner,
+ @NotNull
TimeProvider timeProvider,
+ @NotNull
Set<SaiSSTableReference> references,
+ @Nullable
SparkRangeFilter sparkRangeFilter,
+ @NotNull
Collection<PartitionKeyFilter> partitionKeyFilters,
+ @Nullable
CandidateTokens candidateTokens,
+ @NotNull
SSTableTimeRangeFilter sstableTimeRangeFilter,
+ @Nullable
PruneColumnFilter columnFilter,
+ boolean
readIndexOffset,
+ boolean
useIncrementalRepair,
+ @NotNull Stats
stats)
+ {
+ Set<org.apache.cassandra.spark.reader.SSTableReader> readers =
references.stream()
+ .map(reference -> {
+ try
+ {
+ return
org.apache.cassandra.spark.reader.SSTableReader.builder(metadata,
reference.sstable)
Review Comment:
The readers are opened sequentially in the task thread compared to
concurrently in trunk where `ssTables.openAll()` submits one task per SSTable
to an executor.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]