You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ma...@apache.org on 2023/07/26 16:27:27 UTC
[cassandra] 03/22: Literal on-disk index and index write path (#9)
This is an automated email from the ASF dual-hosted git repository.
maedhroz pushed a commit to branch cep-7-sai
in repository https://gitbox.apache.org/repos/asf/cassandra.git
commit 9492860a03b9919372bba456a9c7b26f266db4bc
Author: Mike Adamson <mi...@gmail.com>
AuthorDate: Thu Apr 13 17:23:13 2023 +0100
Literal on-disk index and index write path (#9)
This commit contains the following additions
to SAI:
- The index write path and index building
based around StorageAttachedIndexBuilder
and StorageAttachedIndexWriter
- The on-disk index versioning using the
SSTable Descriptor analog IndexDescriptor
with Version and OnDiskFormat
- The literal on-disk index using the
LiteralIndexWriter
patch by Mike Adamson; reviewed by Caleb Rackliffe and Andres de la Peña for CASSANDRA-18062
Co-authored-by: Mike Adamson <mi...@gmail.com>
Co-authored-by: Caleb Rackliffe <ca...@gmail.com>
Co-authored-by: Andres de la Peña <a....@gmail.com>
Co-authored-by: Piotr Kołaczkowski <pk...@gmail.com>
Co-authored-by: Jason Rutherglen <ja...@gmail.com>
---
conf/cassandra.yaml | 6 +
.../config/CassandraRelevantProperties.java | 2 +
src/java/org/apache/cassandra/config/Config.java | 2 +
.../cassandra/config/DatabaseDescriptor.java | 7 +
.../config/StorageAttachedIndexOptions.java | 44 +
.../statements/schema/CreateIndexStatement.java | 80 +-
src/java/org/apache/cassandra/db/ReadCommand.java | 1 -
.../org/apache/cassandra/db/lifecycle/Tracker.java | 6 +
.../cassandra/db/memtable/AbstractMemtable.java | 23 +
.../org/apache/cassandra/db/memtable/Flushing.java | 7 +
.../org/apache/cassandra/db/memtable/Memtable.java | 5 +
.../apache/cassandra/index/sai/IndexContext.java | 306 ++++---
.../apache/cassandra/index/sai/QueryContext.java | 11 +-
.../apache/cassandra/index/sai/SSTableContext.java | 190 +++++
.../cassandra/index/sai/SSTableContextManager.java | 148 ++++
.../cassandra/index/sai/StorageAttachedIndex.java | 241 +++++-
.../index/sai/StorageAttachedIndexBuilder.java | 352 ++++++++
.../index/sai/StorageAttachedIndexGroup.java | 214 ++++-
.../index/sai/analyzer/AbstractAnalyzer.java | 2 -
.../index/sai/disk/IndexSearchResultIterator.java | 141 ++++
.../index/sai/disk/PerColumnIndexWriter.java | 36 +-
.../PerSSTableIndexWriter.java} | 25 +-
.../cassandra/index/sai/disk/PrimaryKeyMap.java | 76 ++
.../cassandra/index/sai/disk/RowMapping.java | 175 ++++
.../cassandra/index/sai/disk/SSTableIndex.java | 260 ++++++
.../index/sai/disk/StorageAttachedIndexWriter.java | 258 ++++++
.../index/sai/disk/format/IndexComponent.java | 88 ++
.../index/sai/disk/format/IndexDescriptor.java | 433 ++++++++++
.../index/sai/disk/format/OnDiskFormat.java | 165 ++++
.../cassandra/index/sai/disk/format/Version.java | 154 ++++
.../index/sai/disk/io/IndexFileUtils.java | 148 ++++
.../index/sai/disk/io/IndexInputReader.java | 141 ++++
.../index/sai/disk/io/IndexOutputWriter.java | 121 +++
.../index/sai/disk/io/RAMIndexOutput.java | 86 ++
.../sai/disk/io/SeekingRandomAccessInput.java | 70 ++
.../cassandra/index/sai/disk/v1/DirectReaders.java | 250 ++++++
.../cassandra/index/sai/disk/v1/LongArray.java | 90 ++
.../index/sai/disk/v1/MemtableIndexWriter.java | 179 ++++
.../index/sai/disk/v1/MetadataSource.java | 96 +++
.../index/sai/disk/v1/MetadataWriter.java | 85 ++
.../index/sai/disk/v1/PerColumnIndexFiles.java | 81 ++
.../index/sai/disk/v1/RowAwarePrimaryKeyMap.java | 198 +++++
.../cassandra/index/sai/disk/v1/SAICodecUtils.java | 259 ++++++
.../index/sai/disk/v1/SSTableComponentsWriter.java | 90 ++
.../index/sai/disk/v1/SSTableIndexWriter.java | 336 ++++++++
.../index/sai/disk/v1/V1OnDiskFormat.java | 247 ++++++
.../index/sai/disk/v1/V1SSTableIndex.java | 204 +++++
.../disk/v1/bitpack/AbstractBlockPackedReader.java | 68 ++
.../disk/v1/bitpack/AbstractBlockPackedWriter.java | 124 +++
.../sai/disk/v1/bitpack/BlockPackedReader.java | 128 +++
.../sai/disk/v1/bitpack/BlockPackedWriter.java | 77 ++
.../v1/bitpack/MonotonicBlockPackedReader.java | 120 +++
.../v1/bitpack/MonotonicBlockPackedWriter.java | 87 ++
.../v1/bitpack/NumericValuesMeta.java} | 29 +-
.../sai/disk/v1/bitpack/NumericValuesWriter.java | 100 +++
.../sai/disk/v1/postings/MergePostingList.java | 130 +++
.../disk/v1/postings/PostingListRangeIterator.java | 171 ++++
.../index/sai/disk/v1/postings/PostingsReader.java | 366 ++++++++
.../index/sai/disk/v1/postings/PostingsWriter.java | 316 +++++++
.../sai/disk/v1/segment/IndexSegmentSearcher.java | 93 +++
.../v1/segment/IndexSegmentSearcherContext.java | 55 ++
.../v1/segment/LiteralIndexSegmentSearcher.java | 109 +++
.../v1/segment/LiteralIndexSegmentTermsReader.java | 175 ++++
.../index/sai/disk/v1/segment/Segment.java | 129 +++
.../index/sai/disk/v1/segment/SegmentBuilder.java | 260 ++++++
.../index/sai/disk/v1/segment/SegmentMetadata.java | 357 ++++++++
.../sai/disk/v1/sortedterms/SortedTermsMeta.java | 48 ++
.../sai/disk/v1/sortedterms/SortedTermsReader.java | 234 ++++++
.../sai/disk/v1/sortedterms/SortedTermsWriter.java | 214 +++++
.../sai/disk/v1/sortedterms/package-info.java | 58 ++
.../index/sai/disk/v1/trie/LiteralIndexWriter.java | 111 +++
.../index/sai/disk/v1/trie/TriePrefixSearcher.java | 98 +++
.../disk/v1/trie/TrieTermsDictionaryReader.java | 93 +++
.../disk/v1/trie/TrieTermsDictionaryWriter.java | 99 +++
.../KeyRangeConcatIterator.java | 8 +-
.../KeyRangeIntersectionIterator.java | 6 +-
.../sai/{utils => iterators}/KeyRangeIterator.java | 3 +-
.../KeyRangeUnionIterator.java | 3 +-
.../index/sai/memory/ByteSliceReader.java | 124 +++
.../index/sai/memory/InMemoryKeyRangeIterator.java | 2 +-
.../cassandra/index/sai/memory/MemtableIndex.java | 2 +-
.../index/sai/memory/MemtableIndexManager.java | 157 ++++
.../index/sai/memory/MemtableTermsIterator.java | 135 +++
.../index/sai/memory/RAMPostingSlices.java | 137 +++
.../index/sai/memory/RAMStringIndexer.java | 167 ++++
.../index/sai/memory/TrieMemoryIndex.java | 2 +-
.../index/sai/metrics/ColumnQueryMetrics.java | 91 ++
...bleStateMetrics.java => IndexGroupMetrics.java} | 16 +-
.../cassandra/index/sai/metrics/IndexMetrics.java | 35 +-
.../sai/metrics/MulticastQueryEventListeners.java | 90 ++
.../index/sai/metrics/QueryEventListener.java | 75 ++
.../index/sai/metrics/TableQueryMetrics.java | 55 +-
.../index/sai/metrics/TableStateMetrics.java | 17 +-
.../cassandra/index/sai/plan/Expression.java | 5 +
.../apache/cassandra/index/sai/plan/Operation.java | 2 +-
.../cassandra/index/sai/plan/QueryController.java | 41 +-
.../cassandra/index/sai/plan/QueryViewBuilder.java | 192 +++++
.../sai/plan/StorageAttachedIndexQueryPlan.java | 4 +-
.../sai/plan/StorageAttachedIndexSearcher.java | 11 +-
.../index/sai/postings/OrdinalPostingList.java | 18 +-
.../index/sai/postings/PeekablePostingList.java | 116 +++
.../cassandra/index/sai/postings/PostingList.java | 60 ++
.../index/sai/utils/NamedMemoryLimiter.java | 79 ++
.../cassandra/index/sai/utils/PrimaryKey.java | 348 +++++---
.../index/sai/utils/PrimaryKeyFactory.java | 57 --
.../cassandra/index/sai/utils/TermsIterator.java | 46 +
.../apache/cassandra/index/sai/utils/TypeUtil.java | 9 +
.../cassandra/index/sai/view/IndexViewManager.java | 150 ++++
.../cassandra/index/sai/view/RangeTermTree.java | 134 +++
.../org/apache/cassandra/index/sai/view/View.java | 100 +++
...ystemView.java => ColumnIndexesSystemView.java} | 6 +-
.../sai/virtual/SSTableIndexesSystemView.java | 132 +++
.../index/sai/virtual/SegmentsSystemView.java | 125 +++
.../sai/virtual/StorageAttachedIndexTables.java | 6 +-
.../io/compress/CompressedSequentialWriter.java | 35 +
.../apache/cassandra/io/sstable/Descriptor.java | 10 +-
.../org/apache/cassandra/io/sstable/SSTable.java | 40 +
.../cassandra/io/sstable/format/TOCComponent.java | 11 +
.../apache/cassandra/io/tries/ValueIterator.java | 46 +-
src/java/org/apache/cassandra/io/tries/Walker.java | 76 +-
.../apache/cassandra/io/util/DataOutputPlus.java | 9 +-
.../org/apache/cassandra/io/util/PageAware.java | 2 +-
.../io/util/TailOverridingRebufferer.java | 2 +-
.../utils/bytecomparable/ByteComparable.java | 5 +
.../utils/concurrent/LightweightRecycler.java | 2 +-
.../cassandra/utils/concurrent/ThreadLocals.java | 2 +-
.../cassandra/distributed/test/sai/SAIUtil.java | 4 +-
.../config/DatabaseDescriptorRefTest.java | 8 +-
.../config/StorageAttachedIndexOptionsTest.java | 44 +
.../unit/org/apache/cassandra/index/StubIndex.java | 3 +-
.../cassandra/index/sai/IndexingSchemaLoader.java | 173 ----
.../org/apache/cassandra/index/sai/SAITester.java | 651 ++++++++++++++-
.../index/sai/cql/AbstractQueryTester.java | 4 +-
.../cassandra/index/sai/cql/BaseDataModel.java | 84 +-
.../index/sai/cql/CollectionIndexingTest.java | 2 +-
.../index/sai/cql/DecimalLargeValueTest.java | 6 +-
.../cassandra/index/sai/cql/IndexQuerySupport.java | 52 +-
.../sai/cql/MixedIndexImplementationsTest.java | 2 +
.../index/sai/cql/StorageAttachedIndexDDLTest.java | 924 +++++++++++++++++++--
.../cassandra/index/sai/cql/types/AsciiTest.java | 2 +-
.../cassandra/index/sai/cql/types/BigintTest.java | 2 +-
.../cassandra/index/sai/cql/types/BooleanTest.java | 2 +-
.../cassandra/index/sai/cql/types/DataSet.java | 20 +-
.../cassandra/index/sai/cql/types/DateTest.java | 2 +-
.../cassandra/index/sai/cql/types/DecimalTest.java | 2 +-
.../cassandra/index/sai/cql/types/DoubleTest.java | 2 +-
.../cassandra/index/sai/cql/types/FloatTest.java | 2 +-
.../index/sai/cql/types/IndexingTypeSupport.java | 51 +-
.../cassandra/index/sai/cql/types/InetTest.java | 2 +-
.../cassandra/index/sai/cql/types/IntTest.java | 2 +-
.../sai/cql/types/NumericTypeSortingTest.java | 20 +-
.../cassandra/index/sai/cql/types/QuerySet.java | 126 ++-
.../index/sai/cql/types/SmallintTest.java | 2 +-
.../cassandra/index/sai/cql/types/TextTest.java | 2 +-
.../cassandra/index/sai/cql/types/TimeTest.java | 2 +-
.../index/sai/cql/types/TimestampTest.java | 2 +-
.../index/sai/cql/types/TimeuuidTest.java | 2 +-
.../cassandra/index/sai/cql/types/TinyintTest.java | 2 +-
.../cassandra/index/sai/cql/types/UuidTest.java | 2 +-
.../cassandra/index/sai/cql/types/VarintTest.java | 2 +-
.../cql/types/collections/CollectionDataSet.java | 39 +-
.../collections/lists/FrozenListAsciiTest.java | 2 +-
.../collections/lists/FrozenListDecimalTest.java | 2 +-
.../types/collections/lists/FrozenListIntTest.java | 2 +-
.../collections/lists/FrozenListVarintTest.java | 2 +-
.../cql/types/collections/lists/ListAsciiTest.java | 2 +-
.../types/collections/lists/ListBigintTest.java | 2 +-
.../cql/types/collections/lists/ListDateTest.java | 2 +-
.../types/collections/lists/ListDecimalTest.java | 2 +-
.../types/collections/lists/ListDoubleTest.java | 2 +-
.../cql/types/collections/lists/ListFloatTest.java | 2 +-
.../lists/ListFrozenCollectionTest.java | 2 +-
.../cql/types/collections/lists/ListInetTest.java | 2 +-
.../cql/types/collections/lists/ListIntTest.java | 2 +-
.../types/collections/lists/ListSmallintTest.java | 2 +-
.../cql/types/collections/lists/ListTextTest.java | 2 +-
.../cql/types/collections/lists/ListTimeTest.java | 2 +-
.../types/collections/lists/ListTimestampTest.java | 2 +-
.../types/collections/lists/ListTimeuuidTest.java | 2 +-
.../types/collections/lists/ListTinyintTest.java | 2 +-
.../cql/types/collections/lists/ListUuidTest.java | 2 +-
.../types/collections/lists/ListVarintTest.java | 2 +-
.../types/collections/maps/FrozenMapAsciiTest.java | 2 +-
.../collections/maps/FrozenMapDecimalTest.java | 2 +-
.../types/collections/maps/FrozenMapIntTest.java | 2 +-
.../collections/maps/FrozenMapVarintTest.java | 2 +-
.../cql/types/collections/maps/MapAsciiTest.java | 2 +-
.../cql/types/collections/maps/MapBigintTest.java | 2 +-
.../cql/types/collections/maps/MapDateTest.java | 2 +-
.../cql/types/collections/maps/MapDecimalTest.java | 2 +-
.../cql/types/collections/maps/MapDoubleTest.java | 2 +-
.../collections/maps/MapEntriesAsciiTest.java | 2 +-
.../maps/MapEntriesFrozenCollectionTest.java | 2 +-
.../types/collections/maps/MapEntriesIntTest.java | 2 +-
.../collections/maps/MapEntriesVarintTest.java | 2 +-
.../cql/types/collections/maps/MapFloatTest.java | 2 +-
.../collections/maps/MapFrozenCollectionTest.java | 2 +-
.../cql/types/collections/maps/MapInetTest.java | 2 +-
.../sai/cql/types/collections/maps/MapIntTest.java | 2 +-
.../types/collections/maps/MapKeysAsciiTest.java | 2 +-
.../maps/MapKeysFrozenCollectionTest.java | 2 +-
.../cql/types/collections/maps/MapKeysIntTest.java | 2 +-
.../types/collections/maps/MapKeysVarintTest.java | 2 +-
.../types/collections/maps/MapSmallintTest.java | 2 +-
.../cql/types/collections/maps/MapTextTest.java | 2 +-
.../cql/types/collections/maps/MapTimeTest.java | 2 +-
.../types/collections/maps/MapTimestampTest.java | 2 +-
.../types/collections/maps/MapTimeuuidTest.java | 2 +-
.../cql/types/collections/maps/MapTinyintTest.java | 2 +-
.../cql/types/collections/maps/MapUuidTest.java | 2 +-
.../types/collections/maps/MapValuesAsciiTest.java | 2 +-
.../maps/MapValuesFrozenCollectionTest.java | 2 +-
.../types/collections/maps/MapValuesIntTest.java | 2 +-
.../collections/maps/MapValuesVarintTest.java | 2 +-
.../cql/types/collections/maps/MapVarintTest.java | 2 +-
.../types/collections/maps/MultiMapAsciiTest.java | 2 +-
.../types/collections/maps/MultiMapIntTest.java | 2 +-
.../types/collections/maps/MultiMapVarintTest.java | 2 +-
.../types/collections/sets/FrozenSetAsciiTest.java | 2 +-
.../collections/sets/FrozenSetDecimalTest.java | 2 +-
.../types/collections/sets/FrozenSetIntTest.java | 2 +-
.../collections/sets/FrozenSetVarintTest.java | 2 +-
.../cql/types/collections/sets/SetAsciiTest.java | 2 +-
.../cql/types/collections/sets/SetBigintTest.java | 2 +-
.../cql/types/collections/sets/SetDateTest.java | 2 +-
.../cql/types/collections/sets/SetDecimalTest.java | 2 +-
.../cql/types/collections/sets/SetDoubleTest.java | 2 +-
.../cql/types/collections/sets/SetFloatTest.java | 2 +-
.../collections/sets/SetFrozenCollectionTest.java | 2 +-
.../cql/types/collections/sets/SetInetTest.java | 2 +-
.../sai/cql/types/collections/sets/SetIntTest.java | 2 +-
.../types/collections/sets/SetSmallintTest.java | 2 +-
.../cql/types/collections/sets/SetTextTest.java | 2 +-
.../cql/types/collections/sets/SetTimeTest.java | 2 +-
.../types/collections/sets/SetTimestampTest.java | 2 +-
.../types/collections/sets/SetTimeuuidTest.java | 2 +-
.../cql/types/collections/sets/SetTinyintTest.java | 2 +-
.../cql/types/collections/sets/SetUuidTest.java | 2 +-
.../cql/types/collections/sets/SetVarintTest.java | 2 +-
.../types/multicell/FrozenTupleCollectionTest.java | 2 +-
.../cql/types/multicell/FrozenTupleDataSet.java | 2 +-
.../sai/cql/types/multicell/FrozenTupleTest.java | 2 +-
.../cql/types/multicell/FrozenTupleTupleTest.java | 2 +-
.../types/multicell/FrozenUDTCollectionTest.java | 2 +-
.../sai/cql/types/multicell/FrozenUDTDataSet.java | 6 +-
.../sai/cql/types/multicell/FrozenUDTTest.java | 2 +-
.../index/sai/cql/types/multicell/TupleTest.java | 2 +-
.../cassandra/index/sai/disk/ArrayPostingList.java | 92 ++
.../cassandra/index/sai/disk/NodeStartupTest.java | 407 +++++++++
.../index/sai/disk/SelectiveIntersectionTest.java | 159 ++++
.../index/sai/disk/SingleNodeQueryFailureTest.java | 111 +++
.../index/sai/disk/format/IndexDescriptorTest.java | 122 +++
.../index/sai/disk/format/VersionTest.java | 50 ++
.../index/sai/disk/io/TrackingIndexFileUtils.java | 116 +++
.../index/sai/disk/v1/InvertedIndexBuilder.java | 61 ++
.../sai/disk/v1/InvertedIndexSearcherTest.java | 218 +++++
.../cassandra/index/sai/disk/v1/MetadataTest.java | 189 +++++
.../index/sai/disk/v1/SAICodecUtilsTest.java | 390 +++++++++
.../index/sai/disk/v1/SegmentFlushTest.java | 222 +++++
.../cassandra/index/sai/disk/v1/SegmentTest.java | 165 ++++
.../cassandra/index/sai/disk/v1/SorterTest.java | 71 ++
.../index/sai/disk/v1/TermsReaderTest.java | 180 ++++
.../cassandra/index/sai/disk/v1/TermsScanner.java | 96 +++
.../sai/disk/v1/bitpack/NumericValuesTest.java | 125 +++
.../sai/disk/v1/postings/MergePostingListTest.java | 333 ++++++++
.../index/sai/disk/v1/postings/PostingsTest.java | 394 +++++++++
.../disk/v1/postings/ScanningPostingsReader.java | 45 +
.../sai/disk/v1/sortedterms/SortedTermsTest.java | 415 +++++++++
.../sai/disk/v1/trie/TriePrefixSearcherTest.java | 160 ++++
.../sai/disk/v1/trie/TrieTermsDictionaryTest.java | 171 ++++
.../index/sai/disk/v1/trie/TrieTermsIterator.java | 82 ++
.../index/sai/functional/CompactionTest.java | 339 ++++++++
.../index/sai/functional/DiskSpaceTest.java | 56 ++
.../index/sai/functional/DropTableTest.java | 106 +++
.../index/sai/functional/FailureTest.java | 125 +++
.../index/sai/functional/FlushingTest.java | 60 ++
.../index/sai/functional/GroupComponentsTest.java | 120 +++
.../index/sai/functional/NodeRestartTest.java | 180 ++++
.../index/sai/functional/SnapshotTest.java | 167 ++++
.../AbstractKeyRangeIteratorTester.java | 9 +-
.../KeyRangeConcatIteratorTest.java | 11 +-
.../KeyRangeIntersectionIteratorTest.java | 4 +-
.../KeyRangeUnionIteratorTest.java | 76 +-
.../sai/{utils => iterators}/LongIterator.java | 7 +-
.../AbstractInMemoryKeyRangeIteratorTester.java | 7 +-
.../sai/memory/InMemoryKeyRangeIteratorTest.java | 2 +-
.../PriorityInMemoryKeyRangeIteratorTest.java | 2 +-
.../index/sai/memory/RAMPostingSlicesTest.java | 80 ++
.../index/sai/memory/RAMStringIndexerTest.java | 115 +++
.../index/sai/memory/TrieMemoryIndexTest.java | 2 +-
.../FinalSegmentFlushingFailureTest.java} | 14 +-
.../index/sai/metrics/IndexGroupMetricsTest.java | 92 ++
.../index/sai/metrics/IndexMetricsTest.java | 8 +-
.../index/sai/metrics/QueryMetricsTest.java | 16 +-
.../sai/metrics/SegmentFlushingFailureTester.java | 217 +++++
.../index/sai/metrics/StateMetricsTest.java | 14 +-
.../TinySegmentFlushingFailureTest.java} | 20 +-
.../cassandra/index/sai/plan/OperationTest.java | 179 ++--
.../sai/postings/PeekablePostingListTest.java | 73 ++
.../index/sai/utils/IndexInputLeakDetector.java | 62 ++
.../index/sai/utils/NamedMemoryLimiterTest.java | 82 ++
.../cassandra/index/sai/utils/PrimaryKeyTest.java | 28 +-
.../index/sai/utils/SAIRandomizedTester.java | 89 +-
.../cassandra/index/sai/utils/TypeUtilTest.java | 251 ++++++
.../index/sai/virtual/IndexesSystemViewTest.java | 70 +-
.../index/sai/virtual/SSTablesSystemViewTest.java | 201 +++++
.../index/sai/virtual/SegmentsSystemViewTest.java | 220 +++++
.../cassandra/io/tries/AbstractTrieTestBase.java | 6 +
.../org/apache/cassandra/io/tries/WalkerTest.java | 2 +-
309 files changed, 21598 insertions(+), 1305 deletions(-)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 30fb7eac8b..9e79d3525b 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -1545,6 +1545,12 @@ transparent_data_encryption_options:
store_type: JCEKS
key_password: cassandra
+# Storage Attached Indexing options.
+# sai_options:
+ ## Total permitted memory allowed for writing SAI index segments. This memory
+ ## is split between all SAI indexes being built so more indexes will mean smaller
+ ## segment sizes.
+ # segment_write_buffer_size: 1024MiB
#####################
# SAFETY THRESHOLDS #
diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
index 6fba0d20e7..7a0cfafc90 100644
--- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
+++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
@@ -419,6 +419,8 @@ public enum CassandraRelevantProperties
// SAI specific properties
/** Controls the maximum number of index query intersections that will take part in a query */
SAI_INTERSECTION_CLAUSE_LIMIT("cassandra.sai.intersection.clause.limit", "2"),
+ /** Latest version to be used for SAI index writing */
+ SAI_LATEST_VERSION("cassandra.sai.latest_version", "aa"),
/** Defines how often schema definitions are pulled from the other nodes */
SCHEMA_PULL_INTERVAL_MS("cassandra.schema_pull_interval_ms", "60000"),
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 21d307c601..47b197cf9b 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -777,6 +777,8 @@ public class Config
*/
public volatile double range_tombstone_list_growth_factor = 1.5;
+ public StorageAttachedIndexOptions sai_options = new StorageAttachedIndexOptions();
+
/**
* @deprecated migrate to {@link DatabaseDescriptor#isClientInitialized()}
*/
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index cb3fd9159f..043cb9b5bc 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -967,6 +967,8 @@ public class DatabaseDescriptor
if (conf.dump_heap_on_uncaught_exception && DatabaseDescriptor.getHeapDumpPath() == null)
throw new ConfigurationException(String.format("Invalid configuration. Heap dump is enabled but cannot create heap dump output path: %s.", conf.heap_dump_path != null ? conf.heap_dump_path : "null"));
+
+ conf.sai_options.validate();
}
@VisibleForTesting
@@ -4756,4 +4758,9 @@ public class DatabaseDescriptor
{
return conf != null ? conf.default_compaction : null;
}
+
+ public static DataStorageSpec.IntMebibytesBound getSAISegmentWriteBufferSpace()
+ {
+ return conf.sai_options.segment_write_buffer_size;
+ }
}
diff --git a/src/java/org/apache/cassandra/config/StorageAttachedIndexOptions.java b/src/java/org/apache/cassandra/config/StorageAttachedIndexOptions.java
new file mode 100644
index 0000000000..18bf3506fe
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/StorageAttachedIndexOptions.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.config;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+public class StorageAttachedIndexOptions
+{
+ public static final int DEFAULT_SEGMENT_BUFFER_MB = 1024;
+
+ @VisibleForTesting
+ public static final int MAXIMUM_SEGMENT_BUFFER_MB = 32768;
+
+ @VisibleForTesting
+ public static final String INVALID_BUFFER_SIZE_ERROR = "Invalid value for segment_write_buffer_size. " +
+ "Value must be a positive integer less than " + MAXIMUM_SEGMENT_BUFFER_MB + "MiB";
+
+ public DataStorageSpec.IntMebibytesBound segment_write_buffer_size = new DataStorageSpec.IntMebibytesBound(DEFAULT_SEGMENT_BUFFER_MB);
+
+ public void validate()
+ {
+ if (segment_write_buffer_size.toMebibytes() > MAXIMUM_SEGMENT_BUFFER_MB)
+ {
+ throw new ConfigurationException(INVALID_BUFFER_SIZE_ERROR);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
index 73a7b6aac6..73b66b738d 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
@@ -48,8 +48,35 @@ import static com.google.common.collect.Iterables.tryFind;
public final class CreateIndexStatement extends AlterSchemaStatement
{
+ public static final String SASI_INDEX_DISABLED = "SASI indexes are disabled. Enable in cassandra.yaml to use.";
+ public static final String KEYSPACE_DOES_NOT_EXIST = "Keyspace '%s' doesn't exist";
+ public static final String TABLE_DOES_NOT_EXIST = "Table '%s' doesn't exist";
+ public static final String COUNTER_TABLES_NOT_SUPPORTED = "Secondary indexes on counter tables aren't supported";
+ public static final String MATERIALIZED_VIEWS_NOT_SUPPORTED = "Secondary indexes on materialized views aren't supported";
+ public static final String TRANSIENTLY_REPLICATED_KEYSPACE_NOT_SUPPORTED = "Secondary indexes are not supported on transiently replicated keyspaces";
+ public static final String CUSTOM_CREATE_WITHOUT_COLUMN = "Only CUSTOM indexes can be created without specifying a target column";
+ public static final String CUSTOM_MULTIPLE_COLUMNS = "Only CUSTOM indexes support multiple columns";
+ public static final String DUPLICATE_TARGET_COLUMN = "Duplicate column '%s' in index target list";
+ public static final String COLUMN_DOES_NOT_EXIST = "Column '%s' doesn't exist";
public static final String INVALID_CUSTOM_INDEX_TARGET = "Column '%s' is longer than the permissible name length of %d characters or" +
" contains non-alphanumeric-underscore characters";
+ public static final String COLLECTIONS_WITH_DURATIONS_NOT_SUPPORTED = "Secondary indexes are not supported on collections containing durations";
+ public static final String TUPLES_WITH_DURATIONS_NOT_SUPPORTED = "Secondary indexes are not supported on tuples containing durations";
+ public static final String DURATIONS_NOT_SUPPORTED = "Secondary indexes are not supported on duration columns";
+ public static final String UDTS_WITH_DURATIONS_NOT_SUPPORTED = "Secondary indexes are not supported on UDTs containing durations";
+ public static final String PRIMARY_KEY_IN_COMPACT_STORAGE = "Secondary indexes are not supported on PRIMARY KEY columns in COMPACT STORAGE tables";
+ public static final String COMPACT_COLUMN_IN_COMPACT_STORAGE = "Secondary indexes are not supported on compact value column of COMPACT STORAGE tables";
+ public static final String ONLY_PARTITION_KEY = "Cannot create secondary index on the only partition key column %s";
+ public static final String CREATE_ON_FROZEN_COLUMN = "Cannot create %s() index on frozen column %s. Frozen collections are immutable and must be fully " +
+ "indexed by using the 'full(%s)' modifier";
+ public static final String FULL_ON_FROZEN_COLLECTIONS = "full() indexes can only be created on frozen collections";
+ public static final String NON_COLLECTION_SIMPLE_INDEX = "Cannot create %s() index on %s. Non-collection columns only support simple indexes";
+ public static final String CREATE_WITH_NON_MAP_TYPE = "Cannot create index on %s of column %s with non-map type";
+ public static final String CREATE_ON_NON_FROZEN_UDT = "Cannot create index on non-frozen UDT column %s";
+ public static final String INDEX_ALREADY_EXISTS = "Index '%s' already exists";
+ public static final String INDEX_DUPLICATE_OF_EXISTING = "Index %s is a duplicate of existing index %s";
+ public static final String KEYSPACE_DOES_NOT_MATCH_TABLE = "Keyspace name '%s' doesn't match table name '%s'";
+ public static final String KEYSPACE_DOES_NOT_MATCH_INDEX = "Keyspace name '%s' doesn't match index name '%s'";
private final String indexName;
private final String tableName;
@@ -90,32 +117,32 @@ public final class CreateIndexStatement extends AlterSchemaStatement
Guardrails.createSecondaryIndexesEnabled.ensureEnabled("Creating secondary indexes", state);
if (attrs.isCustom && attrs.customClass.equals(SASIIndex.class.getName()) && !DatabaseDescriptor.getSASIIndexesEnabled())
- throw new InvalidRequestException("SASI indexes are disabled. Enable in cassandra.yaml to use.");
+ throw new InvalidRequestException(SASI_INDEX_DISABLED);
KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
if (null == keyspace)
- throw ire("Keyspace '%s' doesn't exist", keyspaceName);
+ throw ire(KEYSPACE_DOES_NOT_EXIST, keyspaceName);
TableMetadata table = keyspace.getTableOrViewNullable(tableName);
if (null == table)
- throw ire("Table '%s' doesn't exist", tableName);
+ throw ire(TABLE_DOES_NOT_EXIST, tableName);
if (null != indexName && keyspace.hasIndex(indexName))
{
if (ifNotExists)
return schema;
- throw ire("Index '%s' already exists", indexName);
+ throw ire(INDEX_ALREADY_EXISTS, indexName);
}
if (table.isCounter())
- throw ire("Secondary indexes on counter tables aren't supported");
+ throw ire(COUNTER_TABLES_NOT_SUPPORTED);
if (table.isView())
- throw ire("Secondary indexes on materialized views aren't supported");
+ throw ire(MATERIALIZED_VIEWS_NOT_SUPPORTED);
if (Keyspace.open(table.keyspace).getReplicationStrategy().hasTransientReplicas())
- throw new InvalidRequestException("Secondary indexes are not supported on transiently replicated keyspaces");
+ throw new InvalidRequestException(TRANSIENTLY_REPLICATED_KEYSPACE_NOT_SUPPORTED);
// guardrails to limit number of secondary indexes per table.
Guardrails.secondaryIndexesPerTable.guard(table.indexes.size() + 1,
@@ -128,17 +155,17 @@ public final class CreateIndexStatement extends AlterSchemaStatement
List<IndexTarget> indexTargets = Lists.newArrayList(transform(rawIndexTargets, t -> t.prepare(table)));
if (indexTargets.isEmpty() && !attrs.isCustom)
- throw ire("Only CUSTOM indexes can be created without specifying a target column");
+ throw ire(CUSTOM_CREATE_WITHOUT_COLUMN);
if (indexTargets.size() > 1)
{
if (!attrs.isCustom)
- throw ire("Only CUSTOM indexes support multiple columns");
+ throw ire(CUSTOM_MULTIPLE_COLUMNS);
Set<ColumnIdentifier> columns = new HashSet<>();
for (IndexTarget target : indexTargets)
if (!columns.add(target.column))
- throw ire("Duplicate column '%s' in index target list", target.column);
+ throw ire(DUPLICATE_TARGET_COLUMN, target.column);
}
IndexMetadata.Kind kind = attrs.isCustom ? IndexMetadata.Kind.CUSTOM : IndexMetadata.Kind.COMPOSITES;
@@ -158,7 +185,7 @@ public final class CreateIndexStatement extends AlterSchemaStatement
if (ifNotExists)
return schema;
- throw ire("Index %s is a duplicate of existing index %s", index.name, equalIndex.name);
+ throw ire(INDEX_DUPLICATE_OF_EXISTING, index.name, equalIndex.name);
}
TableMetadata newTable = table.withSwapped(table.indexes.with(index));
@@ -181,7 +208,7 @@ public final class CreateIndexStatement extends AlterSchemaStatement
ColumnMetadata column = table.getColumn(target.column);
if (null == column)
- throw ire("Column '%s' doesn't exist", target.column);
+ throw ire(COLUMN_DOES_NOT_EXIST, target.column);
if ((kind == IndexMetadata.Kind.CUSTOM) && !SchemaConstants.isValidName(target.column.toString()))
throw ire(INVALID_CUSTOM_INDEX_TARGET, target.column, SchemaConstants.NAME_LENGTH);
@@ -189,44 +216,43 @@ public final class CreateIndexStatement extends AlterSchemaStatement
if (column.type.referencesDuration())
{
if (column.type.isCollection())
- throw ire("Secondary indexes are not supported on collections containing durations");
+ throw ire(COLLECTIONS_WITH_DURATIONS_NOT_SUPPORTED);
if (column.type.isTuple())
- throw ire("Secondary indexes are not supported on tuples containing durations");
+ throw ire(TUPLES_WITH_DURATIONS_NOT_SUPPORTED);
if (column.type.isUDT())
- throw ire("Secondary indexes are not supported on UDTs containing durations");
+ throw ire(UDTS_WITH_DURATIONS_NOT_SUPPORTED);
- throw ire("Secondary indexes are not supported on duration columns");
+ throw ire(DURATIONS_NOT_SUPPORTED);
}
if (table.isCompactTable())
{
TableMetadata.CompactTableMetadata compactTable = (TableMetadata.CompactTableMetadata) table;
if (column.isPrimaryKeyColumn())
- throw new InvalidRequestException("Secondary indexes are not supported on PRIMARY KEY columns in COMPACT STORAGE tables");
+ throw new InvalidRequestException(PRIMARY_KEY_IN_COMPACT_STORAGE);
if (compactTable.compactValueColumn.equals(column))
- throw new InvalidRequestException("Secondary indexes are not supported on compact value column of COMPACT STORAGE tables");
+ throw new InvalidRequestException(COMPACT_COLUMN_IN_COMPACT_STORAGE);
}
if (column.isPartitionKey() && table.partitionKeyColumns().size() == 1)
- throw ire("Cannot create secondary index on the only partition key column %s", column);
+ throw ire(ONLY_PARTITION_KEY, column);
if (column.type.isFrozenCollection() && target.type != Type.FULL)
- throw ire("Cannot create %s() index on frozen column %s. Frozen collections are immutable and must be fully " +
- "indexed by using the 'full(%s)' modifier", target.type, column, column);
+ throw ire(CREATE_ON_FROZEN_COLUMN, target.type, column, column);
if (!column.type.isFrozenCollection() && target.type == Type.FULL)
- throw ire("full() indexes can only be created on frozen collections");
+ throw ire(FULL_ON_FROZEN_COLLECTIONS);
if (!column.type.isCollection() && target.type != Type.SIMPLE)
- throw ire("Cannot create %s() index on %s. Non-collection columns only support simple indexes", target.type, column);
+ throw ire(NON_COLLECTION_SIMPLE_INDEX, target.type, column);
if (!(column.type instanceof MapType && column.type.isMultiCell()) && (target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES))
- throw ire("Cannot create index on %s of column %s with non-map type", target.type, column);
+ throw ire(CREATE_WITH_NON_MAP_TYPE, target.type, column);
if (column.type.isUDT() && column.type.isMultiCell())
- throw ire("Cannot create index on non-frozen UDT column %s", column);
+ throw ire(CREATE_ON_NON_FROZEN_UDT, column);
}
private String generateIndexName(KeyspaceMetadata keyspace, List<IndexTarget> targets)
@@ -286,10 +312,10 @@ public final class CreateIndexStatement extends AlterSchemaStatement
: indexName.hasKeyspace() ? indexName.getKeyspace() : state.getKeyspace();
if (tableName.hasKeyspace() && !keyspaceName.equals(tableName.getKeyspace()))
- throw ire("Keyspace name '%s' doesn't match table name '%s'", keyspaceName, tableName);
+ throw ire(KEYSPACE_DOES_NOT_MATCH_TABLE, keyspaceName, tableName);
if (indexName.hasKeyspace() && !keyspaceName.equals(indexName.getKeyspace()))
- throw ire("Keyspace name '%s' doesn't match index name '%s'", keyspaceName, tableName);
+ throw ire(KEYSPACE_DOES_NOT_MATCH_INDEX, keyspaceName, tableName);
return new CreateIndexStatement(keyspaceName, tableName.getName(), indexName.getName(), rawIndexTargets, attrs, ifNotExists);
}
diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java
index d46ce1efa2..2ca47ea06f 100644
--- a/src/java/org/apache/cassandra/db/ReadCommand.java
+++ b/src/java/org/apache/cassandra/db/ReadCommand.java
@@ -411,7 +411,6 @@ public abstract class ReadCommand extends AbstractReadQuery
{
cfs.indexManager.checkQueryability(indexQueryPlan);
- Index index = indexQueryPlan.getFirst();
searcher = indexQueryPlan.searcherFor(this);
Tracing.trace("Executing read on {}.{} using index{} {}",
cfs.metadata.keyspace,
diff --git a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java
index 6f2b8c6d85..272bbd9305 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java
@@ -217,6 +217,12 @@ public class Tracker
return accumulate;
}
+ public void updateLiveDiskSpaceUsed(long adjustment)
+ {
+ cfstore.metric.liveDiskSpaceUsed.inc(adjustment);
+ cfstore.metric.totalDiskSpaceUsed.inc(adjustment);
+ }
+
// SETUP / CLEANUP
public void addInitialSSTables(Iterable<SSTableReader> sstables)
diff --git a/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java b/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java
index da62e46dec..2f2c2a2551 100644
--- a/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java
+++ b/src/java/org/apache/cassandra/db/memtable/AbstractMemtable.java
@@ -30,6 +30,7 @@ import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.commitlog.CommitLogPosition;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.rows.EncodingStats;
import org.apache.cassandra.schema.ColumnMetadata;
@@ -39,6 +40,7 @@ import org.github.jamm.Unmetered;
public abstract class AbstractMemtable implements Memtable
{
+ private final AtomicReference<LifecycleTransaction> flushTransaction = new AtomicReference<>(null);
protected final AtomicLong currentOperations = new AtomicLong(0);
protected final ColumnsCollector columnsCollector;
protected final StatsCollector statsCollector = new StatsCollector();
@@ -66,21 +68,25 @@ public abstract class AbstractMemtable implements Memtable
this.minTimestamp = new AtomicLong(minTimestamp);
}
+ @Override
public TableMetadata metadata()
{
return metadata.get();
}
+ @Override
public long operationCount()
{
return currentOperations.get();
}
+ @Override
public long getMinTimestamp()
{
return minTimestamp.get() != EncodingStats.NO_STATS.minTimestamp ? minTimestamp.get() : NO_MIN_TIMESTAMP;
}
+ @Override
public long getMinLocalDeletionTime()
{
return minLocalDeletionTime.get();
@@ -120,6 +126,18 @@ public abstract class AbstractMemtable implements Memtable
return statsCollector.get();
}
+ @Override
+ public LifecycleTransaction getFlushTransaction()
+ {
+ return flushTransaction.get();
+ }
+
+ @Override
+ public LifecycleTransaction setFlushTransaction(LifecycleTransaction flushTransaction)
+ {
+ return this.flushTransaction.getAndSet(flushTransaction);
+ }
+
protected static class ColumnsCollector
{
private final HashMap<ColumnMetadata, AtomicBoolean> predefined = new HashMap<>();
@@ -205,26 +223,31 @@ public abstract class AbstractMemtable implements Memtable
protected abstract class AbstractFlushablePartitionSet<P extends Partition> implements FlushablePartitionSet<P>
{
+ @Override
public long dataSize()
{
return getLiveDataSize();
}
+ @Override
public CommitLogPosition commitLogLowerBound()
{
return AbstractMemtable.this.getCommitLogLowerBound();
}
+ @Override
public LastCommitLogPosition commitLogUpperBound()
{
return AbstractMemtable.this.getFinalCommitLogUpperBound();
}
+ @Override
public EncodingStats encodingStats()
{
return AbstractMemtable.this.encodingStats();
}
+ @Override
public RegularAndStaticColumns columns()
{
return AbstractMemtable.this.columns();
diff --git a/src/java/org/apache/cassandra/db/memtable/Flushing.java b/src/java/org/apache/cassandra/db/memtable/Flushing.java
index f97f59518d..6e269515c7 100644
--- a/src/java/org/apache/cassandra/db/memtable/Flushing.java
+++ b/src/java/org/apache/cassandra/db/memtable/Flushing.java
@@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
+import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -57,6 +58,12 @@ public class Flushing
Memtable memtable,
LifecycleTransaction txn)
{
+ LifecycleTransaction ongoingFlushTransaction = memtable.setFlushTransaction(txn);
+ Preconditions.checkState(ongoingFlushTransaction == null,
+ "Attempted to flush Memtable more than once on %s.%s",
+ cfs.keyspace.getName(),
+ cfs.name);
+
DiskBoundaries diskBoundaries = cfs.getDiskBoundaries();
List<PartitionPosition> boundaries = diskBoundaries.positions;
List<Directories.DataDirectory> locations = diskBoundaries.directories;
diff --git a/src/java/org/apache/cassandra/db/memtable/Memtable.java b/src/java/org/apache/cassandra/db/memtable/Memtable.java
index 3a18a8c7d7..603e3bc028 100644
--- a/src/java/org/apache/cassandra/db/memtable/Memtable.java
+++ b/src/java/org/apache/cassandra/db/memtable/Memtable.java
@@ -26,6 +26,7 @@ import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.commitlog.CommitLogPosition;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.EncodingStats;
@@ -379,6 +380,10 @@ public interface Memtable extends Comparable<Memtable>, UnfilteredSource
/** True if the memtable contains no data */
boolean isClean();
+ // The following two methods provide a way of tracking ongoing flushes
+ LifecycleTransaction setFlushTransaction(LifecycleTransaction transaction);
+ LifecycleTransaction getFlushTransaction();
+
/** Order memtables by time as reflected in the commit log position at time of construction */
default int compareTo(Memtable that)
{
diff --git a/src/java/org/apache/cassandra/index/sai/IndexContext.java b/src/java/org/apache/cassandra/index/sai/IndexContext.java
index fc03796ec1..032f28967c 100644
--- a/src/java/org/apache/cassandra/index/sai/IndexContext.java
+++ b/src/java/org/apache/cassandra/index/sai/IndexContext.java
@@ -21,51 +21,52 @@ package org.apache.cassandra.index.sai;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.statements.schema.IndexTarget;
import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.AsciiType;
import org.apache.cassandra.db.marshal.BooleanType;
import org.apache.cassandra.db.marshal.CompositeType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UUIDType;
-import org.apache.cassandra.db.memtable.Memtable;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.Row;
-import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer;
-import org.apache.cassandra.index.sai.memory.MemtableIndex;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+import org.apache.cassandra.index.sai.disk.format.Version;
+import org.apache.cassandra.index.sai.memory.MemtableIndexManager;
+import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics;
import org.apache.cassandra.index.sai.metrics.IndexMetrics;
import org.apache.cassandra.index.sai.plan.Expression;
import org.apache.cassandra.index.sai.utils.PrimaryKey;
-import org.apache.cassandra.index.sai.utils.PrimaryKeyFactory;
-import org.apache.cassandra.index.sai.utils.KeyRangeIterator;
-import org.apache.cassandra.index.sai.utils.KeyRangeUnionIterator;
import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.index.sai.view.IndexViewManager;
+import org.apache.cassandra.index.sai.view.View;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.IndexMetadata;
-import org.apache.cassandra.utils.Clock;
-import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.Pair;
/**
- * Manage metadata for each column index.
+ * Manages metadata for each column index.
*/
public class IndexContext
{
+ private static final Logger logger = LoggerFactory.getLogger(IndexContext.class);
+
private static final Set<AbstractType<?>> EQ_ONLY_TYPES = ImmutableSet.of(UTF8Type.instance,
AsciiType.instance,
BooleanType.instance,
@@ -82,15 +83,16 @@ public class IndexContext
// Config can be null if the column context is "fake" (i.e. created for a filtering expression).
@Nullable
- private final IndexMetadata config;
+ private final IndexMetadata indexMetadata;
- private final ConcurrentMap<Memtable, MemtableIndex> liveMemtableIndexMap;
+ private final MemtableIndexManager memtableIndexManager;
+ private final IndexViewManager viewManager;
private final IndexMetrics indexMetrics;
-
+ private final ColumnQueryMetrics columnQueryMetrics;
private final AbstractAnalyzer.AnalyzerFactory indexAnalyzerFactory;
private final AbstractAnalyzer.AnalyzerFactory queryAnalyzerFactory;
- private final PrimaryKeyFactory primaryKeyFactory;
+ private final PrimaryKey.Factory primaryKeyFactory;
public IndexContext(String keyspace,
String table,
@@ -98,7 +100,7 @@ public class IndexContext
ClusteringComparator clusteringComparator,
ColumnMetadata columnMetadata,
IndexTarget.Type indexType,
- @Nullable IndexMetadata config)
+ @Nullable IndexMetadata indexMetadata)
{
this.keyspace = Objects.requireNonNull(keyspace);
this.table = Objects.requireNonNull(table);
@@ -107,11 +109,13 @@ public class IndexContext
this.columnMetadata = Objects.requireNonNull(columnMetadata);
this.indexType = Objects.requireNonNull(indexType);
this.validator = TypeUtil.cellValueType(columnMetadata, indexType);
- this.primaryKeyFactory = PrimaryKey.factory(clusteringComparator);
+ this.primaryKeyFactory = new PrimaryKey.Factory(clusteringComparator);
- this.config = config;
- this.indexMetrics = config == null ? null : new IndexMetrics(this);
- this.liveMemtableIndexMap = config == null ? null : new ConcurrentHashMap<>();
+ this.indexMetadata = indexMetadata;
+ this.memtableIndexManager = indexMetadata == null ? null : new MemtableIndexManager(this);
+ this.indexMetrics = indexMetadata == null ? null : new IndexMetrics(this);
+ this.viewManager = new IndexViewManager(this);
+ this.columnQueryMetrics = new ColumnQueryMetrics.TrieIndexMetrics(this);
// We currently only support the NoOpAnalyzer
this.indexAnalyzerFactory = AbstractAnalyzer.fromOptions(getValidator(), Collections.emptyMap());
@@ -123,122 +127,42 @@ public class IndexContext
return partitionKeyType;
}
- public PrimaryKeyFactory keyFactory()
+ public PrimaryKey.Factory keyFactory()
{
return primaryKeyFactory;
}
- public ClusteringComparator comparator()
- {
- return clusteringComparator;
- }
-
public String getKeyspace()
{
return keyspace;
}
- public String getTable()
+ public IndexMetrics getIndexMetrics()
{
- return table;
+ return indexMetrics;
}
- public IndexMetadata getIndexMetadata()
+ public ColumnQueryMetrics getColumnQueryMetrics()
{
- return config;
- }
-
- public long index(DecoratedKey key, Row row, Memtable mt)
- {
- assert config != null : "Attempt to index on a non-indexing context";
-
- MemtableIndex current = liveMemtableIndexMap.get(mt);
-
- // We expect the relevant IndexMemtable to be present most of the time, so only make the
- // call to computeIfAbsent() if it's not. (see https://bugs.openjdk.java.net/browse/JDK-8161372)
- MemtableIndex target = (current != null)
- ? current
- : liveMemtableIndexMap.computeIfAbsent(mt, memtable -> new MemtableIndex(this));
-
- long start = Clock.Global.nanoTime();
-
- long bytes = 0;
-
- if (isNonFrozenCollection())
- {
- Iterator<ByteBuffer> bufferIterator = getValuesOf(row, FBUtilities.nowInSeconds());
- if (bufferIterator != null)
- {
- while (bufferIterator.hasNext())
- {
- ByteBuffer value = bufferIterator.next();
- bytes += target.index(key, row.clustering(), value);
- }
- }
- }
- else
- {
- ByteBuffer value = getValueOf(key, row, FBUtilities.nowInSeconds());
- target.index(key, row.clustering(), value);
- }
- indexMetrics.memtableIndexWriteLatency.update(Clock.Global.nanoTime() - start, TimeUnit.NANOSECONDS);
- return bytes;
+ return columnQueryMetrics;
}
- public void renewMemtable(Memtable renewed)
- {
- assert liveMemtableIndexMap != null : "Attempt to renew memtable on non-indexing context";
-
- for (Memtable memtable : liveMemtableIndexMap.keySet())
- {
- // remove every index but the one that corresponds to the post-truncate Memtable
- if (renewed != memtable)
- {
- liveMemtableIndexMap.remove(memtable);
- }
- }
- }
-
- public void discardMemtable(Memtable discarded)
- {
- assert liveMemtableIndexMap != null : "Attempt to discard memtable from non-indexing context";
-
- liveMemtableIndexMap.remove(discarded);
- }
-
- public KeyRangeIterator searchMemtableIndexes(Expression e, AbstractBounds<PartitionPosition> keyRange)
+ public String getTable()
{
- assert liveMemtableIndexMap != null : "Attempt to perform search on non-indexing context";
-
- Collection<MemtableIndex> memtableIndexes = liveMemtableIndexMap.values();
-
- if (memtableIndexes.isEmpty())
- {
- return KeyRangeIterator.empty();
- }
-
- KeyRangeIterator.Builder builder = KeyRangeUnionIterator.builder(memtableIndexes.size());
-
- for (MemtableIndex memtableIndex : memtableIndexes)
- {
- builder.add(memtableIndex.search(e, keyRange));
- }
-
- return builder.build();
+ return table;
}
- public long liveMemtableWriteCount()
+ public IndexMetadata getIndexMetadata()
{
- assert liveMemtableIndexMap != null : "Attempt to get metrics from non-indexing context";
-
- return liveMemtableIndexMap.values().stream().mapToLong(MemtableIndex::writeCount).sum();
+ return indexMetadata;
}
- public long estimatedMemIndexMemoryUsed()
+ /**
+ * @return A set of SSTables which have attached to them invalid index components.
+ */
+ public Collection<SSTableContext> onSSTableChanged(Collection<SSTableReader> oldSSTables, Collection<SSTableContext> newSSTables, boolean validate)
{
- assert liveMemtableIndexMap != null : "Attempt to get metrics from non-indexing context";
-
- return liveMemtableIndexMap.values().stream().mapToLong(MemtableIndex::estimatedMemoryUsed).sum();
+ return viewManager.update(oldSSTables, newSSTables, validate);
}
public ColumnMetadata getDefinition()
@@ -269,7 +193,7 @@ public class IndexContext
@Nullable
public String getIndexName()
{
- return config == null ? null : config.name;
+ return indexMetadata == null ? null : indexMetadata.name;
}
/**
@@ -292,26 +216,53 @@ public class IndexContext
return queryAnalyzerFactory;
}
- public boolean isIndexed()
+ public View getView()
{
- return config != null;
+ return viewManager.getView();
+ }
+
+ public MemtableIndexManager getMemtableIndexManager()
+ {
+ assert memtableIndexManager != null : "Attempt to use memtable index manager on non-indexed context";
+
+ return memtableIndexManager;
+ }
+
+ /**
+ * @return total number of per-index open files
+ */
+ public int openPerIndexFiles()
+ {
+ return viewManager.getView().size() * Version.LATEST.onDiskFormat().openFilesPerColumnIndex(this);
+ }
+
+ public void drop(Collection<SSTableReader> sstablesToRebuild)
+ {
+ viewManager.drop(sstablesToRebuild);
+ }
+
+ public boolean isNotIndexed()
+ {
+ return indexMetadata == null;
}
/**
* Called when index is dropped. Clear all live in-memory indexes and close
- * analyzer factories.
+ * analyzer factories. Mark all {@link SSTableIndex} as released and per-column index files
+ * will be removed when in-flight queries are completed.
*/
public void invalidate()
{
- if (liveMemtableIndexMap != null)
- liveMemtableIndexMap.clear();
- if (indexMetrics != null)
- indexMetrics.release();
+ viewManager.invalidate();
indexAnalyzerFactory.close();
if (queryAnalyzerFactory != indexAnalyzerFactory)
- {
queryAnalyzerFactory.close();
- }
+ if (memtableIndexManager != null)
+ memtableIndexManager.invalidate();
+ if (indexMetrics != null)
+ indexMetrics.release();
+ if (columnQueryMetrics != null)
+ columnQueryMetrics.release();
}
public boolean supports(Operator op)
@@ -422,7 +373,7 @@ public class IndexContext
return Objects.equals(columnMetadata, other.columnMetadata) &&
(indexType == other.indexType) &&
- Objects.equals(config, other.config) &&
+ Objects.equals(indexMetadata, other.indexMetadata) &&
Objects.equals(partitionKeyType, other.partitionKeyType) &&
Objects.equals(clusteringComparator, other.clusteringComparator);
}
@@ -430,15 +381,15 @@ public class IndexContext
@Override
public int hashCode()
{
- return Objects.hash(columnMetadata, indexType, config, partitionKeyType, clusteringComparator);
+ return Objects.hash(columnMetadata, indexType, indexMetadata, partitionKeyType, clusteringComparator);
}
/**
* A helper method for constructing consistent log messages for specific column indexes.
- *
+ * <p>
* Example: For the index "idx" in keyspace "ks" on table "tb", calling this method with the raw message
* "Flushing new index segment..." will produce...
- *
+ * <p>
* "[ks.tb.idx] Flushing new index segment..."
*
* @param message The raw content of a logging message, without information identifying it with an index.
@@ -448,6 +399,99 @@ public class IndexContext
public String logMessage(String message)
{
// Index names are unique only within a keyspace.
- return String.format("[%s.%s.%s] %s", keyspace, table, config == null ? "?" : config.name, message);
+ return String.format("[%s.%s.%s] %s", keyspace, table, indexMetadata == null ? "?" : indexMetadata.name, message);
+ }
+
+ /**
+ * @return the indexes that are built on the given SSTables on the left and corrupted indexes'
+ * corresponding contexts on the right
+ */
+ public Pair<Collection<SSTableIndex>, Collection<SSTableContext>> getBuiltIndexes(Collection<SSTableContext> sstableContexts, boolean validate)
+ {
+ Set<SSTableIndex> valid = new HashSet<>(sstableContexts.size());
+ Set<SSTableContext> invalid = new HashSet<>();
+
+ for (SSTableContext sstableContext : sstableContexts)
+ {
+ if (sstableContext.sstable.isMarkedCompacted())
+ continue;
+
+ if (!sstableContext.indexDescriptor.isPerColumnIndexBuildComplete(this))
+ {
+ logger.debug(logMessage("An on-disk index build for SSTable {} has not completed."), sstableContext.descriptor());
+ continue;
+ }
+
+ if (sstableContext.indexDescriptor.isIndexEmpty(this))
+ {
+ logger.debug(logMessage("No on-disk index was built for SSTable {} because the SSTable " +
+ "had no indexable rows for the index."), sstableContext.descriptor());
+ continue;
+ }
+
+ try
+ {
+ if (validate)
+ {
+ if (!sstableContext.indexDescriptor.validatePerIndexComponents(this))
+ {
+ logger.warn(logMessage("Invalid per-column component for SSTable {}"), sstableContext.descriptor());
+ invalid.add(sstableContext);
+ continue;
+ }
+ }
+
+ SSTableIndex index = sstableContext.newSSTableIndex(this);
+ logger.debug(logMessage("Successfully created index for SSTable {}."), sstableContext.descriptor());
+
+ // Try to add new index to the set, if set already has such index, we'll simply release and move on.
+ // This covers situation when SSTable collection has the same SSTable multiple
+ // times because we don't know what kind of collection it actually is.
+ if (!valid.add(index))
+ {
+ index.release();
+ }
+ }
+ catch (Throwable e)
+ {
+ logger.warn(logMessage("Failed to update per-column components for SSTable {}"), sstableContext.descriptor(), e);
+ invalid.add(sstableContext);
+ }
+ }
+
+ return Pair.create(valid, invalid);
+ }
+
+ /**
+ * @return the number of indexed rows in this index (aka. a pair of term and rowId)
+ */
+ public long getCellCount()
+ {
+ return getView().getIndexes()
+ .stream()
+ .mapToLong(SSTableIndex::getRowCount)
+ .sum();
+ }
+
+ /**
+ * @return the total size (in bytes) of per-column index components
+ */
+ public long diskUsage()
+ {
+ return getView().getIndexes()
+ .stream()
+ .mapToLong(SSTableIndex::sizeOfPerColumnComponents)
+ .sum();
+ }
+
+ /**
+ * @return the total memory usage (in bytes) of per-column index on-disk data structure
+ */
+ public long indexFileCacheSize()
+ {
+ return getView().getIndexes()
+ .stream()
+ .mapToLong(SSTableIndex::indexFileCacheSize)
+ .sum();
}
}
diff --git a/src/java/org/apache/cassandra/index/sai/QueryContext.java b/src/java/org/apache/cassandra/index/sai/QueryContext.java
index 4fff4160d3..4996acbd97 100644
--- a/src/java/org/apache/cassandra/index/sai/QueryContext.java
+++ b/src/java/org/apache/cassandra/index/sai/QueryContext.java
@@ -40,10 +40,17 @@ public class QueryContext
public final long executionQuotaNano;
+ public long sstablesHit = 0;
+ public long segmentsHit = 0;
public long partitionsRead = 0;
public long rowsFiltered = 0;
- public long queryTimeouts = 0;
+ public long trieSegmentsHit = 0;
+
+ public long triePostingsSkips = 0;
+ public long triePostingsDecodes = 0;
+
+ public boolean queryTimedOut = false;
public QueryContext(ReadCommand readCommand, long executionQuotaMs)
{
@@ -61,7 +68,7 @@ public class QueryContext
{
if (totalQueryTimeNs() >= executionQuotaNano && !DISABLE_TIMEOUT)
{
- queryTimeouts++;
+ queryTimedOut = true;
throw new QueryCancelledException(readCommand);
}
}
diff --git a/src/java/org/apache/cassandra/index/sai/SSTableContext.java b/src/java/org/apache/cassandra/index/sai/SSTableContext.java
new file mode 100644
index 0000000000..d0ff6372b6
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/SSTableContext.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+import java.util.Collections;
+
+import com.google.common.base.Objects;
+
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.concurrent.Ref;
+import org.apache.cassandra.utils.concurrent.RefCounted;
+import org.apache.cassandra.utils.concurrent.SharedCloseableImpl;
+
+/**
+ * An {@link SSTableContext} is created for an individual sstable and is shared across column indexes to track per-sstable
+ * index files.
+ * <p>
+ * The {@link SSTableContext} will be released when receiving a sstable removed notification, but its shared copies in
+ * individual {@link SSTableIndex}es will be released when in-flight read requests complete.
+ */
+public class SSTableContext extends SharedCloseableImpl
+{
+ public final SSTableReader sstable;
+ public final IndexDescriptor indexDescriptor;
+ public final PrimaryKeyMap.Factory primaryKeyMapFactory;
+
+ private SSTableContext(SSTableReader sstable,
+ IndexDescriptor indexDescriptor,
+ PrimaryKeyMap.Factory primaryKeyMapFactory,
+ Cleanup cleanup)
+ {
+ super(cleanup);
+ this.sstable = sstable;
+ this.indexDescriptor = indexDescriptor;
+ this.primaryKeyMapFactory = primaryKeyMapFactory;
+ }
+
+ private SSTableContext(SSTableContext copy)
+ {
+ super(copy);
+ this.sstable = copy.sstable;
+ this.indexDescriptor = copy.indexDescriptor;
+ this.primaryKeyMapFactory = copy.primaryKeyMapFactory;
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public static SSTableContext create(SSTableReader sstable)
+ {
+ Ref<? extends SSTableReader> sstableRef = null;
+ PrimaryKeyMap.Factory primaryKeyMapFactory = null;
+
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable);
+ try
+ {
+ sstableRef = sstable.tryRef();
+
+ if (sstableRef == null)
+ {
+ throw new IllegalStateException("Couldn't acquire reference to the sstable: " + sstable);
+ }
+
+ primaryKeyMapFactory = indexDescriptor.newPrimaryKeyMapFactory(sstable);
+
+ Cleanup cleanup = new Cleanup(primaryKeyMapFactory, indexDescriptor, sstableRef);
+
+ return new SSTableContext(sstable, indexDescriptor, primaryKeyMapFactory, cleanup);
+ }
+ catch (Throwable t)
+ {
+ if (sstableRef != null)
+ {
+ sstableRef.release();
+ }
+
+ throw Throwables.unchecked(Throwables.close(t, Collections.singleton(primaryKeyMapFactory)));
+ }
+ }
+
+ @Override
+ public SSTableContext sharedCopy()
+ {
+ return new SSTableContext(this);
+ }
+
+ /**
+ * Returns a new {@link SSTableIndex} for a per-column index
+ */
+ public SSTableIndex newSSTableIndex(IndexContext indexContext)
+ {
+ return indexDescriptor.newSSTableIndex(this, indexContext);
+ }
+
+ /**
+ * @return descriptor of attached sstable
+ */
+ public Descriptor descriptor()
+ {
+ return sstable.descriptor;
+ }
+
+ /**
+ * @return disk usage (in bytes) of per-sstable index files
+ */
+ public long diskUsage()
+ {
+ return indexDescriptor.sizeOnDiskOfPerSSTableComponents();
+ }
+
+ /**
+ * @return number of open files per {@link SSTableContext} instance
+ */
+ public int openFilesPerSSTable()
+ {
+ return indexDescriptor.version.onDiskFormat().openFilesPerSSTableIndex();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "SSTableContext{" +
+ "sstable=" + sstable.descriptor +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SSTableContext that = (SSTableContext) o;
+ return Objects.equal(sstable.descriptor, that.sstable.descriptor);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(sstable.descriptor.hashCode());
+ }
+
+ private static class Cleanup implements RefCounted.Tidy
+ {
+ private final PrimaryKeyMap.Factory primaryKeyMapFactory;
+ private final IndexDescriptor indexDescriptor;
+ private final Ref<? extends SSTableReader> sstableRef;
+
+ private Cleanup(PrimaryKeyMap.Factory primaryKeyMapFactory,
+ IndexDescriptor indexDescriptor,
+ Ref<? extends SSTableReader> sstableRef)
+ {
+ this.primaryKeyMapFactory = primaryKeyMapFactory;
+ this.indexDescriptor = indexDescriptor;
+ this.sstableRef = sstableRef;
+ }
+
+ @Override
+ public void tidy()
+ {
+ Throwable t = sstableRef.ensureReleased(null);
+ t = Throwables.close(t, Collections.singleton(primaryKeyMapFactory));
+
+ Throwables.maybeFail(t);
+ }
+
+ @Override
+ public String name()
+ {
+ return indexDescriptor.toString();
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/SSTableContextManager.java b/src/java/org/apache/cassandra/index/sai/SSTableContextManager.java
new file mode 100644
index 0000000000..ba65806e86
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/SSTableContextManager.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.utils.Pair;
+
+/**
+ * Manages per-sstable {@link SSTableContext}s for {@link StorageAttachedIndexGroup}
+ */
+@ThreadSafe
+public class SSTableContextManager
+{
+ private static final Logger logger = LoggerFactory.getLogger(SSTableContextManager.class);
+
+ private final ConcurrentHashMap<SSTableReader, SSTableContext> sstableContexts = new ConcurrentHashMap<>();
+
+ /**
+ * Initialize {@link SSTableContext}s if they are not already initialized.
+ *
+ * @param removed SSTables being removed
+ * @param added SSTables being added
+ * @param validate if true, header and footer will be validated.
+ *
+ * @return a set of contexts for SSTables with valid per-SSTable components, and a set of
+ * SSTables with invalid or missing components
+ */
+ public Pair<Set<SSTableContext>, Set<SSTableReader>> update(Collection<SSTableReader> removed, Iterable<SSTableReader> added, boolean validate)
+ {
+ release(removed);
+
+ Set<SSTableContext> contexts = new HashSet<>();
+ Set<SSTableReader> invalid = new HashSet<>();
+
+ for (SSTableReader sstable : added)
+ {
+ if (sstable.isMarkedCompacted())
+ {
+ continue;
+ }
+
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable);
+
+ if (!indexDescriptor.isPerSSTableIndexBuildComplete())
+ {
+ // Don't even try to validate or add the context if the completion marker is missing.
+ continue;
+ }
+
+ try
+ {
+ // Only validate on restart or newly refreshed SSTable. Newly built files are unlikely to be corrupted.
+ if (validate && !sstableContexts.containsKey(sstable) && !indexDescriptor.validatePerSSTableComponents())
+ {
+ logger.warn(indexDescriptor.logMessage("Invalid per-SSTable component for SSTable {}"), sstable.descriptor);
+ invalid.add(sstable);
+ removeInvalidSSTableContext(sstable);
+ continue;
+ }
+ // ConcurrentHashMap#computeIfAbsent guarantees atomicity, so {@link SSTableContext#create(SSTableReader)}}
+ // is called at most once per key.
+ contexts.add(sstableContexts.computeIfAbsent(sstable, SSTableContext::create));
+ }
+ catch (Throwable t)
+ {
+ logger.warn(indexDescriptor.logMessage("Failed to update per-SSTable components for SSTable {}"), sstable.descriptor, t);
+ invalid.add(sstable);
+ removeInvalidSSTableContext(sstable);
+ }
+ }
+
+ return Pair.create(contexts, invalid);
+ }
+
+ public void release(Collection<SSTableReader> toRelease)
+ {
+ toRelease.stream().map(sstableContexts::remove).filter(Objects::nonNull).forEach(SSTableContext::close);
+ }
+
+ /**
+ * @return total number of per-sstable open files for live sstables
+ */
+ int openFiles()
+ {
+ return sstableContexts.values().stream().mapToInt(SSTableContext::openFilesPerSSTable).sum();
+ }
+
+ /**
+ * @return total disk usage (in bytes) of all per-sstable index files
+ */
+ long diskUsage()
+ {
+ return sstableContexts.values().stream().mapToLong(SSTableContext::diskUsage).sum();
+ }
+
+ Set<SSTableReader> sstables()
+ {
+ return sstableContexts.keySet();
+ }
+
+ @VisibleForTesting
+ public int size()
+ {
+ return sstableContexts.size();
+ }
+
+ @VisibleForTesting
+ public void clear()
+ {
+ sstableContexts.values().forEach(SSTableContext::close);
+ sstableContexts.clear();
+ }
+
+ @SuppressWarnings("EmptyTryBlock")
+ private void removeInvalidSSTableContext(SSTableReader sstable)
+ {
+ try (SSTableContext ignored = sstableContexts.remove(sstable))
+ {
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
index 7e9b96f20f..fdc1759cc5 100644
--- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
+++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
@@ -18,21 +18,35 @@
package org.apache.cassandra.index.sai;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture; // checkstyle: permit this import
import java.util.concurrent.Future;
+import java.util.function.BooleanSupplier;
+import java.util.stream.Collectors;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures; // checkstyle: permit this import
+import com.google.common.util.concurrent.ListenableFuture; // checkstyle: permit this import
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQL3Type;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.statements.schema.IndexTarget;
@@ -42,7 +56,7 @@ import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.WriteContext;
-import org.apache.cassandra.db.compaction.CompactionInfo;
+import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
import org.apache.cassandra.db.marshal.AbstractType;
@@ -60,14 +74,21 @@ import org.apache.cassandra.index.IndexRegistry;
import org.apache.cassandra.index.SecondaryIndexBuilder;
import org.apache.cassandra.index.TargetParser;
import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+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.TypeUtil;
+import org.apache.cassandra.index.sai.view.View;
import org.apache.cassandra.index.transactions.IndexTransaction;
+import org.apache.cassandra.io.sstable.Component;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.SSTableIdFactory;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.concurrent.OpOrder;
@@ -83,20 +104,34 @@ public class StorageAttachedIndex implements Index
Collection<SSTableReader> sstablesToRebuild,
boolean isFullRebuild)
{
- // Return an empty builder for in-memory implementation
- return new SecondaryIndexBuilder()
- {
- @Override
- public void build()
- {
- }
-
- @Override
- public CompactionInfo getCompactionInfo()
- {
- return null;
- }
- };
+ NavigableMap<SSTableReader, Set<StorageAttachedIndex>> sstables = new TreeMap<>(Comparator.comparing(s -> s.descriptor.id, SSTableIdFactory.COMPARATOR));
+ StorageAttachedIndexGroup group = StorageAttachedIndexGroup.getIndexGroup(cfs);
+
+ assert group != null : "Index group does not exist for table " + cfs.keyspace + '.' + cfs.name;
+
+ indexes.stream()
+ .filter((i) -> i instanceof StorageAttachedIndex)
+ .forEach((i) ->
+ {
+ StorageAttachedIndex sai = (StorageAttachedIndex) i;
+ IndexContext indexContext = ((StorageAttachedIndex) i).getIndexContext();
+
+ // If this is not a full manual index rebuild we can skip SSTables that already have an
+ // attached index. Otherwise, we override any pre-existent index.
+ Collection<SSTableReader> ss = sstablesToRebuild;
+ if (!isFullRebuild)
+ {
+ ss = sstablesToRebuild.stream()
+ .filter(s -> !IndexDescriptor.create(s).isPerColumnIndexBuildComplete(indexContext))
+ .collect(Collectors.toList());
+ }
+
+ group.dropIndexSSTables(ss, sai);
+
+ ss.forEach(sstable -> sstables.computeIfAbsent(sstable, ignore -> new HashSet<>()).add(sai));
+ });
+
+ return new StorageAttachedIndexBuilder(group, sstables, isFullRebuild, false);
}
}
@@ -119,6 +154,12 @@ public class StorageAttachedIndex implements Index
private final ColumnFamilyStore baseCfs;
private final IndexContext indexContext;
+ // Tracks whether we've started the index build on initialization.
+ private volatile boolean initBuildStarted = false;
+
+ // Tracks whether the index has been invalidated due to removal, a table drop, etc.
+ private volatile boolean valid = true;
+
public StorageAttachedIndex(ColumnFamilyStore baseCfs, IndexMetadata config)
{
this.baseCfs = baseCfs;
@@ -229,17 +270,96 @@ public class StorageAttachedIndex implements Index
{
// New storage-attached indexes will be available for queries after on disk index data are built.
// Memtable data will be indexed via flushing triggered by schema change
- return () -> startInitialBuild(baseCfs).get();
+ // We only want to validate the index files if we are starting up
+ return () -> startInitialBuild(baseCfs, StorageService.instance.isStarting()).get();
}
- private Future<?> startInitialBuild(ColumnFamilyStore baseCfs)
+ private Future<?> startInitialBuild(ColumnFamilyStore baseCfs, boolean validate)
{
if (baseCfs.indexManager.isIndexQueryable(this))
{
- logger.debug(indexContext.logMessage("Skipping validation and building in initialization task, as pre-join has already made the storage attached index queryable..."));
+ logger.debug(indexContext.logMessage("Skipping validation and building in initialization task, as pre-join has already made the storage-attached index queryable..."));
+ initBuildStarted = true;
+ return CompletableFuture.completedFuture(null);
+ }
+
+ // stop in-progress compaction tasks to prevent compacted sstable not being indexed.
+ logger.debug(indexContext.logMessage("Stopping active compactions to make sure all sstables are indexed after initial build."));
+ CompactionManager.instance.interruptCompactionFor(Collections.singleton(baseCfs.metadata()),
+ ssTableReader -> true,
+ true);
+
+ // Force another flush to make sure on disk index is generated for memtable data before marking it queryable.
+ // In the case of offline scrub, there are no live memtables.
+ if (!baseCfs.getTracker().getView().liveMemtables.isEmpty())
+ {
+ baseCfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.INDEX_BUILD_STARTED);
+ }
+
+ // It is now safe to flush indexes directly from flushing Memtables.
+ initBuildStarted = true;
+
+ StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs);
+
+ assert indexGroup != null : "Index group does not exist for table " + baseCfs.keyspace + '.' + baseCfs.name;
+
+ List<SSTableReader> nonIndexed = findNonIndexedSSTables(baseCfs, indexGroup, validate);
+
+ if (nonIndexed.isEmpty())
+ {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ // split sorted sstables into groups with similar size and build each group in separate compaction thread
+ List<List<SSTableReader>> groups = groupBySize(nonIndexed, DatabaseDescriptor.getConcurrentIndexBuilders());
+ List<ListenableFuture<?>> futures = new ArrayList<>();
+
+ for (List<SSTableReader> group : groups)
+ {
+ SortedMap<SSTableReader, Set<StorageAttachedIndex>> current = new TreeMap<>(Comparator.comparing(s -> s.descriptor.id, SSTableIdFactory.COMPARATOR));
+ group.forEach(sstable -> current.put(sstable, Collections.singleton(this)));
+
+ futures.add(CompactionManager.instance.submitIndexBuild(new StorageAttachedIndexBuilder(indexGroup, current, false, true)));
+ }
+
+ logger.info(indexContext.logMessage("Submitting {} parallel initial index builds over {} total sstables..."), futures.size(), nonIndexed.size());
+ return Futures.allAsList(futures);
+ }
+
+ /**
+ * Splits SSTables into groups of similar overall size.
+ *
+ * @param toRebuild a list of SSTables to split (Note that this list will be sorted in place!)
+ * @param parallelism an upper bound on the number of groups
+ *
+ * @return a {@link List} of SSTable groups, each represented as a {@link List} of {@link SSTableReader}
+ */
+ @VisibleForTesting
+ public static List<List<SSTableReader>> groupBySize(List<SSTableReader> toRebuild, int parallelism)
+ {
+ List<List<SSTableReader>> groups = new ArrayList<>();
+
+ toRebuild.sort(Comparator.comparingLong(SSTableReader::onDiskLength).reversed());
+ Iterator<SSTableReader> sortedSSTables = toRebuild.iterator();
+ double dataPerCompactor = toRebuild.stream().mapToLong(SSTableReader::onDiskLength).sum() * 1.0 / parallelism;
+
+ while (sortedSSTables.hasNext())
+ {
+ long sum = 0;
+ List<SSTableReader> current = new ArrayList<>();
+
+ while (sortedSSTables.hasNext() && sum < dataPerCompactor)
+ {
+ SSTableReader sstable = sortedSSTables.next();
+ sum += sstable.onDiskLength();
+ current.add(sstable);
+ }
+
+ assert !current.isEmpty();
+ groups.add(current);
}
- baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED);
- return CompletableFuture.completedFuture(null);
+
+ return groups;
}
@Override
@@ -259,6 +379,14 @@ public class StorageAttachedIndex implements Index
{
return () ->
{
+ // mark index as invalid, in-progress SSTableIndexWriters will abort
+ valid = false;
+
+ // in case of dropping table, SSTable indexes should already been removed by SSTableListChangedNotification.
+ Set<Component> toRemove = getComponents();
+ for (SSTableIndex sstableIndex : indexContext.getView().getIndexes())
+ sstableIndex.getSSTable().unregisterComponents(toRemove, baseCfs.getTracker());
+
indexContext.invalidate();
return null;
};
@@ -277,14 +405,38 @@ public class StorageAttachedIndex implements Index
return this::startPreJoinTask;
}
+ public boolean isInitBuildStarted()
+ {
+ return initBuildStarted;
+ }
+
+ public BooleanSupplier isIndexValid()
+ {
+ return () -> valid;
+ }
+
private Future<?> startPreJoinTask()
{
try
{
if (baseCfs.indexManager.isIndexQueryable(this))
+ {
logger.debug(indexContext.logMessage("Skipping validation in pre-join task, as the initialization task has already made the index queryable..."));
+ baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED);
+ return null;
+ }
+
+ StorageAttachedIndexGroup indexGroup = StorageAttachedIndexGroup.getIndexGroup(baseCfs);
+
+ assert indexGroup != null : "Index group does not exist for table";
- baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED);
+ Collection<SSTableReader> nonIndexed = findNonIndexedSSTables(baseCfs, indexGroup, true);
+
+ if (nonIndexed.isEmpty())
+ {
+ // If the index is complete, mark it queryable before the node starts accepting requests:
+ baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED);
+ }
}
catch (Throwable t)
{
@@ -358,6 +510,40 @@ public class StorageAttachedIndex implements Index
public void validate(PartitionUpdate update) throws InvalidRequestException
{}
+ /**
+ * This method is called by the startup tasks to find SSTables that don't have indexes. The method is
+ * synchronized so that the view is unchanged between validation and the selection of non-indexed SSTables.
+ *
+ * @return a list SSTables without attached indexes
+ */
+ private synchronized List<SSTableReader> findNonIndexedSSTables(ColumnFamilyStore baseCfs, StorageAttachedIndexGroup group, boolean validate)
+ {
+ Set<SSTableReader> sstables = baseCfs.getLiveSSTables();
+
+ // Initialize the SSTable indexes w/ valid existing components...
+ assert group != null : "Missing index group on " + baseCfs.name;
+ group.onSSTableChanged(Collections.emptyList(), sstables, Collections.singleton(this), validate);
+
+ // ...then identify and rebuild the SSTable indexes that are missing.
+ List<SSTableReader> nonIndexed = new ArrayList<>();
+ View view = indexContext.getView();
+
+ for (SSTableReader sstable : sstables)
+ {
+ // An SSTable is considered not indexed if:
+ // 1. The current view does not contain the SSTable
+ // 2. The SSTable is not marked compacted
+ // 3. The column index does not have a completion marker
+ if (!view.containsSSTable(sstable) && !sstable.isMarkedCompacted() &&
+ !IndexDescriptor.create(sstable).isPerColumnIndexBuildComplete(indexContext))
+ {
+ nonIndexed.add(sstable);
+ }
+ }
+
+ return nonIndexed;
+ }
+
@Override
public Searcher searcherFor(ReadCommand command) throws InvalidRequestException
{
@@ -372,6 +558,16 @@ public class StorageAttachedIndex implements Index
throw new UnsupportedOperationException("Storage-attached index flush observers should never be created directly.");
}
+ @Override
+ public Set<Component> getComponents()
+ {
+ return Version.LATEST.onDiskFormat()
+ .perColumnIndexComponents(indexContext)
+ .stream()
+ .map(c -> Version.LATEST.makePerIndexComponent(c, indexContext))
+ .collect(Collectors.toSet());
+ }
+
@Override
public Indexer indexerFor(DecoratedKey key,
RegularAndStaticColumns columns,
@@ -432,7 +628,8 @@ public class StorageAttachedIndex implements Index
@Override
public void insertRow(Row row)
{
- adjustMemtableSize(indexContext.index(key, row, memtable), CassandraWriteContext.fromContext(writeContext).getGroup());
+ adjustMemtableSize(indexContext.getMemtableIndexManager().index(key, row, memtable),
+ CassandraWriteContext.fromContext(writeContext).getGroup());
}
@Override
diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java
new file mode 100644
index 0000000000..8908b83df0
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java
@@ -0,0 +1,352 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Maps;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.compaction.CompactionInfo;
+import org.apache.cassandra.db.compaction.CompactionInterruptedException;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.db.lifecycle.Tracker;
+import org.apache.cassandra.index.SecondaryIndexBuilder;
+import org.apache.cassandra.index.sai.disk.StorageAttachedIndexWriter;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.KeyIterator;
+import org.apache.cassandra.io.sstable.SSTableIdentityIterator;
+import org.apache.cassandra.io.sstable.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.TimeUUID;
+import org.apache.cassandra.utils.concurrent.CountDownLatch;
+import org.apache.cassandra.utils.concurrent.Ref;
+
+import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
+
+/**
+ * Multiple storage-attached indexes can start building concurrently. We need to make sure:
+ * 1. Per-SSTable index files are built only once
+ * a. Per-SSTable index files already built, do nothing
+ * b. Per-SSTable index files are currently building, we need to wait until it's built in order to consider index built.
+ * 2. Per-column index files are built for each column index
+ */
+public class StorageAttachedIndexBuilder extends SecondaryIndexBuilder
+{
+ protected static final Logger logger = LoggerFactory.getLogger(StorageAttachedIndexBuilder.class);
+
+ // make sure only one builder can write to per sstable files when multiple storage-attached indexes are created simultaneously.
+ private static final Map<SSTableReader, CountDownLatch> inProgress = Maps.newConcurrentMap();
+
+ private final StorageAttachedIndexGroup group;
+ private final TableMetadata metadata;
+ private final Tracker tracker;
+ private final TimeUUID compactionId = nextTimeUUID();
+ private final boolean isFullRebuild;
+ private final boolean isInitialBuild;
+
+ private final SortedMap<SSTableReader, Set<StorageAttachedIndex>> sstables;
+
+ private long bytesProcessed = 0;
+ private final long totalSizeInBytes;
+
+ StorageAttachedIndexBuilder(StorageAttachedIndexGroup group,
+ SortedMap<SSTableReader, Set<StorageAttachedIndex>> sstables,
+ boolean isFullRebuild,
+ boolean isInitialBuild)
+ {
+ this.group = group;
+ this.metadata = group.metadata();
+ this.sstables = sstables;
+ this.tracker = group.table().getTracker();
+ this.isFullRebuild = isFullRebuild;
+ this.isInitialBuild = isInitialBuild;
+ this.totalSizeInBytes = sstables.keySet().stream().mapToLong(SSTableReader::uncompressedLength).sum();
+ }
+
+ @Override
+ public void build()
+ {
+ logger.debug(logMessage("Starting full index build"));
+ for (Map.Entry<SSTableReader, Set<StorageAttachedIndex>> e : sstables.entrySet())
+ {
+ SSTableReader sstable = e.getKey();
+ Set<StorageAttachedIndex> indexes = e.getValue();
+
+ Set<StorageAttachedIndex> existing = validateIndexes(indexes, sstable.descriptor);
+ if (existing.isEmpty())
+ {
+ logger.debug(logMessage("{} dropped during index build"), indexes);
+ continue;
+ }
+
+ if (indexSSTable(sstable, existing))
+ {
+ return;
+ }
+ }
+ }
+
+ private String logMessage(String message)
+ {
+ return String.format("[%s.%s.*] %s", metadata.keyspace, metadata.name, message);
+ }
+
+ /**
+ * @return true if index build should be stopped
+ */
+ private boolean indexSSTable(SSTableReader sstable, Set<StorageAttachedIndex> indexes)
+ {
+ logger.debug(logMessage("Starting index build on {}"), sstable.descriptor);
+
+ CountDownLatch perSSTableFileLock = null;
+ StorageAttachedIndexWriter indexWriter = null;
+
+ Ref<? extends SSTableReader> ref = sstable.tryRef();
+ if (ref == null)
+ {
+ logger.warn(logMessage("Couldn't acquire reference to the SSTable {}. It may have been removed."), sstable.descriptor);
+ return false;
+ }
+
+ try (RandomAccessReader dataFile = sstable.openDataReader();
+ LifecycleTransaction txn = LifecycleTransaction.offline(OperationType.INDEX_BUILD, sstable))
+ {
+ perSSTableFileLock = shouldWritePerSSTableFiles(sstable);
+ // If we were unable to get the per-SSTable file lock it means that the
+ // per-SSTable components are already being built, so we only want to
+ // build the per-index components
+ boolean perIndexComponentsOnly = perSSTableFileLock == null;
+ // remove existing per column index files instead of overwriting
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable);
+ indexes.forEach(index -> indexDescriptor.deleteColumnIndex(index.getIndexContext()));
+
+ indexWriter = StorageAttachedIndexWriter.createBuilderWriter(indexDescriptor, indexes, txn, perIndexComponentsOnly);
+
+ indexWriter.begin();
+
+ long previousBytesRead = 0;
+
+ try (KeyIterator keys = sstable.keyIterator())
+ {
+ while (keys.hasNext())
+ {
+ if (isStopRequested())
+ {
+ logger.debug(indexDescriptor.logMessage("Index build has been stopped"));
+ throw new CompactionInterruptedException(getCompactionInfo());
+ }
+
+ DecoratedKey key = keys.next();
+
+ indexWriter.startPartition(key, -1, -1);
+
+ long position = sstable.getPosition(key, SSTableReader.Operator.EQ);
+ dataFile.seek(position);
+ ByteBufferUtil.readWithShortLength(dataFile); // key
+
+ try (SSTableIdentityIterator partition = SSTableIdentityIterator.create(sstable, dataFile, key))
+ {
+ // if the row has statics attached, it has to be indexed separately
+ if (metadata.hasStaticColumns())
+ indexWriter.nextUnfilteredCluster(partition.staticRow());
+
+ while (partition.hasNext())
+ indexWriter.nextUnfilteredCluster(partition.next());
+ }
+ long bytesRead = keys.getBytesRead();
+ bytesProcessed += bytesRead - previousBytesRead;
+ previousBytesRead = bytesRead;
+ }
+
+ completeSSTable(indexWriter, sstable, indexes, perSSTableFileLock);
+ }
+
+ return false;
+ }
+ catch (Throwable t)
+ {
+ if (indexWriter != null)
+ {
+ indexWriter.abort(t, true);
+ }
+
+ if (t instanceof InterruptedException)
+ {
+ // TODO: Is there anything that makes more sense than just restoring the interrupt?
+ logger.warn(logMessage("Interrupted while building indexes {} on SSTable {}"), indexes, sstable.descriptor);
+ Thread.currentThread().interrupt();
+ return true;
+ }
+ else if (t instanceof CompactionInterruptedException)
+ {
+ //TODO Shouldn't do this if the stop was interrupted by a truncate
+ if (isInitialBuild)
+ {
+ logger.error(logMessage("Stop requested while building initial indexes {} on SSTable {}."), indexes, sstable.descriptor);
+ throw Throwables.unchecked(t);
+ }
+ else
+ {
+ logger.info(logMessage("Stop requested while building indexes {} on SSTable {}."), indexes, sstable.descriptor);
+ return true;
+ }
+ }
+ else
+ {
+ logger.error(logMessage("Unable to build indexes {} on SSTable {}. Cause: {}."), indexes, sstable, t.getMessage());
+ throw Throwables.unchecked(t);
+ }
+ }
+ finally
+ {
+ ref.release();
+ // release current lock in case of error
+ if (perSSTableFileLock != null)
+ {
+ inProgress.remove(sstable);
+ perSSTableFileLock.decrement();
+ }
+ }
+ }
+
+ @Override
+ public CompactionInfo getCompactionInfo()
+ {
+ return new CompactionInfo(metadata,
+ OperationType.INDEX_BUILD,
+ bytesProcessed,
+ totalSizeInBytes,
+ compactionId,
+ sstables.keySet());
+ }
+
+ /**
+ * if the per sstable index files are already created, no need to write them again, unless found corrupted on rebuild
+ * if not created, try to acquire a lock, so only one builder will generate per sstable index files
+ */
+ private CountDownLatch shouldWritePerSSTableFiles(SSTableReader sstable)
+ {
+ // if per-table files are incomplete or checksum failed during full rebuild.
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable);
+ if (!indexDescriptor.isPerSSTableIndexBuildComplete() ||
+ (isFullRebuild && !indexDescriptor.validatePerSSTableComponentsChecksum()))
+ {
+ CountDownLatch latch = CountDownLatch.newCountDownLatch(1);
+ if (inProgress.putIfAbsent(sstable, latch) == null)
+ {
+ // lock owner should clean up existing per-SSTable files
+ group.deletePerSSTableFiles(Collections.singleton(sstable));
+ return latch;
+ }
+ }
+ return null;
+ }
+
+ private void completeSSTable(SSTableFlushObserver indexWriter,
+ SSTableReader sstable,
+ Set<StorageAttachedIndex> indexes,
+ CountDownLatch latch) throws InterruptedException
+ {
+ indexWriter.complete();
+
+ if (latch != null)
+ {
+ // current builder owns the lock
+ latch.decrement();
+ }
+ else
+ {
+ /*
+ * When there is no lock, it means the per sstable index files are already created, just proceed to finish.
+ * When there is a lock held by another builder, wait for it to finish before finishing marking current index built.
+ */
+ latch = inProgress.get(sstable);
+ if (latch != null)
+ latch.await();
+ }
+
+ Set<StorageAttachedIndex> existing = validateIndexes(indexes, sstable.descriptor);
+ if (existing.isEmpty())
+ {
+ logger.debug(logMessage("{} dropped during index build"), indexes);
+ return;
+ }
+
+ // register custom index components into existing sstables
+ sstable.registerComponents(StorageAttachedIndexGroup.getLiveComponents(sstable, existing), tracker);
+ Set<StorageAttachedIndex> incomplete = group.onSSTableChanged(Collections.emptyList(), Collections.singleton(sstable), existing, false);
+
+ if (!incomplete.isEmpty())
+ {
+ // If this occurs during an initial index build, there is only one index in play, and
+ // throwing here to terminate makes sense. (This allows the initialization task to fail
+ // correctly and be marked as failed by the SIM.) In other cases, such as rebuilding a
+ // set of indexes for a new added/streamed SSTables, we terminate pessimistically. In
+ // other words, we abort the SSTable index write across all column indexes and mark
+ // then non-queryable until a restart or other incremental rebuild occurs.
+ throw new RuntimeException(logMessage("Failed to update views on column indexes " + incomplete + " on indexes " + indexes + '.'));
+ }
+ }
+
+ /**
+ * In case of full rebuild, stop the index build if any index is dropped.
+ * Otherwise, skip dropped indexes to avoid exception during repair/streaming.
+ */
+ private Set<StorageAttachedIndex> validateIndexes(Set<StorageAttachedIndex> indexes, Descriptor descriptor)
+ {
+ Set<StorageAttachedIndex> existing = new HashSet<>();
+ Set<StorageAttachedIndex> dropped = new HashSet<>();
+
+ for (StorageAttachedIndex index : indexes)
+ {
+ if (group.containsIndex(index))
+ existing.add(index);
+ else
+ dropped.add(index);
+ }
+
+ if (!dropped.isEmpty())
+ {
+ String droppedIndexes = dropped.stream().map(sai -> sai.getIndexContext().getIndexName()).collect(Collectors.toList()).toString();
+ if (isFullRebuild)
+ throw new RuntimeException(logMessage(String.format("%s are dropped, will stop index build.", droppedIndexes)));
+ else
+ logger.debug(logMessage("Skip building dropped index {} on sstable {}"), droppedIndexes, descriptor.baseFile());
+ }
+
+ return existing;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java
index a06bdca59f..6d1d49c483 100644
--- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java
+++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexGroup.java
@@ -19,6 +19,7 @@ package org.apache.cassandra.index.sai;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
@@ -27,8 +28,11 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.DecoratedKey;
@@ -40,6 +44,11 @@ import org.apache.cassandra.db.lifecycle.Tracker;
import org.apache.cassandra.db.memtable.Memtable;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+import org.apache.cassandra.index.sai.disk.StorageAttachedIndexWriter;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.format.Version;
+import org.apache.cassandra.index.sai.metrics.IndexGroupMetrics;
import org.apache.cassandra.index.sai.metrics.TableQueryMetrics;
import org.apache.cassandra.index.sai.metrics.TableStateMetrics;
import org.apache.cassandra.index.sai.plan.StorageAttachedIndexQueryPlan;
@@ -47,11 +56,16 @@ import org.apache.cassandra.index.transactions.IndexTransaction;
import org.apache.cassandra.io.sstable.Component;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.notifications.INotification;
import org.apache.cassandra.notifications.INotificationConsumer;
import org.apache.cassandra.notifications.MemtableDiscardedNotification;
import org.apache.cassandra.notifications.MemtableRenewedNotification;
+import org.apache.cassandra.notifications.SSTableAddedNotification;
+import org.apache.cassandra.notifications.SSTableListChangedNotification;
import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.Throwables;
/**
* Orchestrates building of storage-attached indices, and manages lifecycle of resources shared between them.
@@ -59,16 +73,23 @@ import org.apache.cassandra.schema.TableMetadata;
@ThreadSafe
public class StorageAttachedIndexGroup implements Index.Group, INotificationConsumer
{
+ private static final Logger logger = LoggerFactory.getLogger(StorageAttachedIndexGroup.class);
+
private final TableQueryMetrics queryMetrics;
private final TableStateMetrics stateMetrics;
- private final Set<Index> indexes = Sets.newConcurrentHashSet();
+ private final IndexGroupMetrics groupMetrics;
+ private final Set<StorageAttachedIndex> indexes = Sets.newConcurrentHashSet();
private final ColumnFamilyStore baseCfs;
+ private final SSTableContextManager contextManager;
+
StorageAttachedIndexGroup(ColumnFamilyStore baseCfs)
{
this.baseCfs = baseCfs;
this.queryMetrics = new TableQueryMetrics(baseCfs.metadata());
this.stateMetrics = new TableStateMetrics(baseCfs.metadata(), this);
+ this.groupMetrics = new IndexGroupMetrics(baseCfs.metadata(), this);
+ this.contextManager = new SSTableContextManager();
Tracker tracker = baseCfs.getTracker();
tracker.subscribe(this);
@@ -83,14 +104,14 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons
@Override
public Set<Index> getIndexes()
{
- return indexes;
+ return ImmutableSet.copyOf(indexes);
}
@Override
public void addIndex(Index index)
{
assert index instanceof StorageAttachedIndex;
- indexes.add(index);
+ indexes.add((StorageAttachedIndex) index);
}
@Override
@@ -100,10 +121,13 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons
boolean removed = indexes.remove(index);
assert removed : "Cannot remove non-existing index " + index;
/*
- * For the in-memory implementation we only need to unsubscribe from the tracker
+ * per index files are dropped via {@link StorageAttachedIndex#getInvalidateTask()}
*/
if (indexes.isEmpty())
{
+ for (SSTableReader sstable : contextManager.sstables())
+ sstable.unregisterComponents(IndexDescriptor.create(sstable).getLivePerSSTableComponents(), baseCfs.getTracker());
+ deletePerSSTableFiles(baseCfs.getLiveSSTables());
baseCfs.getTracker().unsubscribe(this);
}
}
@@ -113,11 +137,13 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons
{
// in case of dropping table, sstable contexts should already been removed by SSTableListChangedNotification.
queryMetrics.release();
+ groupMetrics.release();
stateMetrics.release();
baseCfs.getTracker().unsubscribe(this);
}
@Override
+ @SuppressWarnings("SuspiciousMethodCalls")
public boolean containsIndex(Index index)
{
return indexes.contains(index);
@@ -165,7 +191,19 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons
@Override
public SSTableFlushObserver getFlushObserver(Descriptor descriptor, LifecycleNewTracker tracker, TableMetadata tableMetadata)
{
- return null;
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(descriptor, tableMetadata.comparator);
+ try
+ {
+ return StorageAttachedIndexWriter.createFlushObserverWriter(indexDescriptor, indexes, tracker);
+ }
+ catch (Throwable t)
+ {
+ String message = "Unable to create storage-attached index writer on SSTable flush." +
+ " All indexes from this table are going to be marked as non-queryable and will need to be rebuilt.";
+ logger.error(indexDescriptor.logMessage(message), t);
+ indexes.forEach(StorageAttachedIndex::makeIndexNonQueryable);
+ return null;
+ }
}
@Override
@@ -181,23 +219,147 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons
return getComponents(indexes);
}
- // TODO: Currently returns an empty set for in-memory only implementation
- static Set<Component> getComponents(Collection<Index> indices)
+ static Set<Component> getComponents(Collection<StorageAttachedIndex> indices)
+ {
+ Set<Component> components = Version.LATEST.onDiskFormat()
+ .perSSTableIndexComponents()
+ .stream()
+ .map(Version.LATEST::makePerSSTableComponent)
+ .collect(Collectors.toSet());
+ indices.forEach(index -> components.addAll(index.getComponents()));
+ return components;
+ }
+
+ // This differs from getComponents in that it only returns index components that exist on disk.
+ // It avoids errors being logged by the SSTable.readTOC method when we have an empty index.
+ @VisibleForTesting
+ public static Set<Component> getLiveComponents(SSTableReader sstable, Collection<StorageAttachedIndex> indices)
{
- return Collections.emptySet();
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable);
+ Set<Component> components = indexDescriptor.getLivePerSSTableComponents();
+ indices.forEach(index -> components.addAll(indexDescriptor.getLivePerIndexComponents(index.getIndexContext())));
+ return components;
}
@Override
public void handleNotification(INotification notification, Object sender)
{
- if (notification instanceof MemtableRenewedNotification)
+ // unfortunately, we can only check the type of notification via instanceof :(
+ if (notification instanceof SSTableAddedNotification)
{
- indexes.forEach(index -> ((StorageAttachedIndex)index).getIndexContext().renewMemtable(((MemtableRenewedNotification) notification).renewed));
+ SSTableAddedNotification notice = (SSTableAddedNotification) notification;
+
+ // Avoid validation for index files just written following Memtable flush.
+ boolean validate = !notice.memtable().isPresent();
+ onSSTableChanged(Collections.emptySet(), notice.added, indexes, validate);
+ }
+ else if (notification instanceof SSTableListChangedNotification)
+ {
+ SSTableListChangedNotification notice = (SSTableListChangedNotification) notification;
+
+ // Avoid validation for index files just written during compaction.
+ onSSTableChanged(notice.removed, notice.added, indexes, false);
+ }
+ else if (notification instanceof MemtableRenewedNotification)
+ {
+ indexes.forEach(index -> index.getIndexContext().getMemtableIndexManager().renewMemtable(((MemtableRenewedNotification) notification).renewed));
}
else if (notification instanceof MemtableDiscardedNotification)
{
- indexes.forEach(index -> ((StorageAttachedIndex)index).getIndexContext().discardMemtable(((MemtableDiscardedNotification) notification).memtable));
+ indexes.forEach(index -> index.getIndexContext().getMemtableIndexManager().discardMemtable(((MemtableDiscardedNotification) notification).memtable));
+ }
+ }
+
+ void deletePerSSTableFiles(Collection<SSTableReader> sstables)
+ {
+ contextManager.release(sstables);
+ sstables.forEach(sstableReader -> IndexDescriptor.create(sstableReader).deletePerSSTableIndexComponents());
+ }
+
+ void dropIndexSSTables(Collection<SSTableReader> ss, StorageAttachedIndex index)
+ {
+ try
+ {
+ index.getIndexContext().drop(ss);
+ }
+ catch (Throwable t)
+ {
+ // Mark the index non-queryable, as its view may be compromised.
+ index.makeIndexNonQueryable();
+
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ /**
+ * This method is synchronized to avoid concurrent initialization tasks validating same per-SSTable files.
+ *
+ * @return the set of column indexes that were marked as non-queryable as a result of their per-SSTable index
+ * files being corrupt or being unable to successfully update their views
+ */
+ synchronized Set<StorageAttachedIndex> onSSTableChanged(Collection<SSTableReader> removed, Iterable<SSTableReader> added,
+ Set<StorageAttachedIndex> indexes, boolean validate)
+ {
+ Pair<Set<SSTableContext>, Set<SSTableReader>> results = contextManager.update(removed, added, validate);
+
+ if (!results.right.isEmpty())
+ {
+ results.right.forEach(sstable -> {
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable);
+ indexDescriptor.deletePerSSTableIndexComponents();
+ // Column indexes are invalid if their SSTable-level components are corrupted so delete
+ // their associated index files and mark them non-queryable.
+ indexes.forEach(index -> {
+ indexDescriptor.deleteColumnIndex(index.getIndexContext());
+ index.makeIndexNonQueryable();
+ });
+ });
+ return indexes;
+ }
+
+ Set<StorageAttachedIndex> incomplete = new HashSet<>();
+
+ for (StorageAttachedIndex index : indexes)
+ {
+ Collection<SSTableContext> invalid = index.getIndexContext().onSSTableChanged(removed, results.left, validate);
+
+ if (!invalid.isEmpty())
+ {
+ // Delete the index files and mark the index non-queryable, as its view may be compromised,
+ // and incomplete, for our callers:
+ invalid.forEach(context -> context.indexDescriptor.deleteColumnIndex(index.getIndexContext()));
+ index.makeIndexNonQueryable();
+ incomplete.add(index);
+ }
}
+ return incomplete;
+ }
+
+ /**
+ * open index files by checking number of {@link SSTableContext} and {@link SSTableIndex},
+ * so transient open files during validation and files that are still open for in-flight requests will not be tracked.
+ *
+ * @return total number of open files for all {@link StorageAttachedIndex}es.
+ */
+ public int openIndexFiles()
+ {
+ return contextManager.openFiles() + indexes.stream().mapToInt(index -> index.getIndexContext().openPerIndexFiles()).sum();
+ }
+
+ /**
+ * @return total disk usage (in bytes) of all per-sstable index files
+ */
+ public long diskUsage()
+ {
+ return contextManager.diskUsage();
+ }
+
+ /**
+ * @return count of indexes building
+ */
+ public int totalIndexBuildsInProgress()
+ {
+ return (int) indexes.stream().filter(i -> baseCfs.indexManager.isIndexBuilding(i.getIndexMetadata().name)).count();
}
/**
@@ -216,6 +378,15 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons
return indexes.size();
}
+ /**
+ * @return total disk usage of all per-sstable index files and per-column index files
+ */
+ public long totalDiskUsage()
+ {
+ return diskUsage() + indexes.stream().flatMap(index -> index.getIndexContext().getView().getIndexes().stream())
+ .mapToLong(SSTableIndex::sizeOfPerColumnComponents).sum();
+ }
+
public TableMetadata metadata()
{
return baseCfs.metadata();
@@ -226,12 +397,31 @@ public class StorageAttachedIndexGroup implements Index.Group, INotificationCons
return baseCfs;
}
+ @VisibleForTesting
+ public SSTableContextManager sstableContextManager()
+ {
+ return contextManager;
+ }
+
+ /**
+ * simulate index loading on restart with index file validation
+ */
+ @VisibleForTesting
+ public void unsafeReload()
+ {
+ contextManager.clear();
+ onSSTableChanged(baseCfs.getLiveSSTables(), Collections.emptySet(), indexes, false);
+ onSSTableChanged(Collections.emptySet(), baseCfs.getLiveSSTables(), indexes, true);
+ }
+
/**
* Simulate the index going through a restart of node
*/
@VisibleForTesting
public void reset()
{
- indexes.forEach(i -> ((StorageAttachedIndex) i).makeIndexNonQueryable());
+ contextManager.clear();
+ indexes.forEach(StorageAttachedIndex::makeIndexNonQueryable);
+ onSSTableChanged(baseCfs.getLiveSSTables(), Collections.emptySet(), indexes, false);
}
}
diff --git a/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java b/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java
index 64a2f2fed5..d1e7bae2fe 100644
--- a/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java
+++ b/src/java/org/apache/cassandra/index/sai/analyzer/AbstractAnalyzer.java
@@ -28,7 +28,6 @@ import org.apache.cassandra.db.marshal.AbstractType;
public abstract class AbstractAnalyzer implements Iterator<ByteBuffer>
{
protected ByteBuffer next = null;
- String nextLiteral = null;
/**
* @return true if index value is transformed, e.g. normalized or lower-cased or tokenized.
@@ -66,7 +65,6 @@ public abstract class AbstractAnalyzer implements Iterator<ByteBuffer>
public void reset(ByteBuffer input)
{
this.next = null;
- this.nextLiteral = null;
resetInternal(input);
}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/IndexSearchResultIterator.java b/src/java/org/apache/cassandra/index/sai/disk/IndexSearchResultIterator.java
new file mode 100644
index 0000000000..f10bf5b8ae
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/IndexSearchResultIterator.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+
+public class IndexSearchResultIterator extends KeyRangeIterator
+{
+ private static final Logger logger = LoggerFactory.getLogger(IndexSearchResultIterator.class);
+
+ private final QueryContext context;
+ private final KeyRangeIterator union;
+ private final Collection<SSTableIndex> referencedIndexes;
+
+ private IndexSearchResultIterator(KeyRangeIterator union, Collection<SSTableIndex> referencedIndexes, QueryContext queryContext)
+ {
+ super(union.getMinimum(), union.getMaximum(), union.getCount());
+
+ this.union = union;
+ this.referencedIndexes = referencedIndexes;
+ this.context = queryContext;
+ }
+
+ /**
+ * Builds a new {@link IndexSearchResultIterator} that wraps a {@link KeyRangeUnionIterator} over the
+ * results of searching the {@link org.apache.cassandra.index.sai.memory.MemtableIndex} and the {@link SSTableIndex}es.
+ */
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public static IndexSearchResultIterator build(Expression expression,
+ Collection<SSTableIndex> sstableIndexes,
+ AbstractBounds<PartitionPosition> keyRange,
+ QueryContext queryContext)
+ {
+ List<KeyRangeIterator> subIterators = new ArrayList<>(1 + sstableIndexes.size());
+
+ KeyRangeIterator memtableIterator = expression.context.getMemtableIndexManager().searchMemtableIndexes(expression, keyRange);
+ if (memtableIterator != null)
+ subIterators.add(memtableIterator);
+
+ for (SSTableIndex sstableIndex : sstableIndexes)
+ {
+ try
+ {
+ queryContext.checkpoint();
+ queryContext.sstablesHit++;
+
+ if (sstableIndex.isReleased())
+ throw new IllegalStateException(sstableIndex.getIndexContext().logMessage("Index was released from the view during the query"));
+
+ List<KeyRangeIterator> segmentIterators = sstableIndex.search(expression, keyRange, queryContext);
+
+ if (!segmentIterators.isEmpty())
+ subIterators.addAll(segmentIterators);
+ }
+ catch (Throwable e)
+ {
+ if (!(e instanceof QueryCancelledException))
+ logger.debug(sstableIndex.getIndexContext().logMessage(String.format("Failed search an index %s, aborting query.", sstableIndex.getSSTable())), e);
+
+ throw Throwables.cleaned(e);
+ }
+ }
+
+ KeyRangeIterator union = KeyRangeUnionIterator.build(subIterators);
+ return new IndexSearchResultIterator(union, sstableIndexes, queryContext);
+ }
+
+ protected PrimaryKey computeNext()
+ {
+ try
+ {
+ return union.hasNext() ? union.next() : endOfData();
+ }
+ finally
+ {
+ context.checkpoint();
+ }
+ }
+
+ protected void performSkipTo(PrimaryKey nextKey)
+ {
+ try
+ {
+ union.skipTo(nextKey);
+ }
+ finally
+ {
+ context.checkpoint();
+ }
+ }
+
+ public void close()
+ {
+ FileUtils.closeQuietly(union);
+ referencedIndexes.forEach(IndexSearchResultIterator::releaseQuietly);
+ referencedIndexes.clear();
+ }
+
+ private static void releaseQuietly(SSTableIndex index)
+ {
+ try
+ {
+ index.release();
+ }
+ catch (Throwable e)
+ {
+ logger.error(index.getIndexContext().logMessage(String.format("Failed to release index on SSTable %s", index.getSSTable())), e);
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/index/sai/memory/InMemoryKeyRangeIteratorTest.java b/src/java/org/apache/cassandra/index/sai/disk/PerColumnIndexWriter.java
similarity index 50%
copy from test/unit/org/apache/cassandra/index/sai/memory/InMemoryKeyRangeIteratorTest.java
copy to src/java/org/apache/cassandra/index/sai/disk/PerColumnIndexWriter.java
index 71db5c03d8..4ce75cbe08 100644
--- a/test/unit/org/apache/cassandra/index/sai/memory/InMemoryKeyRangeIteratorTest.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/PerColumnIndexWriter.java
@@ -15,24 +15,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.cassandra.index.sai.memory;
+package org.apache.cassandra.index.sai.disk;
-import java.util.Arrays;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.io.IOException;
+import com.google.common.base.Stopwatch;
+
+import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.index.sai.utils.PrimaryKey;
-import org.apache.cassandra.index.sai.utils.KeyRangeIterator;
-public class InMemoryKeyRangeIteratorTest extends AbstractInMemoryKeyRangeIteratorTester
+/**
+ * Creates the on-disk components for a given column index.
+ */
+public interface PerColumnIndexWriter
{
- @Override
- protected KeyRangeIterator makeIterator(long minimumTokenValue, long maximumTokenValue, long... tokens)
- {
- SortedSet<PrimaryKey> set = new TreeSet<>();
+ /**
+ * Adds a row to this index.
+ */
+ void addRow(PrimaryKey key, Row row, long sstableRowId) throws IOException;
- Arrays.stream(tokens).forEach(t -> set.add(keyForToken(t)));
+ /**
+ * Builds on-disk index data structures from accumulated data, moves them all to the filesystem, and fsync created files.
+ */
+ void complete(Stopwatch stopwatch) throws IOException;
- return new InMemoryKeyRangeIterator(set);
- }
+ /**
+ * Aborts accumulating data. Allows to clean up resources on error.
+ *
+ * Note: Implementations should be idempotent, i.e. safe to call multiple times without producing undesirable side-effects.
+ */
+ void abort(Throwable cause);
}
diff --git a/src/java/org/apache/cassandra/index/sai/virtual/StorageAttachedIndexTables.java b/src/java/org/apache/cassandra/index/sai/disk/PerSSTableIndexWriter.java
similarity index 64%
copy from src/java/org/apache/cassandra/index/sai/virtual/StorageAttachedIndexTables.java
copy to src/java/org/apache/cassandra/index/sai/disk/PerSSTableIndexWriter.java
index 0d4f12678f..09c8a34a9e 100644
--- a/src/java/org/apache/cassandra/index/sai/virtual/StorageAttachedIndexTables.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/PerSSTableIndexWriter.java
@@ -15,21 +15,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.cassandra.index.sai.disk;
-package org.apache.cassandra.index.sai.virtual;
+import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
-import org.apache.cassandra.db.virtual.VirtualTable;
-
-public class StorageAttachedIndexTables
+/**
+ * Writes all SSTable-attached index files. The files written are version specific.
+ */
+public interface PerSSTableIndexWriter
{
- private StorageAttachedIndexTables()
+ PerSSTableIndexWriter NONE = (key) -> {};
+
+ void nextRow(PrimaryKey primaryKey) throws IOException;
+
+ default void complete() throws IOException
{}
- public static Collection<VirtualTable> getAll(String keyspace)
- {
- return Collections.singletonList(new IndexesSystemView(keyspace));
- }
+ default void abort()
+ {}
}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMap.java
new file mode 100644
index 0000000000..fa47b9f4a4
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/PrimaryKeyMap.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+
+/**
+ * A bidirectional map of {@link PrimaryKey} to row ID. Implementations of this interface
+ * are not expected to be threadsafe.
+ */
+@NotThreadSafe
+public interface PrimaryKeyMap extends Closeable
+{
+ /**
+ * A factory for creating {@link PrimaryKeyMap} instances. Implementations of this
+ * interface are expected to be threadsafe.
+ */
+ @NotThreadSafe
+ interface Factory extends Closeable
+ {
+ /**
+ * Creates a new {@link PrimaryKeyMap} instance
+ *
+ * @return a {@link PrimaryKeyMap}
+ * @throws IOException if the {@link PrimaryKeyMap} couldn't be created
+ */
+ PrimaryKeyMap newPerSSTablePrimaryKeyMap() throws IOException;
+
+ @Override
+ default void close()
+ {
+ }
+ }
+
+ /**
+ * Returns a {@link PrimaryKey} for a row ID
+ *
+ * @param sstableRowId the row ID to lookup
+ * @return the {@link PrimaryKey} associated with the row ID
+ */
+ PrimaryKey primaryKeyFromRowId(long sstableRowId);
+
+ /**
+ * Returns a row ID for a {@link PrimaryKey}
+ *
+ * @param key the {@link PrimaryKey} to lookup
+ * @return the row ID associated with the {@link PrimaryKey}
+ */
+ long rowIdFromPrimaryKey(PrimaryKey key);
+
+ @Override
+ default void close()
+ {
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java b/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java
new file mode 100644
index 0000000000..fadbae5540
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk;
+
+import java.util.Collections;
+import java.util.Iterator;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.carrotsearch.hppc.LongArrayList;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.rows.RangeTombstoneMarker;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.index.sai.memory.MemtableIndex;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.index.sai.utils.PrimaryKeys;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.utils.AbstractGuavaIterator;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+
+/**
+ * In memory representation of {@link PrimaryKey} to row ID mappings which only contains
+ * {@link Row} regardless of whether it's live or deleted. ({@link RangeTombstoneMarker} is not included.)
+ *
+ * While this inherits the threading behaviour of {@link InMemoryTrie} of single-writer / multiple-reader,
+ * since it is only used by {@link StorageAttachedIndexWriter}, which is not threadsafe, we can consider
+ * this class not threadsafe as well.
+ */
+@NotThreadSafe
+public class RowMapping
+{
+ private static final InMemoryTrie.UpsertTransformer<Long, Long> OVERWRITE_TRANSFORMER = (existing, update) -> update;
+
+ public static final RowMapping DUMMY = new RowMapping()
+ {
+ @Override
+ public Iterator<Pair<ByteComparable, LongArrayList>> merge(MemtableIndex index) { return Collections.emptyIterator(); }
+
+ @Override
+ public void complete() {}
+
+ @Override
+ public boolean isComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public void add(PrimaryKey key, long sstableRowId) {}
+ };
+
+ private final InMemoryTrie<Long> rowMapping = new InMemoryTrie<>(BufferType.OFF_HEAP);
+
+ private boolean complete = false;
+
+ public PrimaryKey minKey;
+ public PrimaryKey maxKey;
+
+ public long maxSSTableRowId = -1;
+
+ private RowMapping()
+ {}
+
+ /**
+ * Create row mapping for FLUSH operation only.
+ */
+ public static RowMapping create(OperationType opType)
+ {
+ if (opType == OperationType.FLUSH)
+ return new RowMapping();
+ return DUMMY;
+ }
+
+ /**
+ * Link the term -> {@link PrimaryKeys} mappings from a provided {@link MemtableIndex} to
+ * the {@link PrimaryKey} -> row ID mappings maintained here in {@link #rowMapping} to produce
+ * mappings of terms to their postings lists.
+ *
+ * @param index a Memtable-attached column index
+ *
+ * @return an iterator of term -> postings list {@link Pair}s
+ */
+ public Iterator<Pair<ByteComparable, LongArrayList>> merge(MemtableIndex index)
+ {
+ assert complete : "RowMapping is not built.";
+
+ Iterator<Pair<ByteComparable, PrimaryKeys>> iterator = index.iterator();
+ return new AbstractGuavaIterator<Pair<ByteComparable, LongArrayList>>()
+ {
+ @Override
+ protected Pair<ByteComparable, LongArrayList> computeNext()
+ {
+ while (iterator.hasNext())
+ {
+ Pair<ByteComparable, PrimaryKeys> pair = iterator.next();
+
+ LongArrayList postings = null;
+ Iterator<PrimaryKey> primaryKeys = pair.right.iterator();
+
+ while (primaryKeys.hasNext())
+ {
+ PrimaryKey primaryKey = primaryKeys.next();
+ ByteComparable byteComparable = primaryKey::asComparableBytes;
+ Long sstableRowId = rowMapping.get(byteComparable);
+
+ // The in-memory index does not handle deletions, so it is possible to
+ // have a primary key in the index that doesn't exist in the row mapping
+ if (sstableRowId != null)
+ {
+ postings = postings == null ? new LongArrayList() : postings;
+ postings.add(sstableRowId);
+ }
+ }
+ if (postings != null)
+ return Pair.create(pair.left, postings);
+ }
+ return endOfData();
+ }
+ };
+ }
+
+ /**
+ * Complete building in memory RowMapping, mark it as immutable.
+ */
+ public void complete()
+ {
+ assert !complete : "RowMapping can only be built once.";
+ this.complete = true;
+ }
+
+ public boolean isComplete()
+ {
+ return complete;
+ }
+
+ /**
+ * Include PrimaryKey to RowId mapping
+ */
+ public void add(PrimaryKey key, long sstableRowId) throws InMemoryTrie.SpaceExhaustedException
+ {
+ assert !complete : "Cannot modify built RowMapping.";
+
+ ByteComparable byteComparable = key::asComparableBytes;
+ rowMapping.putSingleton(byteComparable, sstableRowId, OVERWRITE_TRANSFORMER);
+
+ maxSSTableRowId = Math.max(maxSSTableRowId, sstableRowId);
+
+ // data is written in token sorted order
+ if (minKey == null)
+ minKey = key;
+ maxKey = key;
+ }
+
+ public boolean hasRows()
+ {
+ return maxSSTableRowId >= 0;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/SSTableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/SSTableIndex.java
new file mode 100644
index 0000000000..2227291626
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/SSTableIndex.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.sai.disk;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.virtual.SimpleDataSet;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.SSTableContext;
+import org.apache.cassandra.index.sai.disk.format.Version;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.io.sstable.SSTableIdFactory;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+
+/**
+ * A reference-counted container of a {@link SSTableReader} for each column index that:
+ * <ul>
+ * <li>Manages references to the SSTable for each query</li>
+ * <li>Exposes a version agnostic searcher onto the column index</li>
+ * <li>Exposes the index metadata for the column index</li>
+ * </ul>
+ */
+public abstract class SSTableIndex
+{
+ private static final Logger logger = LoggerFactory.getLogger(SSTableIndex.class);
+
+ // sort sstable indexes by first key, then last key, then descriptor id
+ public static final Comparator<SSTableIndex> COMPARATOR = Comparator.comparing((SSTableIndex s) -> s.getSSTable().first)
+ .thenComparing(s -> s.getSSTable().last)
+ .thenComparing(s -> s.getSSTable().descriptor.id, SSTableIdFactory.COMPARATOR);
+
+ protected final SSTableContext sstableContext;
+ protected final IndexContext indexContext;
+
+ private final AtomicInteger references = new AtomicInteger(1);
+ private final AtomicBoolean obsolete = new AtomicBoolean(false);
+
+ public SSTableIndex(SSTableContext sstableContext, IndexContext indexContext)
+ {
+ assert indexContext.getValidator() != null;
+
+ this.sstableContext = sstableContext.sharedCopy(); // this line must not be before any code that may throw
+ this.indexContext = indexContext;
+ }
+
+ /**
+ * Returns the amount of memory occupied by the index when it is initially loaded.
+ * This is the amount of data loaded into internal memory buffers by the index and
+ * does include the class footprint overhead. It used by the index metrics.
+ */
+ public abstract long indexFileCacheSize();
+
+ /**
+ * Returns the number of indexed rows in the index. This comes from the index
+ * metadata created when the index was written and is used by the index metrics.
+ */
+ public abstract long getRowCount();
+
+ /**
+ * Returns the minimum indexed rowId for the index. This comes from the index
+ * metadata created when the index was written and is used by the index metrics.
+ */
+ public abstract long minSSTableRowId();
+
+ /**
+ * Returns the maximum indexed rowId for the index. This comes from the index
+ * metadata created when the index was written and is used by the index metrics.
+ */
+ public abstract long maxSSTableRowId();
+
+ /**
+ * Returns the minimum term held in the index based on the natural sort order of
+ * the index column type comparator. It comes from the index metadata created when
+ * the index was written and is used by the index metrics and used in queries to
+ * determine whether a term, or range or terms, exists in the index.
+ */
+ public abstract ByteBuffer minTerm();
+
+ /**
+ * Returns the maximum term held in the index based on the natural sort order of
+ * the index column type comparator. It comes from the index metadata created when
+ * the index was written and is used by the index metrics and used in queries to
+ * determine whether a term, or range or terms, exists in the index.
+ */
+ public abstract ByteBuffer maxTerm();
+
+ /**
+ * Returns the key bounds of the index. It is created from the minimum and
+ * maximum keys held in the metadata and is used to determine whether
+ * sstable indexes overlap or not.
+ */
+ public abstract AbstractBounds<PartitionPosition> bounds();
+
+ /**
+ * Perform a search on the index for a single expression and keyRange.
+ *
+ * The result is a {@link List} of {@link KeyRangeIterator} because there will
+ * be a {@link KeyRangeIterator} for each segment in the index. The result
+ * will never be null but may be an empty {@link List}.
+ *
+ * @param expression The {@link Expression} to be searched for
+ * @param keyRange The {@link AbstractBounds<PartitionPosition>} defining the
+ * token range for the search
+ * @param context The {@link QueryContext} holding the per-query state
+ * @return a {@link List} of {@link KeyRangeIterator}s containing the results
+ * of the search
+ */
+ public abstract List<KeyRangeIterator> search(Expression expression,
+ AbstractBounds<PartitionPosition> keyRange,
+ QueryContext context) throws IOException;
+
+ /**
+ * Populates a virtual table using the index metadata owned by the index
+ */
+ public abstract void populateSegmentView(SimpleDataSet dataSet);
+
+ protected abstract void internalRelease();
+
+ /**
+ * @return total size of per-column index components, in bytes
+ */
+ public long sizeOfPerColumnComponents()
+ {
+ return sstableContext.indexDescriptor.sizeOnDiskOfPerIndexComponents(indexContext);
+ }
+
+ public IndexContext getIndexContext()
+ {
+ return indexContext;
+ }
+
+ public SSTableContext getSSTableContext()
+ {
+ return sstableContext;
+ }
+
+ public Version getVersion()
+ {
+ return sstableContext.indexDescriptor.version;
+ }
+
+ public SSTableReader getSSTable()
+ {
+ return sstableContext.sstable;
+ }
+
+ public boolean reference()
+ {
+ while (true)
+ {
+ int n = references.get();
+ if (n <= 0)
+ return false;
+ if (references.compareAndSet(n, n + 1))
+ {
+ return true;
+ }
+ }
+ }
+
+ public boolean isReleased()
+ {
+ return references.get() <= 0;
+ }
+
+ public void releaseQuietly()
+ {
+ try
+ {
+ release();
+ }
+ catch (Throwable e)
+ {
+ logger.error(getIndexContext().logMessage("Failed to release index on SSTable {}"), getSSTable().descriptor, e);
+ }
+ }
+
+ public void release()
+ {
+ int n = references.decrementAndGet();
+
+ if (n == 0)
+ {
+ internalRelease();
+ sstableContext.close();
+
+ /*
+ * When SSTable is removed, storage-attached index components will be automatically removed by LogTransaction.
+ * We only remove index components explicitly in case of index corruption or index rebuild.
+ */
+ if (obsolete.get())
+ {
+ sstableContext.indexDescriptor.deleteColumnIndex(indexContext);
+ }
+ }
+ }
+
+ public void markObsolete()
+ {
+ obsolete.getAndSet(true);
+ release();
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SSTableIndex other = (SSTableIndex)o;
+ return Objects.equal(sstableContext, other.sstableContext) && Objects.equal(indexContext, other.indexContext);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(sstableContext, indexContext);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("column", indexContext.getColumnName())
+ .add("sstable", sstableContext.sstable.descriptor)
+ .add("minTerm", indexContext.getValidator().getString(minTerm()))
+ .add("maxTerm", indexContext.getValidator().getString(maxTerm()))
+ .add("totalRows", sstableContext.sstable.getTotalRows())
+ .toString();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java
new file mode 100644
index 0000000000..f315e0dbba
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/StorageAttachedIndexWriter.java
@@ -0,0 +1,258 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.io.sstable.SSTableFlushObserver;
+import org.apache.cassandra.utils.Throwables;
+
+/**
+ * Writes all on-disk index structures attached to a given SSTable.
+ */
+@NotThreadSafe
+public class StorageAttachedIndexWriter implements SSTableFlushObserver
+{
+ private static final Logger logger = LoggerFactory.getLogger(StorageAttachedIndexWriter.class);
+
+ private final IndexDescriptor indexDescriptor;
+ private final Collection<StorageAttachedIndex> indexes;
+ private final Collection<PerColumnIndexWriter> perIndexWriters;
+ private final PerSSTableIndexWriter perSSTableWriter;
+ private final Stopwatch stopwatch = Stopwatch.createUnstarted();
+ private final RowMapping rowMapping;
+ private final boolean propagateErrors;
+
+ private DecoratedKey currentKey;
+ private boolean tokenOffsetWriterCompleted = false;
+ private boolean aborted = false;
+
+ private long sstableRowId = 0;
+
+ public static StorageAttachedIndexWriter createFlushObserverWriter(IndexDescriptor indexDescriptor,
+ Collection<StorageAttachedIndex> indexes,
+ LifecycleNewTracker lifecycleNewTracker) throws IOException
+ {
+ return new StorageAttachedIndexWriter(indexDescriptor, indexes, lifecycleNewTracker, false, false);
+
+ }
+
+ public static StorageAttachedIndexWriter createBuilderWriter(IndexDescriptor indexDescriptor,
+ Collection<StorageAttachedIndex> indexes,
+ LifecycleNewTracker lifecycleNewTracker,
+ boolean perIndexComponentsOnly) throws IOException
+ {
+ return new StorageAttachedIndexWriter(indexDescriptor, indexes, lifecycleNewTracker, perIndexComponentsOnly, true);
+ }
+
+ private StorageAttachedIndexWriter(IndexDescriptor indexDescriptor,
+ Collection<StorageAttachedIndex> indexes,
+ LifecycleNewTracker lifecycleNewTracker,
+ boolean perIndexComponentsOnly,
+ boolean propagateErrors) throws IOException
+ {
+ this.indexDescriptor = indexDescriptor;
+ this.indexes = indexes;
+ this.rowMapping = RowMapping.create(lifecycleNewTracker.opType());
+ this.propagateErrors = propagateErrors;
+ this.perIndexWriters = indexes.stream().map(index -> indexDescriptor.newPerColumnIndexWriter(index,
+ lifecycleNewTracker,
+ rowMapping))
+ .filter(Objects::nonNull) // a null here means the column had no data to flush
+ .collect(Collectors.toList());
+
+ // If the SSTable components are already being built by another index build then we don't want
+ // to build them again so use a null writer
+ this.perSSTableWriter = perIndexComponentsOnly ? PerSSTableIndexWriter.NONE : indexDescriptor.newPerSSTableIndexWriter();
+ }
+
+ @Override
+ public void begin()
+ {
+ logger.debug(indexDescriptor.logMessage("Starting partition iteration for storage-attached index flush for SSTable {}..."), indexDescriptor.sstableDescriptor);
+ stopwatch.start();
+ }
+
+ @Override
+ public void startPartition(DecoratedKey key, long keyPosition, long keyPositionForSASI)
+ {
+ if (aborted) return;
+
+ currentKey = key;
+ }
+
+ @Override
+ public void nextUnfilteredCluster(Unfiltered unfiltered)
+ {
+ if (aborted) return;
+
+ // Ignore range tombstones...
+ if (!unfiltered.isRow())
+ return;
+
+ try
+ {
+ addRow((Row)unfiltered);
+ }
+ catch (Throwable t)
+ {
+ logger.error(indexDescriptor.logMessage("Failed to record a row during an index build"), t);
+ abort(t, true);
+ }
+ }
+
+ @Override
+ public void staticRow(Row staticRow)
+ {
+ if (aborted) return;
+
+ if (staticRow.isEmpty())
+ return;
+
+ try
+ {
+ addRow(staticRow);
+ }
+ catch (Throwable t)
+ {
+ logger.error(indexDescriptor.logMessage("Failed to record a static row during an index build"), t);
+ abort(t, true);
+ }
+ }
+
+ @Override
+ public void complete()
+ {
+ if (aborted) return;
+
+ long start = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+
+ logger.debug(indexDescriptor.logMessage("Completed partition iteration for index flush for SSTable {}. Elapsed time: {} ms"),
+ indexDescriptor.sstableDescriptor, start);
+
+ try
+ {
+ perSSTableWriter.complete();
+ tokenOffsetWriterCompleted = true;
+
+ long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+
+ logger.debug(indexDescriptor.logMessage("Completed per-SSTable write for SSTable {}. Duration: {} ms. Total elapsed time: {} ms."),
+ indexDescriptor.sstableDescriptor, elapsed - start, elapsed);
+
+ start = elapsed;
+
+ rowMapping.complete();
+
+ for (PerColumnIndexWriter perIndexWriter : perIndexWriters)
+ {
+ perIndexWriter.complete(stopwatch);
+ }
+ elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+ logger.debug(indexDescriptor.logMessage("Completed per-index writes for SSTable {}. Duration: {} ms. Total elapsed time: {} ms."),
+ indexDescriptor.sstableDescriptor, elapsed - start, elapsed);
+ }
+ catch (Throwable t)
+ {
+ logger.error(indexDescriptor.logMessage("Failed to complete an index build"), t);
+ abort(t, true);
+ }
+ }
+
+ /**
+ * Aborts all column index writers and, only if they have not yet completed, SSTable-level component writers.
+ *
+ * @param accumulator the initial exception thrown from the failed writer
+ */
+ @Override
+ public void abort(Throwable accumulator)
+ {
+ abort(accumulator, false);
+ }
+
+ /**
+ *
+ * @param accumulator original cause of the abort
+ * @param fromIndex true if the cause of the abort was the index itself, false otherwise
+ */
+ public void abort(Throwable accumulator, boolean fromIndex)
+ {
+ // Mark the write operation aborted, so we can short-circuit any further operations on the component writers.
+ aborted = true;
+
+ // Make any indexes involved in this transaction non-queryable, as they will likely not match the backing table.
+ if (fromIndex)
+ indexes.forEach(StorageAttachedIndex::makeIndexNonQueryable);
+
+ for (PerColumnIndexWriter perIndexWriter : perIndexWriters)
+ {
+ try
+ {
+ perIndexWriter.abort(accumulator);
+ }
+ catch (Throwable t)
+ {
+ if (accumulator != null)
+ {
+ accumulator.addSuppressed(t);
+ }
+ }
+ }
+
+ if (!tokenOffsetWriterCompleted)
+ {
+ // If the token/offset files have already been written successfully, they can be reused later.
+ perSSTableWriter.abort();
+ }
+
+ // If the abort was from an index error and this is part of an index build operation then
+ // propagate the error up to the SecondaryIndexManager, so it can be correctly marked as failed.
+ if (fromIndex && propagateErrors)
+ throw Throwables.unchecked(accumulator);
+ }
+
+ private void addRow(Row row) throws IOException, InMemoryTrie.SpaceExhaustedException
+ {
+ PrimaryKey primaryKey = indexDescriptor.primaryKeyFactory.create(currentKey, row.clustering());
+ perSSTableWriter.nextRow(primaryKey);
+ rowMapping.add(primaryKey, sstableRowId);
+
+ for (PerColumnIndexWriter w : perIndexWriters)
+ {
+ w.addRow(primaryKey, row, sstableRowId);
+ }
+ sstableRowId++;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java
new file mode 100644
index 0000000000..670ba82c09
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.format;
+
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryWriter;
+
+/**
+ * This is a definitive list of all the on-disk components for all versions
+ */
+public enum IndexComponent
+{
+ /**
+ * Metadata for per-column index components
+ */
+ META("Meta"),
+
+ /**
+ * Term dictionary written by {@link TrieTermsDictionaryWriter} stores mappings of term and
+ * file pointer to posting block on posting file.
+ */
+ TERMS_DATA("TermsData"),
+
+ /**
+ * Stores postings written by {@link PostingsWriter}
+ */
+ POSTING_LISTS("PostingLists"),
+
+ /**
+ * If present indicates that the column index build completed successfully
+ */
+ COLUMN_COMPLETION_MARKER("ColumnComplete"),
+
+
+ // per-sstable components
+ /**
+ * Partition key token value for rows including row tombstone and static row. (access key is rowId)
+ */
+ TOKEN_VALUES("TokenValues"),
+
+ /**
+ * An on-disk trie containing the primary keys used for looking up the rowId from a partition key
+ */
+ PRIMARY_KEY_TRIE("PrimaryKeyTrie"),
+
+ /**
+ * Prefix-compressed blocks of primary keys used for rowId to partition key lookups
+ */
+ PRIMARY_KEY_BLOCKS("PrimaryKeyBlocks"),
+
+ /**
+ * Encoded sequence of offsets to primary key blocks
+ */
+ PRIMARY_KEY_BLOCK_OFFSETS("PrimaryKeyBlockOffsets"),
+
+ /**
+ * Metadata for per-SSTable on-disk components.
+ */
+ GROUP_META("GroupMeta"),
+
+ /**
+ * If present indicates that the per-sstable index build completed successfully
+ */
+ GROUP_COMPLETION_MARKER("GroupComplete");
+
+ public final String name;
+
+ IndexComponent(String name)
+ {
+ this.name = name;
+ }
+}
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
new file mode 100644
index 0000000000..7261671eca
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/format/IndexDescriptor.java
@@ -0,0 +1,433 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.format;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.io.Files;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.SSTableContext;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter;
+import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.RowMapping;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.io.sstable.Component;
+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.io.util.FileHandle;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.IOUtils;
+
+/**
+ * The {@link IndexDescriptor} is an analog of the SSTable {@link Descriptor} and provides version
+ * specific information about the on-disk state of a {@link StorageAttachedIndex}.
+ *
+ * The {@link IndexDescriptor} is primarily responsible for maintaining a view of the on-disk state
+ * of an index for a specific {@link org.apache.cassandra.io.sstable.SSTable}.
+ *
+ * It is responsible for opening files for use by writers and readers.
+ *
+ * Its remaining responsibility is to act as a proxy to the {@link OnDiskFormat} associated with the
+ * index {@link Version}.
+ */
+public class IndexDescriptor
+{
+ private static final Logger logger = LoggerFactory.getLogger(IndexDescriptor.class);
+
+ public final Version version;
+ public final Descriptor sstableDescriptor;
+ public final ClusteringComparator clusteringComparator;
+ public final PrimaryKey.Factory primaryKeyFactory;
+
+ private IndexDescriptor(Version version, Descriptor sstableDescriptor, ClusteringComparator clusteringComparator)
+ {
+ this.version = version;
+ this.sstableDescriptor = sstableDescriptor;
+ this.clusteringComparator = clusteringComparator;
+ this.primaryKeyFactory = new PrimaryKey.Factory(clusteringComparator);
+ }
+
+ public static IndexDescriptor create(Descriptor descriptor, ClusteringComparator clusteringComparator)
+ {
+ return new IndexDescriptor(Version.LATEST, descriptor, clusteringComparator);
+ }
+
+ public static IndexDescriptor create(SSTableReader sstable)
+ {
+ for (Version version : Version.ALL)
+ {
+ IndexDescriptor indexDescriptor = new IndexDescriptor(version,
+ sstable.descriptor,
+ sstable.metadata().comparator);
+
+ if (version.onDiskFormat().isPerSSTableIndexBuildComplete(indexDescriptor))
+ {
+ return indexDescriptor;
+ }
+ }
+ return new IndexDescriptor(Version.LATEST,
+ sstable.descriptor,
+ sstable.metadata().comparator);
+ }
+
+ public String componentName(IndexComponent indexComponent)
+ {
+ return version.fileNameFormatter().format(indexComponent, null);
+ }
+
+ public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(SSTableReader sstable)
+ {
+ return version.onDiskFormat().newPrimaryKeyMapFactory(this, sstable);
+ }
+
+ public SSTableIndex newSSTableIndex(SSTableContext sstableContext, IndexContext indexContext)
+ {
+ return version.onDiskFormat().newSSTableIndex(sstableContext, indexContext);
+ }
+
+ public PerSSTableIndexWriter newPerSSTableIndexWriter() throws IOException
+ {
+ return version.onDiskFormat().newPerSSTableIndexWriter(this);
+ }
+
+ public PerColumnIndexWriter newPerColumnIndexWriter(StorageAttachedIndex index,
+ LifecycleNewTracker tracker,
+ RowMapping rowMapping)
+ {
+ return version.onDiskFormat().newPerColumnIndexWriter(index, this, tracker, rowMapping);
+ }
+
+ public boolean isPerSSTableIndexBuildComplete()
+ {
+ return version.onDiskFormat().isPerSSTableIndexBuildComplete(this);
+ }
+
+ public boolean isPerColumnIndexBuildComplete(IndexContext indexContext)
+ {
+ return version.onDiskFormat().isPerColumnIndexBuildComplete(this, indexContext);
+ }
+
+ public boolean hasComponent(IndexComponent indexComponent)
+ {
+ return fileFor(indexComponent).exists();
+ }
+
+ public boolean hasComponent(IndexComponent indexComponent, IndexContext indexContext)
+ {
+ return fileFor(indexComponent, indexContext).exists();
+ }
+
+ public File fileFor(IndexComponent indexComponent)
+ {
+ return createFile(indexComponent, null);
+ }
+
+ public File fileFor(IndexComponent indexComponent, IndexContext indexContext)
+ {
+ return createFile(indexComponent, indexContext);
+ }
+
+ public boolean isIndexEmpty(IndexContext indexContext)
+ {
+ // The index is empty if the index build completed successfully in that both
+ // a GROUP_COMPLETION_MARKER companent and a COLUMN_COMPLETION_MARKER exist for
+ // the index and the number of per-index components is 1 indicating that only the
+ // COLUMN_COMPLETION_MARKER exists for the index, as this is the only file that
+ // will be written if the index is empty
+ return isPerColumnIndexBuildComplete(indexContext) && numberOfPerIndexComponents(indexContext) == 1;
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ public void createComponentOnDisk(IndexComponent component) throws IOException
+ {
+ Files.touch(fileFor(component).toJavaIOFile());
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ public void createComponentOnDisk(IndexComponent component, IndexContext indexContext) throws IOException
+ {
+ Files.touch(fileFor(component, indexContext).toJavaIOFile());
+ }
+
+ public IndexInput openPerSSTableInput(IndexComponent indexComponent)
+ {
+ File file = fileFor(indexComponent);
+ if (logger.isTraceEnabled())
+ logger.trace(logMessage("Opening blocking index input for file {} ({})"),
+ file,
+ FBUtilities.prettyPrintMemory(file.length()));
+
+ return IndexFileUtils.instance.openBlockingInput(file);
+ }
+
+ public IndexInput openPerIndexInput(IndexComponent indexComponent, IndexContext indexContext)
+ {
+ final File file = fileFor(indexComponent, indexContext);
+ if (logger.isTraceEnabled())
+ logger.trace(logMessage("Opening blocking index input for file {} ({})"),
+ file,
+ FBUtilities.prettyPrintMemory(file.length()));
+
+ return IndexFileUtils.instance.openBlockingInput(file);
+ }
+
+ public IndexOutputWriter openPerSSTableOutput(IndexComponent component) throws IOException
+ {
+ return openPerSSTableOutput(component, false);
+ }
+
+ public IndexOutputWriter openPerSSTableOutput(IndexComponent component, boolean append) throws IOException
+ {
+ final File file = fileFor(component);
+
+ if (logger.isTraceEnabled())
+ logger.trace(logMessage("Creating SSTable attached index output for component {} on file {}..."),
+ component,
+ file);
+
+ IndexOutputWriter writer = IndexFileUtils.instance.openOutput(file);
+
+ if (append)
+ {
+ writer.skipBytes(file.length());
+ }
+
+ return writer;
+ }
+
+ public IndexOutputWriter openPerIndexOutput(IndexComponent indexComponent, IndexContext indexContext) throws IOException
+ {
+ return openPerIndexOutput(indexComponent, indexContext, false);
+ }
+
+ public IndexOutputWriter openPerIndexOutput(IndexComponent component, IndexContext indexContext, boolean append) throws IOException
+ {
+ final File file = fileFor(component, indexContext);
+
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("Creating sstable attached index output for component {} on file {}..."),
+ component,
+ file);
+
+ IndexOutputWriter writer = IndexFileUtils.instance.openOutput(file);
+
+ if (append)
+ {
+ writer.skipBytes(file.length());
+ }
+
+ return writer;
+ }
+
+ public FileHandle createPerSSTableFileHandle(IndexComponent indexComponent)
+ {
+ final File file = fileFor(indexComponent);
+
+ if (logger.isTraceEnabled())
+ {
+ logger.trace(logMessage("Opening {} file handle for {} ({})"),
+ file, FBUtilities.prettyPrintMemory(file.length()));
+ }
+
+ return new FileHandle.Builder(file).mmapped(true).complete();
+ }
+
+ public FileHandle createPerIndexFileHandle(IndexComponent indexComponent, IndexContext indexContext)
+ {
+ final File file = fileFor(indexComponent, indexContext);
+
+ if (logger.isTraceEnabled())
+ {
+ logger.trace(indexContext.logMessage("Opening file handle for {} ({})"),
+ file, FBUtilities.prettyPrintMemory(file.length()));
+ }
+
+ return new FileHandle.Builder(file).mmapped(true).complete();
+ }
+
+ public Set<Component> getLivePerSSTableComponents()
+ {
+ return version.onDiskFormat()
+ .perSSTableIndexComponents()
+ .stream()
+ .filter(c -> fileFor(c).exists())
+ .map(version::makePerSSTableComponent)
+ .collect(Collectors.toSet());
+ }
+
+ public Set<Component> getLivePerIndexComponents(IndexContext indexContext)
+ {
+ return version.onDiskFormat()
+ .perColumnIndexComponents(indexContext)
+ .stream()
+ .filter(c -> fileFor(c, indexContext).exists())
+ .map(c -> version.makePerIndexComponent(c, indexContext))
+ .collect(Collectors.toSet());
+ }
+
+ public long sizeOnDiskOfPerSSTableComponents()
+ {
+ return version.onDiskFormat()
+ .perSSTableIndexComponents()
+ .stream()
+ .map(this::fileFor)
+ .filter(File::exists)
+ .mapToLong(File::length)
+ .sum();
+ }
+
+ public long sizeOnDiskOfPerIndexComponents(IndexContext indexContext)
+ {
+ return version.onDiskFormat()
+ .perColumnIndexComponents(indexContext)
+ .stream()
+ .map(c -> fileFor(c, indexContext))
+ .filter(File::exists)
+ .mapToLong(File::length)
+ .sum();
+ }
+
+ @VisibleForTesting
+ public long sizeOnDiskOfPerIndexComponent(IndexComponent indexComponent, IndexContext indexContext)
+ {
+ File componentFile = fileFor(indexComponent, indexContext);
+ return componentFile.exists() ? componentFile.length() : 0;
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ public boolean validatePerIndexComponents(IndexContext indexContext)
+ {
+ logger.info(indexContext.logMessage("Validating per-column index components"));
+ return version.onDiskFormat().validatePerColumnIndexComponents(this, indexContext, false);
+ }
+
+ @VisibleForTesting
+ public boolean validatePerIndexComponentsChecksum(IndexContext indexContext)
+ {
+ return version.onDiskFormat().validatePerColumnIndexComponents(this, indexContext, true);
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ public boolean validatePerSSTableComponents()
+ {
+ return version.onDiskFormat().validatePerSSTableIndexComponents(this, false);
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ public boolean validatePerSSTableComponentsChecksum()
+ {
+ return version.onDiskFormat().validatePerSSTableIndexComponents(this, true);
+ }
+
+ public void deletePerSSTableIndexComponents()
+ {
+ version.onDiskFormat()
+ .perSSTableIndexComponents()
+ .stream()
+ .map(this::fileFor)
+ .filter(File::exists)
+ .forEach(this::deleteComponent);
+ }
+
+ public void deleteColumnIndex(IndexContext indexContext)
+ {
+ version.onDiskFormat()
+ .perColumnIndexComponents(indexContext)
+ .stream()
+ .map(c -> fileFor(c, indexContext))
+ .filter(File::exists)
+ .forEach(this::deleteComponent);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(sstableDescriptor, version);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IndexDescriptor other = (IndexDescriptor)o;
+ return Objects.equal(sstableDescriptor, other.sstableDescriptor) &&
+ Objects.equal(version, other.version);
+ }
+
+ @Override
+ public String toString()
+ {
+ return sstableDescriptor.toString() + "-SAI";
+ }
+
+ public String logMessage(String message)
+ {
+ // Index names are unique only within a keyspace.
+ return String.format("[%s.%s.*] %s",
+ sstableDescriptor.ksname,
+ sstableDescriptor.cfname,
+ message);
+ }
+
+ private File createFile(IndexComponent component, IndexContext indexContext)
+ {
+ Component customComponent = version.makePerIndexComponent(component, indexContext);
+ return sstableDescriptor.fileFor(customComponent);
+ }
+
+ private long numberOfPerIndexComponents(IndexContext indexContext)
+ {
+ return version.onDiskFormat()
+ .perColumnIndexComponents(indexContext)
+ .stream()
+ .map(c -> fileFor(c, indexContext))
+ .filter(File::exists)
+ .count();
+ }
+
+ private void deleteComponent(File file)
+ {
+ logger.debug(logMessage("Deleting storage-attached index component file {}"), file);
+ try
+ {
+ IOUtils.deleteFilesIfExist(file.toPath());
+ }
+ catch (IOException e)
+ {
+ logger.warn(logMessage("Unable to delete storage-attached index component file {} due to {}."), file, e.getMessage(), e);
+ }
+ }
+}
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
new file mode 100644
index 0000000000..cf19dceb6b
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/format/OnDiskFormat.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.format;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.SSTableContext;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter;
+import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.RowMapping;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+
+/**
+ * An interface to the on-disk format of an index. This provides format agnostic methods
+ * to read and write an on-disk format.
+ *
+ * The methods on this interface can be logically mapped into the following groups
+ * based on their method parameters:
+ * <ul>
+ * <li>Methods taking no parameters. These methods return static information about the
+ * format. This can include static information about the per-sstable components</li>
+ * <li>Methods taking just an {@link IndexContext}. These methods return static information
+ * specific to the index. This can be information relating to the type of index being used</li>
+ * <li>Methods taking an {@link IndexDescriptor}. These methods interact with the on-disk components, or
+ * return objects that will interact with the on-disk components, or return information about the on-disk
+ * components. If they take an {@link IndexContext} as well they will be interacting with per-column index files;
+ * otherwise they will be interacting with per-sstable index files</li>
+ * <li>Methods taking an {@link IndexComponent}. These methods only interact with a single index component or
+ * set of index components</li>
+ *
+ * </ul>
+ */
+public interface OnDiskFormat
+{
+ /**
+ * Returns a {@link PrimaryKeyMap.Factory} for the SSTable
+ *
+ * @param indexDescriptor The {@link IndexDescriptor} for the SSTable
+ * @param sstable The {@link SSTableReader} associated with the {@link IndexDescriptor}
+ */
+ PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexDescriptor indexDescriptor, SSTableReader sstable);
+
+ /**
+ * Create a new {@link SSTableIndex} for an on-disk index.
+ *
+ * @param sstableContext The {@link SSTableContext} holding the per-SSTable information for the index
+ * @param indexContext The {@link IndexContext} holding the per-index information for the index
+ * @return the new {@link SSTableIndex} for the on-disk index
+ */
+ SSTableIndex newSSTableIndex(SSTableContext sstableContext, IndexContext indexContext);
+
+ /**
+ * Create a new {@link PerSSTableIndexWriter} to write the per-SSTable on-disk components of an index.
+ *
+ * @param indexDescriptor The {@link IndexDescriptor} for the SSTable
+ * @throws IOException if the writer couldn't be created
+ */
+ PerSSTableIndexWriter newPerSSTableIndexWriter(IndexDescriptor indexDescriptor) throws IOException;
+
+ /**
+ * Create a new {@link PerColumnIndexWriter} to write the per-column on-disk components of an index. The {@link LifecycleNewTracker}
+ * is used to determine the type of index write about to happen this will either be an
+ * {@code OperationType.FLUSH} indicating that we are about to flush a {@link org.apache.cassandra.index.sai.memory.MemtableIndex}
+ * or one of the other operation types indicating that we will be writing from an existing SSTable
+ *
+ * @param index The {@link StorageAttachedIndex} holding the current index build status
+ * @param indexDescriptor The {@link IndexDescriptor} for the SSTable
+ * @param tracker The {@link LifecycleNewTracker} for index build operation.
+ * @param rowMapping The {@link RowMapping} that is used to map rowID to {@code PrimaryKey} during the write operation
+ */
+ PerColumnIndexWriter newPerColumnIndexWriter(StorageAttachedIndex index,
+ IndexDescriptor indexDescriptor,
+ LifecycleNewTracker tracker,
+ RowMapping rowMapping);
+
+ /**
+ * Returns true if the per-sstable index components have been built and are valid.
+ *
+ * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index
+ */
+ boolean isPerSSTableIndexBuildComplete(IndexDescriptor indexDescriptor);
+
+ /**
+ * Returns true if the per-column index components have been built and are valid.
+ *
+ * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index
+ * @param indexContext The {@link IndexContext} for the index
+ */
+ boolean isPerColumnIndexBuildComplete(IndexDescriptor indexDescriptor, IndexContext indexContext);
+
+ /**
+ * Validate all the per-SSTable on-disk components and throw if a component is not valid
+ *
+ * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index
+ * @param checksum {@code true} if the checksum should be tested as part of the validation
+ *
+ * @return true if all the per-SSTable components are valid
+ */
+ boolean validatePerSSTableIndexComponents(IndexDescriptor indexDescriptor, boolean checksum);
+
+ /**
+ * Validate all the per-column on-disk components and throw if a component is not valid
+ *
+ * @param indexDescriptor The {@link IndexDescriptor} for the SSTable SAI index
+ * @param indexContext The {@link IndexContext} holding the per-index information for the index
+ * @param checksum {@code true} if the checksum should be tested as part of the validation
+ *
+ * @return true if all the per-column components are valid
+ */
+ boolean validatePerColumnIndexComponents(IndexDescriptor indexDescriptor, IndexContext indexContext, boolean checksum);
+
+ /**
+ * Returns the set of {@link IndexComponent} for the per-SSTable part of an index.
+ * This is a complete set of components that could exist on-disk. It does not imply that the
+ * components currently exist on-disk.
+ */
+ Set<IndexComponent> perSSTableIndexComponents();
+
+ /**
+ * Returns the set of {@link IndexComponent} for the per-column part of an index.
+ * This is a complete set of components that could exist on-disk. It does not imply that the
+ * components currently exist on-disk.
+ *
+ * @param indexContext The {@link IndexContext} for the index
+ */
+ Set<IndexComponent> perColumnIndexComponents(IndexContext indexContext);
+
+ /**
+ * Return the number of open per-SSTable files that can be open during a query.
+ * This is a static indication of the files that can be held open by an index
+ * for queries. It is not a dynamic calculation.
+ */
+ int openFilesPerSSTableIndex();
+
+ /**
+ * Return the number of open per-column index files that can be open during a query.
+ * This is a static indication of the files that can be help open by an index
+ * for queries. It is not a dynamic calculation.
+ *
+ * @param indexContext The {@link IndexContext} for the index
+ */
+ int openFilesPerColumnIndex(IndexContext indexContext);
+}
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
new file mode 100644
index 0000000000..16e84d94c7
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/format/Version.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.format;
+
+import java.util.Comparator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Objects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.V1OnDiskFormat;
+import org.apache.cassandra.io.sstable.Component;
+import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
+
+/**
+ * Format version of indexing component, denoted as [major][minor]. Same forward-compatibility rules apply as to
+ * {@link org.apache.cassandra.io.sstable.format.Version}.
+ */
+public class Version implements Comparable<Version>
+{
+ private static final String SAI_DESCRIPTOR = "SAI";
+ private static final String SAI_SEPARATOR = "+";
+
+ // Current version
+ public static final Version AA = new Version("aa", V1OnDiskFormat.instance, (c, i) -> defaultFileNameFormat(c, i, "aa"));
+
+ // These should be added in reverse order so that the latest version is used first. Version matching tests
+ // are more likely to match the latest version, so we want to test that one first.
+ public static final SortedSet<Version> ALL = new TreeSet<Version>(Comparator.reverseOrder()) {{
+ add(AA);
+ }};
+
+ public static final Version EARLIEST = AA;
+ // The latest version can be configured to be an earlier version to support partial upgrades that don't
+ // write newer versions of the on-disk formats.
+ public static final Version LATEST = CassandraRelevantProperties.SAI_LATEST_VERSION.convert(Version::parse);
+
+ private final String version;
+ private final OnDiskFormat onDiskFormat;
+ private final FileNameFormatter fileNameFormatter;
+
+ private Version(String version, OnDiskFormat onDiskFormat, FileNameFormatter fileNameFormatter)
+ {
+ this.version = version;
+ this.onDiskFormat = onDiskFormat;
+ this.fileNameFormatter = fileNameFormatter;
+ }
+
+ public static Version parse(String versionString)
+ {
+ for (Version version : ALL)
+ if (version.version.equals(versionString))
+ return version;
+ throw new IllegalArgumentException("The version string " + versionString + " does not represent a valid SAI version. " +
+ "It should be one of " + ALL.stream().map(Version::toString).collect(Collectors.joining(", ")));
+ }
+
+ @Override
+ public int compareTo(Version other)
+ {
+ return version.compareTo(other.version);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(version);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Version other = (Version)o;
+ return Objects.equal(version, other.version);
+ }
+
+ @Override
+ public String toString()
+ {
+ return version;
+ }
+
+ public boolean onOrAfter(Version other)
+ {
+ return version.compareTo(other.version) >= 0;
+ }
+
+ public OnDiskFormat onDiskFormat()
+ {
+ return onDiskFormat;
+ }
+
+ public Component makePerSSTableComponent(IndexComponent indexComponent)
+ {
+ return SSTableFormat.Components.Types.CUSTOM.createComponent(fileNameFormatter.format(indexComponent, null));
+ }
+
+ public Component makePerIndexComponent(IndexComponent indexComponent, IndexContext indexContext)
+ {
+ return SSTableFormat.Components.Types.CUSTOM.createComponent(fileNameFormatter.format(indexComponent, indexContext));
+ }
+
+ public FileNameFormatter fileNameFormatter()
+ {
+ return fileNameFormatter;
+ }
+
+ public interface FileNameFormatter
+ {
+ String format(IndexComponent indexComponent, IndexContext indexContext);
+ }
+
+ /**
+ * SAI default filename formatter. This is the current SAI on-disk filename format
+ *
+ * Format: <sstable descriptor>-SAI+<version>(+<index name>)+<component name>.db
+ * Note: The index name is excluded for per-SSTable index files that are shared
+ * across all the per-column indexes for the SSTable.
+ */
+ private static String defaultFileNameFormat(IndexComponent indexComponent, IndexContext indexContext, String version)
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ stringBuilder.append(SAI_DESCRIPTOR);
+ stringBuilder.append(SAI_SEPARATOR).append(version);
+ if (indexContext != null)
+ stringBuilder.append(SAI_SEPARATOR).append(indexContext.getIndexName());
+ stringBuilder.append(SAI_SEPARATOR).append(indexComponent.name);
+ stringBuilder.append(Descriptor.EXTENSION);
+
+ return stringBuilder.toString();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexFileUtils.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexFileUtils.java
new file mode 100644
index 0000000000..1394d20555
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexFileUtils.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.sai.disk.io;
+
+import java.io.IOException;
+import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.io.util.SequentialWriter;
+import org.apache.cassandra.io.util.SequentialWriterOption;
+import org.apache.lucene.store.IndexInput;
+
+public class IndexFileUtils
+{
+ @VisibleForTesting
+ protected static final SequentialWriterOption DEFAULT_WRITER_OPTION = SequentialWriterOption.newBuilder()
+ .trickleFsync(DatabaseDescriptor.getTrickleFsync())
+ .trickleFsyncByteInterval(DatabaseDescriptor.getTrickleFsyncIntervalInKiB() * 1024)
+ .bufferType(BufferType.OFF_HEAP)
+ .finishOnClose(true)
+ .build();
+
+ public static final IndexFileUtils instance = new IndexFileUtils(DEFAULT_WRITER_OPTION);
+
+ private final SequentialWriterOption writerOption;
+
+ @VisibleForTesting
+ protected IndexFileUtils(SequentialWriterOption writerOption)
+ {
+ this.writerOption = writerOption;
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public IndexOutputWriter openOutput(File file)
+ {
+ assert writerOption.finishOnClose() : "IndexOutputWriter relies on close() to sync with disk.";
+
+ return new IndexOutputWriter(new IncrementalChecksumSequentialWriter(file, writerOption));
+ }
+
+ public IndexInput openInput(FileHandle handle)
+ {
+ return IndexInputReader.create(handle);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public IndexInput openBlockingInput(File file)
+ {
+ FileHandle fileHandle = new FileHandle.Builder(file).complete();
+ RandomAccessReader randomReader = fileHandle.createReader();
+
+ return IndexInputReader.create(randomReader, fileHandle::close);
+ }
+
+ public interface ChecksumWriter
+ {
+ long getChecksum();
+ }
+
+ static class IncrementalChecksumSequentialWriter extends SequentialWriter implements ChecksumWriter
+ {
+ private final CRC32 checksum = new CRC32();
+
+ IncrementalChecksumSequentialWriter(File file, SequentialWriterOption writerOption)
+ {
+ super(file, writerOption);
+ }
+
+ @Override
+ public void writeByte(int b) throws IOException
+ {
+ super.writeByte(b);
+ checksum.update(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException
+ {
+ super.write(b);
+ checksum.update(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ super.write(b, off, len);
+ checksum.update(b, off, len);
+ }
+
+ @Override
+ public void writeChar(int v) throws IOException
+ {
+ super.writeChar(v);
+ addTochecksum(v, 2);
+ }
+
+ @Override
+ public void writeInt(int v) throws IOException
+ {
+ super.writeInt(v);
+ addTochecksum(v, 4);
+ }
+
+ @Override
+ public void writeLong(long v) throws IOException
+ {
+ super.writeLong(v);
+ addTochecksum(v, 8);
+ }
+
+ public long getChecksum()
+ {
+ return checksum.getValue();
+ }
+
+ private void addTochecksum(long bytes, int count)
+ {
+ int origCount = count;
+ if (ByteOrder.BIG_ENDIAN == buffer.order())
+ while (count > 0) checksum.update((int) (bytes >>> (8 * --count)));
+ else
+ while (count > 0) checksum.update((int) (bytes >>> (8 * (origCount - count--))));
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java
new file mode 100644
index 0000000000..00a862e53a
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.io;
+
+
+import java.io.IOException;
+
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+
+/**
+ * This is a wrapper over a Cassandra {@link RandomAccessReader} that provides an {@link IndexInput}
+ * interface for Lucene classes that need {@link IndexInput}. This is an optimisation because the
+ * Lucene {@link DataInput} reads bytes one at a time whereas the {@link RandomAccessReader} is
+ * optimised to read multibyte objects faster.
+ */
+public class IndexInputReader extends IndexInput
+{
+ private final RandomAccessReader input;
+ private final Runnable doOnClose;
+
+ private IndexInputReader(RandomAccessReader input, Runnable doOnClose)
+ {
+ super(input.getPath());
+ this.input = input;
+ this.doOnClose = doOnClose;
+ }
+
+ public static IndexInputReader create(RandomAccessReader input)
+ {
+ return new IndexInputReader(input, () -> {});
+ }
+
+ public static IndexInputReader create(RandomAccessReader input, Runnable doOnClose)
+ {
+ return new IndexInputReader(input, doOnClose);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public static IndexInputReader create(FileHandle handle)
+ {
+ RandomAccessReader reader = handle.createReader();
+ return new IndexInputReader(reader, () -> {});
+ }
+
+ @Override
+ public byte readByte() throws IOException
+ {
+ return input.readByte();
+ }
+
+ @Override
+ public void readBytes(byte[] bytes, int off, int len) throws IOException
+ {
+ input.readFully(bytes, off, len);
+ }
+
+ /**
+ * Using {@link RandomAccessReader#readShort()} directly is faster than {@link DataInput#readShort()} which calls
+ * {@link DataInput#readByte()} one by one
+ */
+ @Override
+ public short readShort() throws IOException
+ {
+ return input.readShort();
+ }
+
+ /**
+ * Using {@link RandomAccessReader#readInt()} directly is faster than {@link DataInput#readInt()} which
+ * calls {@link DataInput#readByte()} one by one
+ */
+ @Override
+ public int readInt() throws IOException
+ {
+ return input.readInt();
+ }
+
+ /**
+ * Using {@link RandomAccessReader#readLong()} directly is faster than {@link DataInput#readLong()} which
+ * calls {@link DataInput#readByte()} one by one
+ */
+ @Override
+ public long readLong() throws IOException
+ {
+ return input.readLong();
+ }
+
+ @Override
+ public void close()
+ {
+ try
+ {
+ input.close();
+ }
+ finally
+ {
+ doOnClose.run();
+ }
+ }
+
+ @Override
+ public long getFilePointer()
+ {
+ return input.getFilePointer();
+ }
+
+ @Override
+ public void seek(long position)
+ {
+ input.seek(position);
+ }
+
+ @Override
+ public long length()
+ {
+ return input.length();
+ }
+
+ @Override
+ public IndexInput slice(String sliceDescription, long offset, long length)
+ {
+ throw new UnsupportedOperationException("Slice operations are not supported");
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutputWriter.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutputWriter.java
new file mode 100644
index 0000000000..4208048cf6
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexOutputWriter.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.io;
+
+import java.io.IOException;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.io.util.SequentialWriter;
+import org.apache.lucene.store.IndexOutput;
+
+/**
+ * This is a wrapper over a Cassandra {@link SequentialWriter} that provides a Lucene {@link IndexOutput}
+ * interface for the Lucene index writers.
+ */
+@NotThreadSafe
+public class IndexOutputWriter extends IndexOutput
+{
+ private static final Logger logger = LoggerFactory.getLogger(IndexOutputWriter.class);
+
+ private final SequentialWriter out;
+ private boolean closed;
+
+ public IndexOutputWriter(SequentialWriter out)
+ {
+ super(out.getPath(), out.getPath());
+ this.out = out;
+ }
+
+ public void skipBytes(long length) throws IOException
+ {
+ this.out.skipBytes(length);
+ }
+
+ public File getFile()
+ {
+ return new File(out.getPath());
+ }
+
+ @Override
+ public long getChecksum()
+ {
+ return ((IndexFileUtils.ChecksumWriter)out).getChecksum();
+ }
+
+ @Override
+ public long getFilePointer()
+ {
+ return out.position();
+ }
+
+ @Override
+ public void writeBytes(byte[] bytes, int offset, int len) throws IOException
+ {
+ out.write(bytes, offset, len);
+ }
+
+ @Override
+ public void writeByte(byte b) throws IOException
+ {
+ out.writeByte(b);
+ }
+
+ @Override
+ public void close()
+ {
+ // IndexOutput#close contract allows any output to be closed multiple times,
+ // and Lucene does it in few places. SequentialWriter can be closed once.
+ if (!closed)
+ {
+ if (logger.isTraceEnabled())
+ {
+ logger.trace("Closing index output: {}", this);
+ }
+
+ // The writer should sync its contents to disk before closing...
+ out.close();
+ closed = true;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("path", out.getPath())
+ .add("bytesWritten", getFilePointer())
+ .add("crc", getChecksum())
+ .toString();
+ }
+
+ /**
+ * Returns {@link SequentialWriter} associated with this writer. Convenient when interacting with Cassandra codebase to
+ * write files to disk. Note that all bytes written to the returned writer will still contribute to the checksum.
+ *
+ * @return {@link SequentialWriter} associated with this writer
+ */
+ public SequentialWriter asSequentialWriter()
+ {
+ return out;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/RAMIndexOutput.java b/src/java/org/apache/cassandra/index/sai/disk/io/RAMIndexOutput.java
new file mode 100644
index 0000000000..709f4e6d40
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/io/RAMIndexOutput.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.io;
+
+import java.io.IOException;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * A replacement for {@link org.apache.lucene.store.RAMOutputStream}
+ * that doesn't use monitor locks.
+ */
+@NotThreadSafe
+public class RAMIndexOutput extends IndexOutput
+{
+ private final GrowableByteArrayDataOutput out;
+
+ public RAMIndexOutput(String name)
+ {
+ super("", name);
+ //TODO CASSANDRA-18280 to investigate the initial size allocation
+ out = new GrowableByteArrayDataOutput(128);
+ }
+
+ @Override
+ public long getChecksum()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getFilePointer()
+ {
+ return out.getPosition();
+ }
+
+ @Override
+ public void writeByte(byte b)
+ {
+ out.writeByte(b);
+ }
+
+ @Override
+ public void writeBytes(byte[] bytes, int offset, int len)
+ {
+ out.writeBytes(bytes, offset, len);
+ }
+
+ public void writeTo(IndexOutput externalOut) throws IOException
+ {
+ externalOut.writeBytes(out.getBytes(), 0, out.getPosition());
+ }
+
+ public BytesRef getBytes()
+ {
+ return new BytesRef(out.getBytes(), 0, out.getPosition());
+ }
+
+ public void reset()
+ {
+ out.reset();
+ }
+
+ @Override
+ public void close()
+ {}
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/SeekingRandomAccessInput.java b/src/java/org/apache/cassandra/index/sai/disk/io/SeekingRandomAccessInput.java
new file mode 100644
index 0000000000..fcc5baed9d
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/io/SeekingRandomAccessInput.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.io;
+
+import java.io.IOException;
+
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.RandomAccessInput;
+
+/**
+ * {@link IndexInput} adapter that exposes it as a {@link RandomAccessInput}.
+ */
+public class SeekingRandomAccessInput implements RandomAccessInput
+{
+ private final IndexInput in;
+
+ public SeekingRandomAccessInput(IndexInput in)
+ {
+ this.in = in;
+ }
+
+ @Override
+ public byte readByte(long pos) throws IOException
+ {
+ in.seek(pos);
+ return in.readByte();
+ }
+
+ @Override
+ public short readShort(long pos) throws IOException
+ {
+ in.seek(pos);
+ return in.readShort();
+ }
+
+ @Override
+ public int readInt(long pos) throws IOException
+ {
+ in.seek(pos);
+ return in.readInt();
+ }
+
+ @Override
+ public long readLong(long pos) throws IOException
+ {
+ in.seek(pos);
+ return in.readLong();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "SeekingRandomAccessInput(" + in + ')';
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/DirectReaders.java b/src/java/org/apache/cassandra/index/sai/disk/v1/DirectReaders.java
new file mode 100644
index 0000000000..5d5ebce539
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/DirectReaders.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Sets;
+
+import org.apache.lucene.store.RandomAccessInput;
+
+public class DirectReaders
+{
+ public static final Set<Integer> SUPPORTED_BITS_PER_VALUE = Sets.newHashSet(0, 1, 2, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64);
+ public static final String SUPPORTED_BITS_PER_VALUE_STRING = SUPPORTED_BITS_PER_VALUE.stream()
+ .map(i -> Integer.toString(i))
+ .collect(Collectors.joining(", "));
+
+ public interface Reader
+ {
+ /**
+ * Returns the indexed delta value from the block starting at offset.
+ * This is implemented for each of the supported bits-per-value where
+ * each implementation is responsible for decoding the bits.
+ */
+ long get(RandomAccessInput in, long offset, long index);
+ }
+
+ public static Reader getReaderForBitsPerValue(byte bitsPerValue)
+ {
+ switch (bitsPerValue)
+ {
+ case 0:
+ return READER_0;
+ case 1:
+ return READER_1;
+ case 2:
+ return READER_2;
+ case 4:
+ return READER_4;
+ case 8:
+ return READER_8;
+ case 12:
+ return READER_12;
+ case 16:
+ return READER_16;
+ case 20:
+ return READER_20;
+ case 24:
+ return READER_24;
+ case 28:
+ return READER_28;
+ case 32:
+ return READER_32;
+ case 40:
+ return READER_40;
+ case 48:
+ return READER_48;
+ case 56:
+ return READER_56;
+ case 64:
+ return READER_64;
+ default:
+ throw new IllegalArgumentException("unsupported bitsPerValue: " + bitsPerValue);
+ }
+ }
+
+ private static final Reader READER_0 = (in, offset, index) -> 0;
+
+ private static final Reader READER_1 = (in, offset, index) -> {
+ try
+ {
+ int shift = 7 - (int) (index & 7);
+ return (in.readByte(offset + (index >>> 3)) >>> shift) & 0x1;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_2 = (in, offset, index) -> {
+ try
+ {
+ int shift = (3 - (int) (index & 3)) << 1;
+ return (in.readByte(offset + (index >>> 2)) >>> shift) & 0x3;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_4 = (in, offset, index) -> {
+ try
+ {
+ int shift = (int) ((index + 1) & 1) << 2;
+ return (in.readByte(offset + (index >>> 1)) >>> shift) & 0xF;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_8 = (in, offset, index) -> {
+ try
+ {
+ return in.readByte(offset + index) & 0xFF;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_12 = (in, offset, index) -> {
+ try
+ {
+ long o = (index * 12) >>> 3;
+ int shift = (int) ((index + 1) & 1) << 2;
+ return (in.readShort(offset + o) >>> shift) & 0xFFF;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_16 = (in, offset, index) -> {
+ try
+ {
+ return in.readShort(offset + (index << 1)) & 0xFFFF;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_20 = (in, offset, index) -> {
+ try
+ {
+ long o = (index * 20) >>> 3;
+ int v = in.readInt(offset + o) >>> 8;
+ int shift = (int) ((index + 1) & 1) << 2;
+ return (v >>> shift) & 0xFFFFF;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_24 = (in, offset, index) -> {
+ try
+ {
+ return in.readInt(offset + index * 3) >>> 8;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_28 = (in, offset, index) -> {
+ try
+ {
+ long o = (index * 28) >>> 3;
+ int shift = (int) ((index + 1) & 1) << 2;
+ return (in.readInt(offset + o) >>> shift) & 0xFFFFFFFL;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_32 = (in, offset, index) -> {
+ try
+ {
+ return in.readInt(offset + (index << 2)) & 0xFFFFFFFFL;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_40 = (in, offset, index) -> {
+ try
+ {
+ return in.readLong(offset + index * 5) >>> 24;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_48 = (in, offset, index) -> {
+ try
+ {
+ return in.readLong(offset + index * 6) >>> 16;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_56 = (in, offset, index) -> {
+ try
+ {
+ return in.readLong(offset + index * 7) >>> 8;
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+
+ private static final Reader READER_64 = (in, offset, index) -> {
+ try
+ {
+ return in.readLong(offset + (index << 3));
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ };
+}
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
new file mode 100644
index 0000000000..2c2a1fc39b
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/LongArray.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.function.Supplier;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * Abstraction over a long-indexed array of longs.
+ */
+public interface LongArray extends Closeable
+{
+ /**
+ * Get value at {@code idx}.
+ */
+ long get(long idx);
+
+ /**
+ * Get array length.
+ */
+ long length();
+
+ @Override
+ default void close() throws IOException { }
+
+ @NotThreadSafe
+ class DeferredLongArray implements LongArray
+ {
+ private final Supplier<LongArray> supplier;
+ private LongArray longArray;
+ private boolean opened = false;
+
+ public DeferredLongArray(Supplier<LongArray> supplier)
+ {
+ this.supplier = supplier;
+ }
+
+ @Override
+ public long get(long idx)
+ {
+ open();
+ return longArray.get(idx);
+ }
+
+ @Override
+ public long length()
+ {
+ open();
+ return longArray.length();
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ if (opened)
+ longArray.close();
+ }
+
+ private void open()
+ {
+ if (!opened)
+ {
+ longArray = supplier.get();
+ opened = true;
+ }
+ }
+ }
+
+ interface Factory
+ {
+ LongArray open();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java
new file mode 100644
index 0000000000..6a497fbb33
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.carrotsearch.hppc.LongArrayList;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter;
+import org.apache.cassandra.index.sai.disk.RowMapping;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.cassandra.index.sai.disk.v1.trie.LiteralIndexWriter;
+import org.apache.cassandra.index.sai.memory.MemtableIndex;
+import org.apache.cassandra.index.sai.memory.MemtableTermsIterator;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+
+/**
+ * Column index writer that flushes indexed data directly from the corresponding Memtable index, without buffering index
+ * data in memory.
+ */
+public class MemtableIndexWriter implements PerColumnIndexWriter
+{
+ private static final Logger logger = LoggerFactory.getLogger(MemtableIndexWriter.class);
+
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final MemtableIndex memtable;
+ private final RowMapping rowMapping;
+
+ public MemtableIndexWriter(MemtableIndex memtable,
+ IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ RowMapping rowMapping)
+ {
+ assert rowMapping != null && rowMapping != RowMapping.DUMMY : "Row mapping must exist during FLUSH.";
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.memtable = memtable;
+ this.rowMapping = rowMapping;
+ }
+
+ @Override
+ public void addRow(PrimaryKey key, Row row, long sstableRowId)
+ {
+ // Memtable indexes are flushed directly to disk with the aid of a mapping between primary
+ // keys and row IDs in the flushing SSTable. This writer, therefore, does nothing in
+ // response to the flushing of individual rows.
+ }
+
+ @Override
+ public void abort(Throwable cause)
+ {
+ logger.warn(indexContext.logMessage("Aborting index memtable flush for {}..."), indexDescriptor.sstableDescriptor, cause);
+ indexDescriptor.deleteColumnIndex(indexContext);
+ }
+
+ @Override
+ public void complete(Stopwatch stopwatch) throws IOException
+ {
+ assert rowMapping.isComplete() : "Cannot complete the memtable index writer because the row mapping is not complete";
+
+ long start = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+
+ try
+ {
+ if (!rowMapping.hasRows() || memtable == null || memtable.isEmpty())
+ {
+ logger.debug(indexContext.logMessage("No indexed rows to flush from SSTable {}."), indexDescriptor.sstableDescriptor);
+ // Write a completion marker even though we haven't written anything to the index,
+ // so we won't try to build the index again for the SSTable
+ indexDescriptor.createComponentOnDisk(IndexComponent.COLUMN_COMPLETION_MARKER, indexContext);
+ return;
+ }
+
+ final DecoratedKey minKey = rowMapping.minKey.partitionKey();
+ final DecoratedKey maxKey = rowMapping.maxKey.partitionKey();
+
+ final Iterator<Pair<ByteComparable, LongArrayList>> iterator = rowMapping.merge(memtable);
+
+ try (MemtableTermsIterator terms = new MemtableTermsIterator(memtable.getMinTerm(), memtable.getMaxTerm(), iterator))
+ {
+ long cellCount = flush(minKey, maxKey, indexContext.getValidator(), terms);
+
+ indexDescriptor.createComponentOnDisk(IndexComponent.COLUMN_COMPLETION_MARKER, indexContext);
+
+ indexContext.getIndexMetrics().memtableIndexFlushCount.inc();
+
+ long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+
+ logger.debug(indexContext.logMessage("Completed flushing {} memtable index cells to SSTable {}. Duration: {} ms. Total elapsed: {} ms"),
+ cellCount, indexDescriptor.sstableDescriptor, elapsed - start, elapsed);
+
+ indexContext.getIndexMetrics().memtableFlushCellsPerSecond.update((long) (cellCount * 1000.0 / Math.max(1, elapsed - start)));
+ }
+ }
+ catch (Throwable t)
+ {
+ logger.error(indexContext.logMessage("Error while flushing index {}"), t.getMessage(), t);
+ indexContext.getIndexMetrics().memtableIndexFlushErrors.inc();
+
+ throw t;
+ }
+ }
+
+ private long flush(DecoratedKey minKey,
+ DecoratedKey maxKey,
+ AbstractType<?> termComparator,
+ MemtableTermsIterator terms) throws IOException
+ {
+ long numRows = 0;
+ SegmentMetadata.ComponentMetadataMap indexMetas = null;
+
+ if (TypeUtil.isLiteral(termComparator))
+ {
+ try (LiteralIndexWriter writer = new LiteralIndexWriter(indexDescriptor, indexContext))
+ {
+ indexMetas = writer.writeCompleteSegment(terms);
+ numRows = writer.getPostingsCount();
+ }
+ }
+
+ // If no rows were written we need to delete any created column index components
+ // so that the index is correctly identified as being empty (only having a completion marker)
+ if (numRows == 0)
+ {
+ indexDescriptor.deleteColumnIndex(indexContext);
+ return 0;
+ }
+
+ // During index memtable flush, the data is sorted based on terms.
+ SegmentMetadata metadata = new SegmentMetadata(0,
+ numRows,
+ terms.getMinSSTableRowId(),
+ terms.getMaxSSTableRowId(),
+ indexDescriptor.primaryKeyFactory.createPartitionKeyOnly(minKey),
+ indexDescriptor.primaryKeyFactory.createPartitionKeyOnly(maxKey),
+ terms.getMinTerm(),
+ terms.getMaxTerm(),
+ indexMetas);
+
+ try (MetadataWriter writer = new MetadataWriter(indexDescriptor.openPerIndexOutput(IndexComponent.META, indexContext)))
+ {
+ SegmentMetadata.write(writer, Collections.singletonList(metadata));
+ }
+
+ return numRows;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataSource.java b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataSource.java
new file mode 100644
index 0000000000..b8b7fb66cc
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataSource.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.lucene.store.BufferedChecksumIndexInput;
+import org.apache.lucene.store.ByteArrayIndexInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+
+@NotThreadSafe
+public class MetadataSource
+{
+ private final Map<String, BytesRef> components;
+
+ private MetadataSource(Map<String, BytesRef> components)
+ {
+ this.components = components;
+ }
+
+ public static MetadataSource loadGroupMetadata(IndexDescriptor indexDescriptor) throws IOException
+ {
+ return MetadataSource.load(indexDescriptor.openPerSSTableInput(IndexComponent.GROUP_META));
+ }
+
+ public static MetadataSource loadColumnMetadata(IndexDescriptor indexDescriptor, IndexContext indexContext) throws IOException
+ {
+ return MetadataSource.load(indexDescriptor.openPerIndexInput(IndexComponent.META, indexContext));
+ }
+
+ private static MetadataSource load(IndexInput indexInput) throws IOException
+ {
+ Map<String, BytesRef> components = new HashMap<>();
+
+ try (BufferedChecksumIndexInput input = new BufferedChecksumIndexInput(indexInput))
+ {
+ SAICodecUtils.checkHeader(input);
+ final int num = input.readInt();
+
+ for (int x = 0; x < num; x++)
+ {
+ if (input.length() == input.getFilePointer())
+ {
+ // we should never get here, because we always add footer to the file
+ throw new IllegalStateException("Unexpected EOF in " + input);
+ }
+
+ final String name = input.readString();
+ final int length = input.readInt();
+ final byte[] bytes = new byte[length];
+ input.readBytes(bytes, 0, length);
+
+ components.put(name, new BytesRef(bytes));
+ }
+
+ SAICodecUtils.checkFooter(input);
+ }
+
+ return new MetadataSource(components);
+ }
+
+ public IndexInput get(String name)
+ {
+ BytesRef bytes = components.get(name);
+
+ if (bytes == null)
+ {
+ throw new IllegalArgumentException(String.format("Could not find component '%s'. Available properties are %s.",
+ name, components.keySet()));
+ }
+
+ return new ByteArrayIndexInput(name, bytes.bytes);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataWriter.java
new file mode 100644
index 0000000000..b995f4ef17
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/MetadataWriter.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.BytesRef;
+
+@NotThreadSafe
+public class MetadataWriter implements Closeable
+{
+ private final IndexOutput output;
+ private final Map<String, BytesRef> map = new HashMap<>();
+
+ public MetadataWriter(IndexOutput output)
+ {
+ this.output = output;
+ }
+
+ public Builder builder(String name)
+ {
+ return new Builder(name);
+ }
+
+ public class Builder extends RAMIndexOutput implements Closeable
+ {
+ private Builder(String name)
+ {
+ super(name);
+ }
+
+ @Override
+ public void close()
+ {
+ map.put(getName(), getBytes());
+ }
+ }
+
+ private void finish() throws IOException
+ {
+ SAICodecUtils.writeHeader(output);
+ output.writeInt(map.size());
+ for (Map.Entry<String, BytesRef> entry : map.entrySet())
+ {
+ output.writeString(entry.getKey());
+ output.writeInt(entry.getValue().length);
+ output.writeBytes(entry.getValue().bytes, entry.getValue().offset, entry.getValue().length);
+ }
+ SAICodecUtils.writeFooter(output);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ try
+ {
+ finish();
+ }
+ finally
+ {
+ output.close();
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/PerColumnIndexFiles.java b/src/java/org/apache/cassandra/index/sai/disk/v1/PerColumnIndexFiles.java
new file mode 100644
index 0000000000..7d129ba28f
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/PerColumnIndexFiles.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1;
+
+import java.io.Closeable;
+import java.util.EnumMap;
+import java.util.Map;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+
+/**
+ * Maintains a mapping of {@link IndexComponent}s to associated {@link FileHandle}s for
+ * read operations on the components. Users of this class are returned copies of the
+ * {@link FileHandle}s using {@link FileHandle#sharedCopy()} so returned handles still
+ * need to be closed by the user.
+ */
+public class PerColumnIndexFiles implements Closeable
+{
+ private final Map<IndexComponent, FileHandle> files = new EnumMap<>(IndexComponent.class);
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+
+ public PerColumnIndexFiles(IndexDescriptor indexDescriptor, IndexContext indexContext)
+ {
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ if (TypeUtil.isLiteral(indexContext.getValidator()))
+ {
+ files.put(IndexComponent.POSTING_LISTS, indexDescriptor.createPerIndexFileHandle(IndexComponent.POSTING_LISTS, indexContext));
+ files.put(IndexComponent.TERMS_DATA, indexDescriptor.createPerIndexFileHandle(IndexComponent.TERMS_DATA, indexContext));
+ }
+ }
+
+ public FileHandle termsData()
+ {
+ return getFile(IndexComponent.TERMS_DATA);
+ }
+
+ public FileHandle postingLists()
+ {
+ return getFile(IndexComponent.POSTING_LISTS);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private FileHandle getFile(IndexComponent indexComponent)
+ {
+ FileHandle file = files.get(indexComponent);
+ if (file == null)
+ throw new IllegalArgumentException(String.format(indexContext.logMessage("Component %s not found for SSTable %s"),
+ indexComponent, indexDescriptor.sstableDescriptor));
+
+ return file.sharedCopy();
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(files.values());
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/RowAwarePrimaryKeyMap.java b/src/java/org/apache/cassandra/index/sai/disk/v1/RowAwarePrimaryKeyMap.java
new file mode 100644
index 0000000000..1319006d21
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/RowAwarePrimaryKeyMap.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.sai.disk.v1;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+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.ClusteringComparator;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+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.sortedterms.SortedTermsMeta;
+import org.apache.cassandra.index.sai.disk.v1.sortedterms.SortedTermsReader;
+import org.apache.cassandra.index.sai.disk.v1.trie.TriePrefixSearcher;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+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.ByteComparable;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
+
+/**
+ * A row-aware {@link PrimaryKeyMap}
+ *
+ * This uses the following on-disk structures:
+ * <ul>
+ * <li>Block-packed structure for rowId to token lookups using {@link BlockPackedReader}.
+ * Uses component {@link IndexComponent#TOKEN_VALUES} </li>
+ * <li>A sorted-terms structure for rowId to {@link PrimaryKey} and {@link PrimaryKey} to rowId lookups using
+ * {@link SortedTermsReader} and {@link TriePrefixSearcher}. Uses components {@link IndexComponent#PRIMARY_KEY_TRIE},
+ * {@link IndexComponent#PRIMARY_KEY_BLOCKS} and {@link IndexComponent#PRIMARY_KEY_BLOCK_OFFSETS}</li>
+ * </ul>
+ *
+ * While the {@link RowAwarePrimaryKeyMapFactory} is threadsafe, individual instances of the {@link RowAwarePrimaryKeyMap}
+ * are not.
+ */
+@NotThreadSafe
+public class RowAwarePrimaryKeyMap implements PrimaryKeyMap
+{
+ @ThreadSafe
+ public static class RowAwarePrimaryKeyMapFactory implements Factory
+ {
+ private final LongArray.Factory tokenReaderFactory;
+ private final SortedTermsReader sortedTermsReader;
+ private final SortedTermsMeta sortedTermsMeta;
+ private FileHandle tokensFile = null;
+ private FileHandle primaryKeyBlockOffsetsFile = null;
+ private FileHandle primaryKeyBlocksFile = null;
+ private FileHandle primaryKeyTrieFile = null;
+ private final IPartitioner partitioner;
+ private final ClusteringComparator clusteringComparator;
+ private final PrimaryKey.Factory primaryKeyFactory;
+
+ public RowAwarePrimaryKeyMapFactory(IndexDescriptor indexDescriptor, SSTableReader sstable)
+ {
+ try
+ {
+ MetadataSource metadataSource = MetadataSource.loadGroupMetadata(indexDescriptor);
+ NumericValuesMeta tokensMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.TOKEN_VALUES)));
+ NumericValuesMeta blockOffsetsMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.PRIMARY_KEY_BLOCK_OFFSETS)));
+ this.sortedTermsMeta = new SortedTermsMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.PRIMARY_KEY_BLOCKS)));
+ this.tokensFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.TOKEN_VALUES);
+ this.tokenReaderFactory = new BlockPackedReader(tokensFile, tokensMeta);
+ this.primaryKeyBlockOffsetsFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.PRIMARY_KEY_BLOCK_OFFSETS);
+ this.primaryKeyBlocksFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.PRIMARY_KEY_BLOCKS);
+ this.primaryKeyTrieFile = indexDescriptor.createPerSSTableFileHandle(IndexComponent.PRIMARY_KEY_TRIE);
+ this.sortedTermsReader = new SortedTermsReader(primaryKeyBlocksFile, primaryKeyBlockOffsetsFile, sortedTermsMeta, blockOffsetsMeta);
+ this.partitioner = sstable.metadata().partitioner;
+ this.primaryKeyFactory = indexDescriptor.primaryKeyFactory;
+ this.clusteringComparator = indexDescriptor.clusteringComparator;
+ }
+ catch (Throwable t)
+ {
+ throw Throwables.unchecked(Throwables.close(t, Arrays.asList(tokensFile, primaryKeyBlocksFile, primaryKeyBlockOffsetsFile, primaryKeyTrieFile)));
+ }
+ }
+
+ @Override
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PrimaryKeyMap newPerSSTablePrimaryKeyMap() throws IOException
+ {
+ LongArray rowIdToToken = new LongArray.DeferredLongArray(tokenReaderFactory::open);
+ return new RowAwarePrimaryKeyMap(rowIdToToken,
+ new TriePrefixSearcher(primaryKeyTrieFile.instantiateRebufferer(null), sortedTermsMeta.trieFilePointer),
+ sortedTermsReader.openCursor(),
+ partitioner,
+ primaryKeyFactory,
+ clusteringComparator);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(Arrays.asList(tokensFile, primaryKeyBlocksFile, primaryKeyBlockOffsetsFile, primaryKeyTrieFile));
+ }
+ }
+
+ private final LongArray rowIdToToken;
+ private final TriePrefixSearcher triePrefixSearcher;
+ private final SortedTermsReader.Cursor cursor;
+ private final IPartitioner partitioner;
+ private final PrimaryKey.Factory primaryKeyFactory;
+ private final ClusteringComparator clusteringComparator;
+ private final ByteBuffer tokenBuffer = ByteBuffer.allocate(Long.BYTES);
+
+ private RowAwarePrimaryKeyMap(LongArray rowIdToToken,
+ TriePrefixSearcher triePrefixSearcher,
+ SortedTermsReader.Cursor cursor,
+ IPartitioner partitioner,
+ PrimaryKey.Factory primaryKeyFactory,
+ ClusteringComparator clusteringComparator)
+ {
+ this.rowIdToToken = rowIdToToken;
+ this.triePrefixSearcher = triePrefixSearcher;
+ this.cursor = cursor;
+ this.partitioner = partitioner;
+ this.primaryKeyFactory = primaryKeyFactory;
+ this.clusteringComparator = clusteringComparator;
+ }
+
+ @Override
+ public PrimaryKey primaryKeyFromRowId(long sstableRowId)
+ {
+ tokenBuffer.putLong(rowIdToToken.get(sstableRowId));
+ tokenBuffer.rewind();
+ return primaryKeyFactory.createDeferred(partitioner.getTokenFactory().fromByteArray(tokenBuffer), () -> supplier(sstableRowId));
+ }
+
+ @Override
+ public long rowIdFromPrimaryKey(PrimaryKey key)
+ {
+ return triePrefixSearcher.prefixSearch(key.asComparableBytes(ByteComparable.Version.OSS50));
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(Arrays.asList(triePrefixSearcher, cursor, rowIdToToken));
+ }
+
+ private PrimaryKey supplier(long sstableRowId)
+ {
+ try
+ {
+ cursor.seekToPointId(sstableRowId);
+ ByteSource.Peekable peekable = ByteSource.peekable(cursor.term().asComparableBytes(ByteComparable.Version.OSS50));
+
+ Token token = partitioner.getTokenFactory().fromComparableBytes(ByteSourceInverse.nextComponentSource(peekable),
+ ByteComparable.Version.OSS50);
+ byte[] keyBytes = ByteSourceInverse.getUnescapedBytes(ByteSourceInverse.nextComponentSource(peekable));
+
+ if (keyBytes == null)
+ return primaryKeyFactory.createTokenOnly(token);
+
+ DecoratedKey partitionKey = new BufferDecoratedKey(token, ByteBuffer.wrap(keyBytes));
+
+ Clustering<?> clustering = clusteringComparator.size() == 0
+ ? Clustering.EMPTY
+ : clusteringComparator.clusteringFromByteComparable(ByteBufferAccessor.instance,
+ v -> ByteSourceInverse.nextComponentSource(peekable));
+
+ return primaryKeyFactory.create(partitionKey, clustering);
+ }
+ catch (IOException e)
+ {
+ throw Throwables.cleaned(e);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SAICodecUtils.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SAICodecUtils.java
new file mode 100644
index 0000000000..72d1a55539
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SAICodecUtils.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1;
+
+import java.io.IOException;
+
+import org.apache.cassandra.index.sai.disk.format.Version;
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ChecksumIndexInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.IndexOutput;
+
+import static org.apache.lucene.codecs.CodecUtil.CODEC_MAGIC;
+import static org.apache.lucene.codecs.CodecUtil.FOOTER_MAGIC;
+
+public class SAICodecUtils
+{
+ public static final String FOOTER_POINTER = "footerPointer";
+
+ public static void writeHeader(IndexOutput out) throws IOException
+ {
+ out.writeInt(CODEC_MAGIC);
+ out.writeString(Version.LATEST.toString());
+ }
+
+ public static void writeFooter(IndexOutput out) throws IOException
+ {
+ out.writeInt(FOOTER_MAGIC);
+ out.writeInt(0);
+ writeCRC(out);
+ }
+
+ public static void checkHeader(DataInput in) throws IOException
+ {
+ final int actualMagic = in.readInt();
+ if (actualMagic != CODEC_MAGIC)
+ {
+ throw new CorruptIndexException("codec header mismatch: actual header=" + actualMagic + " vs expected header=" + CODEC_MAGIC, in);
+ }
+ final Version actualVersion = Version.parse(in.readString());
+ if (!actualVersion.onOrAfter(Version.EARLIEST))
+ {
+ throw new IOException("Unsupported version: " + actualVersion);
+ }
+ }
+
+ public static void checkFooter(ChecksumIndexInput in) throws IOException
+ {
+ validateFooter(in, false);
+ long actualChecksum = in.getChecksum();
+ long expectedChecksum = readCRC(in);
+ if (expectedChecksum != actualChecksum)
+ {
+ throw new CorruptIndexException("checksum failed (hardware problem?) : expected=" + Long.toHexString(expectedChecksum) +
+ " actual=" + Long.toHexString(actualChecksum), in);
+ }
+ }
+
+ public static void validate(IndexInput input) throws IOException
+ {
+ checkHeader(input);
+ validateFooterAndResetPosition(input);
+ }
+
+ public static void validate(IndexInput input, long footerPointer) throws IOException
+ {
+ checkHeader(input);
+
+ long current = input.getFilePointer();
+ input.seek(footerPointer);
+ validateFooter(input, true);
+
+ input.seek(current);
+ }
+
+ public static void validateChecksum(IndexInput input) throws IOException
+ {
+ CodecUtil.checksumEntireFile(input);
+ }
+
+ // Copied from Lucene PackedInts as they are not public
+
+ public static int checkBlockSize(int blockSize, int minBlockSize, int maxBlockSize)
+ {
+ if (blockSize >= minBlockSize && blockSize <= maxBlockSize)
+ {
+ if ((blockSize & blockSize - 1) != 0)
+ {
+ throw new IllegalArgumentException("blockSize must be a power of two, got " + blockSize);
+ }
+ else
+ {
+ return Integer.numberOfTrailingZeros(blockSize);
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException("blockSize must be >= " + minBlockSize + " and <= " + maxBlockSize + ", got " + blockSize);
+ }
+ }
+
+ public static int numBlocks(long size, int blockSize)
+ {
+ if (size < 0)
+ throw new IllegalArgumentException("size cannot be negative");
+
+ int numBlocks = (int)(size / (long)blockSize) + (size % (long)blockSize == 0L ? 0 : 1);
+ if ((long)numBlocks * (long)blockSize < size)
+ {
+ throw new IllegalArgumentException("size is too large for this block size");
+ }
+ else
+ {
+ return numBlocks;
+ }
+ }
+
+ // Copied from Lucene BlockPackedReaderIterator as they are not public
+
+ /**
+ * Same as DataInput.readVLong but supports negative values
+ */
+ public static long readVLong(DataInput in) throws IOException
+ {
+ byte b = in.readByte();
+ if (b >= 0) return b;
+ long i = b & 0x7FL;
+ b = in.readByte();
+ i |= (b & 0x7FL) << 7;
+ if (b >= 0) return i;
+ b = in.readByte();
+ i |= (b & 0x7FL) << 14;
+ if (b >= 0) return i;
+ b = in.readByte();
+ i |= (b & 0x7FL) << 21;
+ if (b >= 0) return i;
+ b = in.readByte();
+ i |= (b & 0x7FL) << 28;
+ if (b >= 0) return i;
+ b = in.readByte();
+ i |= (b & 0x7FL) << 35;
+ if (b >= 0) return i;
+ b = in.readByte();
+ i |= (b & 0x7FL) << 42;
+ if (b >= 0) return i;
+ b = in.readByte();
+ i |= (b & 0x7FL) << 49;
+ if (b >= 0) return i;
+ b = in.readByte();
+ i |= (b & 0xFFL) << 56;
+ return i;
+ }
+
+ public static void validateFooterAndResetPosition(IndexInput in) throws IOException
+ {
+ long position = in.getFilePointer();
+ long fileLength = in.length();
+ long footerLength = CodecUtil.footerLength();
+ long footerPosition = fileLength - footerLength;
+
+ if (footerPosition < 0)
+ {
+ throw new CorruptIndexException("invalid codec footer (file truncated?): file length=" + fileLength + ", footer length=" + footerLength, in);
+ }
+
+ in.seek(footerPosition);
+ validateFooter(in, false);
+ in.seek(position);
+ }
+
+ /**
+ * Copied from org.apache.lucene.codecs.CodecUtil.validateFooter(IndexInput).
+ *
+ * If the file is segmented then the footer can exist in the middle of the file
+ * so, we shouldn't check that the footer size is correct, we just check that the
+ * footer values are correct.
+ */
+ private static void validateFooter(IndexInput in, boolean segmented) throws IOException
+ {
+ long remaining = in.length() - in.getFilePointer();
+ long expected = CodecUtil.footerLength();
+
+ if (!segmented)
+ {
+ if (remaining < expected)
+ {
+ throw new CorruptIndexException("misplaced codec footer (file truncated?): remaining=" + remaining + ", expected=" + expected + ", fp=" + in.getFilePointer(), in);
+ }
+ else if (remaining > expected)
+ {
+ throw new CorruptIndexException("misplaced codec footer (file extended?): remaining=" + remaining + ", expected=" + expected + ", fp=" + in.getFilePointer(), in);
+ }
+ }
+
+ final int magic = in.readInt();
+
+ if (magic != FOOTER_MAGIC)
+ {
+ throw new CorruptIndexException("codec footer mismatch (file truncated?): actual footer=" + magic + " vs expected footer=" + FOOTER_MAGIC, in);
+ }
+
+ final int algorithmID = in.readInt();
+
+ if (algorithmID != 0)
+ {
+ throw new CorruptIndexException("codec footer mismatch: unknown algorithmID: " + algorithmID, in);
+ }
+ }
+
+ // Copied from Lucene CodecUtil as they are not public
+
+ /**
+ * Writes CRC32 value as a 64-bit long to the output.
+ * @throws IllegalStateException if CRC is formatted incorrectly (wrong bits set)
+ * @throws IOException if an i/o error occurs
+ */
+ private static void writeCRC(IndexOutput output) throws IOException
+ {
+ long value = output.getChecksum();
+ if ((value & 0xFFFFFFFF00000000L) != 0)
+ {
+ throw new IllegalStateException("Illegal CRC-32 checksum: " + value + " (resource=" + output + ')');
+ }
+ output.writeLong(value);
+ }
+
+ /**
+ * Reads CRC32 value as a 64-bit long from the input.
+ * @throws CorruptIndexException if CRC is formatted incorrectly (wrong bits set)
+ * @throws IOException if an i/o error occurs
+ */
+ private static long readCRC(IndexInput input) throws IOException
+ {
+ long value = input.readLong();
+ if ((value & 0xFFFFFFFF00000000L) != 0)
+ {
+ throw new CorruptIndexException("Illegal CRC-32 checksum: " + value, input);
+ }
+ return value;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java
new file mode 100644
index 0000000000..8845f27300
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableComponentsWriter.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.sai.disk.v1;
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.io.IndexOutputWriter;
+import org.apache.cassandra.index.sai.disk.v1.bitpack.NumericValuesWriter;
+import org.apache.cassandra.index.sai.disk.v1.sortedterms.SortedTermsWriter;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.lucene.util.IOUtils;
+
+public class SSTableComponentsWriter implements PerSSTableIndexWriter
+{
+ protected static final Logger logger = LoggerFactory.getLogger(SSTableComponentsWriter.class);
+
+ private final IndexDescriptor indexDescriptor;
+ private final MetadataWriter metadataWriter;
+ private final NumericValuesWriter tokenWriter;
+ private final SortedTermsWriter sortedTermsWriter;
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public SSTableComponentsWriter(IndexDescriptor indexDescriptor) throws IOException
+ {
+ this.indexDescriptor = indexDescriptor;
+ this.metadataWriter = new MetadataWriter(indexDescriptor.openPerSSTableOutput(IndexComponent.GROUP_META));
+ this.tokenWriter = new NumericValuesWriter(indexDescriptor.componentName(IndexComponent.TOKEN_VALUES),
+ indexDescriptor.openPerSSTableOutput(IndexComponent.TOKEN_VALUES),
+ metadataWriter, false);
+ IndexOutputWriter primaryKeyTrieWriter = indexDescriptor.openPerSSTableOutput(IndexComponent.PRIMARY_KEY_TRIE);
+ IndexOutputWriter primaryKeyBlocksWriter = indexDescriptor.openPerSSTableOutput(IndexComponent.PRIMARY_KEY_BLOCKS);
+ NumericValuesWriter primaryKeyBlockOffsetWriter = new NumericValuesWriter(indexDescriptor.componentName(IndexComponent.PRIMARY_KEY_BLOCK_OFFSETS),
+ indexDescriptor.openPerSSTableOutput(IndexComponent.PRIMARY_KEY_BLOCK_OFFSETS),
+ metadataWriter, true);
+ this.sortedTermsWriter = new SortedTermsWriter(indexDescriptor.componentName(IndexComponent.PRIMARY_KEY_BLOCKS),
+ metadataWriter,
+ primaryKeyBlocksWriter,
+ primaryKeyBlockOffsetWriter,
+ primaryKeyTrieWriter);
+ }
+
+ @Override
+ public void nextRow(PrimaryKey primaryKey) throws IOException
+ {
+ tokenWriter.add(primaryKey.token().getLongValue());
+ sortedTermsWriter.add(primaryKey::asComparableBytes);
+ }
+
+ @Override
+ public void complete() throws IOException
+ {
+ try
+ {
+ indexDescriptor.createComponentOnDisk(IndexComponent.GROUP_COMPLETION_MARKER);
+ }
+ finally
+ {
+ IOUtils.close(tokenWriter, sortedTermsWriter, metadataWriter);
+ }
+ }
+
+ @Override
+ public void abort()
+ {
+ logger.debug(indexDescriptor.logMessage("Aborting per-SSTable index component writer for {}..."), indexDescriptor.sstableDescriptor);
+ indexDescriptor.deletePerSSTableIndexComponents();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java
new file mode 100644
index 0000000000..2334b9cf42
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java
@@ -0,0 +1,336 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer;
+import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentBuilder;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+/**
+ * Column index writer that accumulates (on-heap) indexed data from a compacted SSTable as it's being flushed to disk.
+ */
+@NotThreadSafe
+public class SSTableIndexWriter implements PerColumnIndexWriter
+{
+ private static final Logger logger = LoggerFactory.getLogger(SSTableIndexWriter.class);
+ private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
+
+ public static final int MAX_STRING_TERM_SIZE = Integer.getInteger("cassandra.sai.max_string_term_size_kb", 1) * 1024;
+ public static final int MAX_FROZEN_TERM_SIZE = Integer.getInteger("cassandra.sai.max_frozen_term_size_kb", 5) * 1024;
+ public static final String TERM_OVERSIZE_MESSAGE = "Can't add term of column {} to index for key: {}, term size {} " +
+ "max allowed size {}, use analyzed = true (if not yet set) for that column.";
+
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final long nowInSec = FBUtilities.nowInSeconds();
+ private final AbstractAnalyzer analyzer;
+ private final NamedMemoryLimiter limiter;
+ private final int maxTermSize;
+ private final BooleanSupplier isIndexValid;
+ private final List<SegmentMetadata> segments = new ArrayList<>();
+
+ private boolean aborted = false;
+ private SegmentBuilder currentBuilder;
+
+ public SSTableIndexWriter(IndexDescriptor indexDescriptor, IndexContext indexContext, NamedMemoryLimiter limiter, BooleanSupplier isIndexValid)
+ {
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.analyzer = indexContext.getIndexAnalyzerFactory().create();
+ this.limiter = limiter;
+ this.isIndexValid = isIndexValid;
+ this.maxTermSize = indexContext.isFrozen() ? MAX_FROZEN_TERM_SIZE : MAX_STRING_TERM_SIZE;
+ }
+
+ @Override
+ public void addRow(PrimaryKey key, Row row, long sstableRowId) throws IOException
+ {
+ if (maybeAbort())
+ return;
+
+ if (indexContext.isNonFrozenCollection())
+ {
+ Iterator<ByteBuffer> valueIterator = indexContext.getValuesOf(row, nowInSec);
+ if (valueIterator != null)
+ {
+ while (valueIterator.hasNext())
+ {
+ ByteBuffer value = valueIterator.next();
+ addTerm(TypeUtil.asIndexBytes(value.duplicate(), indexContext.getValidator()), key, sstableRowId, indexContext.getValidator());
+ }
+ }
+ }
+ else
+ {
+ ByteBuffer value = indexContext.getValueOf(key.partitionKey(), row, nowInSec);
+ if (value != null)
+ addTerm(TypeUtil.asIndexBytes(value.duplicate(), indexContext.getValidator()), key, sstableRowId, indexContext.getValidator());
+ }
+ }
+
+ @Override
+ public void complete(Stopwatch stopwatch) throws IOException
+ {
+ if (maybeAbort())
+ return;
+
+ long start = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+ long elapsed;
+
+ boolean emptySegment = currentBuilder == null || currentBuilder.isEmpty();
+ logger.debug(indexContext.logMessage("Completing index flush with {}buffered data..."), emptySegment ? "no " : "");
+
+ try
+ {
+ // parts are present but there is something still in memory, let's flush that inline
+ if (!emptySegment)
+ {
+ flushSegment();
+ elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+ logger.debug(indexContext.logMessage("Completed flush of final segment for SSTable {}. Duration: {} ms. Total elapsed: {} ms"),
+ indexDescriptor.sstableDescriptor,
+ elapsed - start,
+ elapsed);
+ }
+
+ // Even an empty segment may carry some fixed memory, so remove it:
+ if (currentBuilder != null)
+ {
+ long bytesAllocated = currentBuilder.totalBytesAllocated();
+ long globalBytesUsed = currentBuilder.release(indexContext);
+ logger.debug(indexContext.logMessage("Flushing final segment for SSTable {} released {}. Global segment memory usage now at {}."),
+ indexDescriptor.sstableDescriptor, FBUtilities.prettyPrintMemory(bytesAllocated), FBUtilities.prettyPrintMemory(globalBytesUsed));
+ }
+
+ writeSegmentsMetadata();
+ indexDescriptor.createComponentOnDisk(IndexComponent.COLUMN_COMPLETION_MARKER, indexContext);
+ }
+ finally
+ {
+ if (indexContext.getIndexMetrics() != null)
+ {
+ indexContext.getIndexMetrics().segmentsPerCompaction.update(segments.size());
+ segments.clear();
+ indexContext.getIndexMetrics().compactionCount.inc();
+ }
+ }
+ }
+
+ @Override
+ public void abort(Throwable cause)
+ {
+ aborted = true;
+
+ logger.warn(indexContext.logMessage("Aborting SSTable index flush for {}..."), indexDescriptor.sstableDescriptor, cause);
+
+ // It's possible for the current builder to be unassigned after we flush a final segment.
+ if (currentBuilder != null)
+ {
+ // If an exception is thrown out of any writer operation prior to successful segment
+ // flush, we will end up here, and we need to free up builder memory tracked by the limiter:
+ long allocated = currentBuilder.totalBytesAllocated();
+ long globalBytesUsed = currentBuilder.release(indexContext);
+ logger.debug(indexContext.logMessage("Aborting index writer for SSTable {} released {}. Global segment memory usage now at {}."),
+ indexDescriptor.sstableDescriptor, FBUtilities.prettyPrintMemory(allocated), FBUtilities.prettyPrintMemory(globalBytesUsed));
+ }
+
+ indexDescriptor.deleteColumnIndex(indexContext);
+ }
+
+ /**
+ * abort current write if index is dropped
+ *
+ * @return true if current write is aborted.
+ */
+ private boolean maybeAbort()
+ {
+ if (aborted)
+ return true;
+
+ if (isIndexValid.getAsBoolean())
+ return false;
+
+ abort(new RuntimeException(String.format("index %s is dropped", indexContext.getIndexName())));
+ return true;
+ }
+
+ private void addTerm(ByteBuffer term, PrimaryKey key, long sstableRowId, AbstractType<?> type) throws IOException
+ {
+ if (term.remaining() >= maxTermSize)
+ {
+ noSpamLogger.warn(indexContext.logMessage(TERM_OVERSIZE_MESSAGE),
+ indexContext.getColumnName(),
+ indexContext.keyValidator().getString(key.partitionKey().getKey()),
+ FBUtilities.prettyPrintMemory(term.remaining()),
+ FBUtilities.prettyPrintMemory(maxTermSize));
+ return;
+ }
+
+ if (currentBuilder == null)
+ {
+ currentBuilder = newSegmentBuilder();
+ }
+ else if (shouldFlush(sstableRowId))
+ {
+ flushSegment();
+ currentBuilder = newSegmentBuilder();
+ }
+
+ if (term.remaining() == 0) return;
+
+ if (!TypeUtil.isLiteral(type))
+ {
+ limiter.increment(currentBuilder.add(term, key, sstableRowId));
+ }
+ else
+ {
+ analyzer.reset(term);
+ try
+ {
+ while (analyzer.hasNext())
+ {
+ ByteBuffer tokenTerm = analyzer.next();
+ limiter.increment(currentBuilder.add(tokenTerm, key, sstableRowId));
+ }
+ }
+ finally
+ {
+ analyzer.end();
+ }
+ }
+ }
+
+ private boolean shouldFlush(long sstableRowId)
+ {
+ // If we've hit the minimum flush size and, we've breached the global limit, flush a new segment:
+ boolean reachMemoryLimit = limiter.usageExceedsLimit() && currentBuilder.hasReachedMinimumFlushSize();
+
+ if (reachMemoryLimit)
+ {
+ logger.debug(indexContext.logMessage("Global limit of {} and minimum flush size of {} exceeded. " +
+ "Current builder usage is {} for {} cells. Global Usage is {}. Flushing..."),
+ FBUtilities.prettyPrintMemory(limiter.limitBytes()),
+ FBUtilities.prettyPrintMemory(currentBuilder.getMinimumFlushBytes()),
+ FBUtilities.prettyPrintMemory(currentBuilder.totalBytesAllocated()),
+ currentBuilder.getRowCount(),
+ FBUtilities.prettyPrintMemory(limiter.currentBytesUsed()));
+ }
+
+ return reachMemoryLimit || currentBuilder.exceedsSegmentLimit(sstableRowId);
+ }
+
+ private void flushSegment() throws IOException
+ {
+ long start = Clock.Global.nanoTime();
+
+ try
+ {
+ long bytesAllocated = currentBuilder.totalBytesAllocated();
+
+ SegmentMetadata segmentMetadata = currentBuilder.flush(indexDescriptor, indexContext);
+
+ long flushMillis = Math.max(1, TimeUnit.NANOSECONDS.toMillis(Clock.Global.nanoTime() - start));
+
+ if (segmentMetadata != null)
+ {
+ segments.add(segmentMetadata);
+
+ double rowCount = segmentMetadata.numRows;
+ if (indexContext.getIndexMetrics() != null)
+ indexContext.getIndexMetrics().compactionSegmentCellsPerSecond.update((long)(rowCount / flushMillis * 1000.0));
+
+ double segmentBytes = segmentMetadata.componentMetadatas.indexSize();
+ if (indexContext.getIndexMetrics() != null)
+ indexContext.getIndexMetrics().compactionSegmentBytesPerSecond.update((long)(segmentBytes / flushMillis * 1000.0));
+
+ logger.debug(indexContext.logMessage("Flushed segment with {} cells for a total of {} in {} ms."),
+ (long) rowCount, FBUtilities.prettyPrintMemory((long) segmentBytes), flushMillis);
+ }
+
+ // Builder memory is released against the limiter at the conclusion of a successful
+ // flush. Note that any failure that occurs before this (even in term addition) will
+ // actuate this column writer's abort logic from the parent SSTable-level writer, and
+ // that abort logic will release the current builder's memory against the limiter.
+ long globalBytesUsed = currentBuilder.release(indexContext);
+ currentBuilder = null;
+ logger.debug(indexContext.logMessage("Flushing index segment for SSTable {} released {}. Global segment memory usage now at {}."),
+ indexDescriptor.sstableDescriptor, FBUtilities.prettyPrintMemory(bytesAllocated), FBUtilities.prettyPrintMemory(globalBytesUsed));
+
+ }
+ catch (Throwable t)
+ {
+ logger.error(indexContext.logMessage("Failed to build index for SSTable {}."), indexDescriptor.sstableDescriptor, t);
+ indexDescriptor.deleteColumnIndex(indexContext);
+ indexContext.getIndexMetrics().segmentFlushErrors.inc();
+ throw t;
+ }
+ }
+
+ private void writeSegmentsMetadata() throws IOException
+ {
+ if (segments.isEmpty())
+ return;
+
+ try (MetadataWriter writer = new MetadataWriter(indexDescriptor.openPerIndexOutput(IndexComponent.META, indexContext)))
+ {
+ SegmentMetadata.write(writer, segments);
+ }
+ catch (IOException e)
+ {
+ abort(e);
+ throw e;
+ }
+ }
+
+ private SegmentBuilder newSegmentBuilder()
+ {
+ SegmentBuilder builder = new SegmentBuilder.RAMStringSegmentBuilder(indexContext.getValidator(), limiter);
+
+ long globalBytesUsed = limiter.increment(builder.totalBytesAllocated());
+ logger.debug(indexContext.logMessage("Created new segment builder while flushing SSTable {}. Global segment memory usage now at {}."),
+ indexDescriptor.sstableDescriptor,
+ FBUtilities.prettyPrintMemory(globalBytesUsed));
+
+ return builder;
+ }
+}
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
new file mode 100644
index 0000000000..c55d4b07ef
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.Gauge;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.SSTableContext;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.index.sai.disk.PerColumnIndexWriter;
+import org.apache.cassandra.index.sai.disk.PerSSTableIndexWriter;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.RowMapping;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.format.OnDiskFormat;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentBuilder;
+import org.apache.cassandra.index.sai.metrics.AbstractMetrics;
+import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.metrics.CassandraMetricsRegistry;
+import org.apache.cassandra.metrics.DefaultNameFactory;
+import org.apache.lucene.store.IndexInput;
+
+import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemory;
+
+public class V1OnDiskFormat implements OnDiskFormat
+{
+ private static final Logger logger = LoggerFactory.getLogger(V1OnDiskFormat.class);
+
+ private static final Set<IndexComponent> PER_SSTABLE_COMPONENTS = EnumSet.of(IndexComponent.GROUP_COMPLETION_MARKER,
+ IndexComponent.GROUP_META,
+ IndexComponent.TOKEN_VALUES,
+ IndexComponent.PRIMARY_KEY_TRIE,
+ IndexComponent.PRIMARY_KEY_BLOCKS,
+ IndexComponent.PRIMARY_KEY_BLOCK_OFFSETS);
+ private static final Set<IndexComponent> LITERAL_COMPONENTS = EnumSet.of(IndexComponent.COLUMN_COMPLETION_MARKER,
+ IndexComponent.META,
+ IndexComponent.TERMS_DATA,
+ IndexComponent.POSTING_LISTS);
+
+ /**
+ * Global limit on heap consumed by all index segment building that occurs outside the context of Memtable flush.
+ *
+ * Note that to avoid flushing 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).
+ *
+ * ex. If there is only one column index building, it can buffer up to segment_write_buffer_space_mb.
+ *
+ * ex. If there is one column index building per table across 8 compactors, each index will be
+ * eligible to flush once it reaches (segment_write_buffer_space_mb / 8) MBs.
+ */
+ public static final long SEGMENT_BUILD_MEMORY_LIMIT = DatabaseDescriptor.getSAISegmentWriteBufferSpace().toBytes();
+
+ public static final NamedMemoryLimiter SEGMENT_BUILD_MEMORY_LIMITER = new NamedMemoryLimiter(SEGMENT_BUILD_MEMORY_LIMIT,
+ "Storage Attached Index Segment Builder");
+
+ static
+ {
+ CassandraMetricsRegistry.MetricName bufferSpaceUsed = DefaultNameFactory.createMetricName(AbstractMetrics.TYPE, "SegmentBufferSpaceUsedBytes", null);
+ CassandraMetricsRegistry.Metrics.register(bufferSpaceUsed, (Gauge<Long>) SEGMENT_BUILD_MEMORY_LIMITER::currentBytesUsed);
+
+ CassandraMetricsRegistry.MetricName bufferSpaceLimit = DefaultNameFactory.createMetricName(AbstractMetrics.TYPE, "SegmentBufferSpaceLimitBytes", null);
+ CassandraMetricsRegistry.Metrics.register(bufferSpaceLimit, (Gauge<Long>) () -> SEGMENT_BUILD_MEMORY_LIMIT);
+
+ CassandraMetricsRegistry.MetricName buildsInProgress = DefaultNameFactory.createMetricName(AbstractMetrics.TYPE, "ColumnIndexBuildsInProgress", null);
+ CassandraMetricsRegistry.Metrics.register(buildsInProgress, (Gauge<Integer>) SegmentBuilder::getActiveBuilderCount);
+ }
+
+ public static final V1OnDiskFormat instance = new V1OnDiskFormat();
+
+ protected V1OnDiskFormat()
+ {}
+
+ @Override
+ public PrimaryKeyMap.Factory newPrimaryKeyMapFactory(IndexDescriptor indexDescriptor, SSTableReader sstable)
+ {
+ return new RowAwarePrimaryKeyMap.RowAwarePrimaryKeyMapFactory(indexDescriptor, sstable);
+ }
+
+ @Override
+ public SSTableIndex newSSTableIndex(SSTableContext sstableContext, IndexContext indexContext)
+ {
+ return new V1SSTableIndex(sstableContext, indexContext);
+ }
+
+ @Override
+ public PerSSTableIndexWriter newPerSSTableIndexWriter(IndexDescriptor indexDescriptor) throws IOException
+ {
+ return new SSTableComponentsWriter(indexDescriptor);
+ }
+
+ @Override
+ public PerColumnIndexWriter newPerColumnIndexWriter(StorageAttachedIndex index,
+ IndexDescriptor indexDescriptor,
+ LifecycleNewTracker tracker,
+ RowMapping rowMapping)
+ {
+ // If we're not flushing, or we haven't yet started the initialization build, flush from SSTable contents.
+ if (tracker.opType() != OperationType.FLUSH || !index.isInitBuildStarted())
+ {
+ NamedMemoryLimiter limiter = SEGMENT_BUILD_MEMORY_LIMITER;
+ logger.info(index.getIndexContext().logMessage("Starting a compaction index build. Global segment memory usage: {}"),
+ prettyPrintMemory(limiter.currentBytesUsed()));
+
+ return new SSTableIndexWriter(indexDescriptor, index.getIndexContext(), limiter, index.isIndexValid());
+ }
+
+ return new MemtableIndexWriter(index.getIndexContext().getMemtableIndexManager().getPendingMemtableIndex(tracker),
+ indexDescriptor,
+ index.getIndexContext(),
+ rowMapping);
+ }
+
+ @Override
+ public boolean isPerSSTableIndexBuildComplete(IndexDescriptor indexDescriptor)
+ {
+ return indexDescriptor.hasComponent(IndexComponent.GROUP_COMPLETION_MARKER);
+ }
+
+ @Override
+ public boolean isPerColumnIndexBuildComplete(IndexDescriptor indexDescriptor, IndexContext indexContext)
+ {
+ return indexDescriptor.hasComponent(IndexComponent.GROUP_COMPLETION_MARKER) &&
+ indexDescriptor.hasComponent(IndexComponent.COLUMN_COMPLETION_MARKER, indexContext);
+ }
+
+ @Override
+ public boolean validatePerSSTableIndexComponents(IndexDescriptor indexDescriptor, boolean checksum)
+ {
+ for (IndexComponent indexComponent : perSSTableIndexComponents())
+ {
+ if (isNotBuildCompletionMarker(indexComponent))
+ {
+ try (IndexInput input = indexDescriptor.openPerSSTableInput(indexComponent))
+ {
+ if (checksum)
+ SAICodecUtils.validateChecksum(input);
+ else
+ SAICodecUtils.validate(input);
+ }
+ catch (Throwable e)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(indexDescriptor.logMessage("{} failed for index component {} on SSTable {}"),
+ (checksum ? "Checksum validation" : "Validation"),
+ indexComponent,
+ indexDescriptor.sstableDescriptor);
+ }
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean validatePerColumnIndexComponents(IndexDescriptor indexDescriptor, IndexContext indexContext, boolean checksum)
+ {
+ for (IndexComponent indexComponent : perColumnIndexComponents(indexContext))
+ {
+ if (isNotBuildCompletionMarker(indexComponent))
+ {
+ try (IndexInput input = indexDescriptor.openPerIndexInput(indexComponent, indexContext))
+ {
+ if (checksum)
+ SAICodecUtils.validateChecksum(input);
+ else
+ SAICodecUtils.validate(input);
+ }
+ catch (Throwable e)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(indexDescriptor.logMessage("{} failed for index component {} on SSTable {}"),
+ (checksum ? "Checksum validation" : "Validation"),
+ indexComponent,
+ indexDescriptor.sstableDescriptor);
+ }
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Set<IndexComponent> perSSTableIndexComponents()
+ {
+ return PER_SSTABLE_COMPONENTS;
+ }
+
+ @Override
+ public Set<IndexComponent> perColumnIndexComponents(IndexContext indexContext)
+ {
+ return LITERAL_COMPONENTS;
+ }
+
+ @Override
+ public int openFilesPerSSTableIndex()
+ {
+ // For the V1 format there are always 4 open files per SSTable - token values, primary key trie,
+ // primary key blocks, primary key block offsets
+ return 4;
+ }
+
+ @Override
+ public int openFilesPerColumnIndex(IndexContext indexContext)
+ {
+ // For the V1 format there are always 2 open files per index - index (kdtree or terms) + postings
+ return 2;
+ }
+
+ protected boolean isNotBuildCompletionMarker(IndexComponent indexComponent)
+ {
+ return indexComponent != IndexComponent.GROUP_COMPLETION_MARKER &&
+ indexComponent != IndexComponent.COLUMN_COMPLETION_MARKER;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableIndex.java b/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableIndex.java
new file mode 100644
index 0000000000..fd89b302c8
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1SSTableIndex.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.sai.disk.v1;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.virtual.SimpleDataSet;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.SSTableContext;
+import org.apache.cassandra.index.sai.disk.SSTableIndex;
+import org.apache.cassandra.index.sai.disk.v1.segment.Segment;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.CELL_COUNT;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.COLUMN_NAME;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.COMPONENT_METADATA;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.END_TOKEN;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MAX_SSTABLE_ROW_ID;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MAX_TERM;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MIN_SSTABLE_ROW_ID;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.MIN_TERM;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.START_TOKEN;
+import static org.apache.cassandra.index.sai.virtual.SegmentsSystemView.TABLE_NAME;
+
+/**
+ * A version specific implementation of the {@link SSTableIndex} where the
+ * index is segmented
+ */
+public class V1SSTableIndex extends SSTableIndex
+{
+ private final ImmutableList<Segment> segments;
+ private final List<SegmentMetadata> metadatas;
+ private final AbstractBounds<PartitionPosition> bounds;
+ private final ByteBuffer minTerm;
+ private final ByteBuffer maxTerm;
+ private final long minSSTableRowId, maxSSTableRowId;
+ private final long numRows;
+
+ private PerColumnIndexFiles indexFiles;
+
+ public V1SSTableIndex(SSTableContext sstableContext, IndexContext indexContext)
+ {
+ super(sstableContext, indexContext);
+
+ try
+ {
+ this.indexFiles = new PerColumnIndexFiles(sstableContext.indexDescriptor, indexContext);
+
+ ImmutableList.Builder<Segment> segmentsBuilder = ImmutableList.builder();
+
+ final MetadataSource source = MetadataSource.loadColumnMetadata(sstableContext.indexDescriptor, indexContext);
+
+ metadatas = SegmentMetadata.load(source, sstableContext.indexDescriptor.primaryKeyFactory);
+
+ for (SegmentMetadata metadata : metadatas)
+ {
+ segmentsBuilder.add(new Segment(indexContext, sstableContext, indexFiles, metadata));
+ }
+
+ segments = segmentsBuilder.build();
+ assert !segments.isEmpty();
+
+ DecoratedKey minKey = metadatas.get(0).minKey.partitionKey();
+ DecoratedKey maxKey = metadatas.get(metadatas.size() - 1).maxKey.partitionKey();
+
+ this.bounds = AbstractBounds.bounds(minKey, true, maxKey, true);
+
+ this.minTerm = metadatas.stream().map(m -> m.minTerm).min(TypeUtil.comparator(indexContext.getValidator())).orElse(null);
+ this.maxTerm = metadatas.stream().map(m -> m.maxTerm).max(TypeUtil.comparator(indexContext.getValidator())).orElse(null);
+
+ this.numRows = metadatas.stream().mapToLong(m -> m.numRows).sum();
+
+ this.minSSTableRowId = metadatas.get(0).minSSTableRowId;
+ this.maxSSTableRowId = metadatas.get(metadatas.size() - 1).maxSSTableRowId;
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(indexFiles);
+ FileUtils.closeQuietly(sstableContext);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ @Override
+ public long indexFileCacheSize()
+ {
+ return segments.stream().mapToLong(Segment::indexFileCacheSize).sum();
+ }
+
+ @Override
+ public long getRowCount()
+ {
+ return numRows;
+ }
+
+ @Override
+ public long minSSTableRowId()
+ {
+ return minSSTableRowId;
+ }
+
+ @Override
+ public long maxSSTableRowId()
+ {
+ return maxSSTableRowId;
+ }
+
+ @Override
+ public ByteBuffer minTerm()
+ {
+ return minTerm;
+ }
+
+ @Override
+ public ByteBuffer maxTerm()
+ {
+ return maxTerm;
+ }
+
+ @Override
+ public AbstractBounds<PartitionPosition> bounds()
+ {
+ return bounds;
+ }
+
+ @Override
+ public List<KeyRangeIterator> search(Expression expression,
+ AbstractBounds<PartitionPosition> keyRange,
+ QueryContext context) throws IOException
+ {
+ List<KeyRangeIterator> segmentIterators = new ArrayList<>();
+
+ for (Segment segment : segments)
+ {
+ if (segment.intersects(keyRange))
+ {
+ segmentIterators.add(segment.search(expression, context));
+ }
+ }
+
+ return segmentIterators;
+ }
+
+ @Override
+ public void populateSegmentView(SimpleDataSet dataset)
+ {
+ SSTableReader sstable = getSSTable();
+ Token.TokenFactory tokenFactory = sstable.metadata().partitioner.getTokenFactory();
+
+ for (SegmentMetadata metadata : metadatas)
+ {
+ dataset.row(sstable.metadata().keyspace, indexContext.getIndexName(), sstable.getFilename(), metadata.rowIdOffset)
+ .column(TABLE_NAME, sstable.descriptor.cfname)
+ .column(COLUMN_NAME, indexContext.getColumnName())
+ .column(CELL_COUNT, metadata.numRows)
+ .column(MIN_SSTABLE_ROW_ID, metadata.minSSTableRowId)
+ .column(MAX_SSTABLE_ROW_ID, metadata.maxSSTableRowId)
+ .column(START_TOKEN, tokenFactory.toString(metadata.minKey.token()))
+ .column(END_TOKEN, tokenFactory.toString(metadata.maxKey.token()))
+ .column(MIN_TERM, indexContext.getValidator().getSerializer().deserialize(metadata.minTerm).toString())
+ .column(MAX_TERM, indexContext.getValidator().getSerializer().deserialize(metadata.maxTerm).toString())
+ .column(COMPONENT_METADATA, metadata.componentMetadatas.asMap());
+ }
+ }
+
+ @Override
+ protected void internalRelease()
+ {
+ FileUtils.closeQuietly(indexFiles);
+ FileUtils.closeQuietly(segments);
+ }
+}
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
new file mode 100644
index 0000000000..4d0bb40118
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedReader.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.v1.bitpack;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.LongArray;
+import org.apache.lucene.store.IndexInput;
+
+@NotThreadSafe
+public abstract class AbstractBlockPackedReader implements LongArray
+{
+ private final int blockShift;
+ private final int blockMask;
+ private final long valueCount;
+ private final byte[] blockBitsPerValue;
+ private final SeekingRandomAccessInput input;
+
+ AbstractBlockPackedReader(IndexInput indexInput, byte[] blockBitsPerValue, int blockShift, int blockMask, long valueCount)
+ {
+ this.blockShift = blockShift;
+ this.blockMask = blockMask;
+ this.valueCount = valueCount;
+ this.input = new SeekingRandomAccessInput(indexInput);
+ this.blockBitsPerValue = blockBitsPerValue;
+ }
+
+ protected abstract long blockOffsetAt(int block);
+
+ @Override
+ public long get(final long valueIndex)
+ {
+ if (valueIndex < 0 || valueIndex >= valueCount)
+ {
+ throw new IndexOutOfBoundsException(String.format("Index should be between [0, %d), but was %d.", valueCount, valueIndex));
+ }
+
+ int blockIndex = (int) (valueIndex >>> blockShift);
+ int inBlockIndex = (int) (valueIndex & blockMask);
+ DirectReaders.Reader subReader = DirectReaders.getReaderForBitsPerValue(blockBitsPerValue[blockIndex]);
+ return delta(blockIndex, inBlockIndex) + subReader.get(input, blockOffsetAt(blockIndex), inBlockIndex);
+ }
+
+ @Override
+ public long length()
+ {
+ return valueCount;
+ }
+
+ abstract long delta(int block, int idx);
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedWriter.java
new file mode 100644
index 0000000000..ead4f5986f
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/AbstractBlockPackedWriter.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.bitpack;
+
+import java.io.IOException;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.DirectWriter;
+
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.checkBlockSize;
+
+/**
+ * Modified copy of {@code org.apache.lucene.util.packed.AbstractBlockPackedWriter} to use {@link DirectWriter} for
+ * optimised reads that doesn't require seeking through the whole file to open a thread-exclusive reader.
+ */
+public abstract class AbstractBlockPackedWriter
+{
+ static final int MIN_BLOCK_SIZE = 64;
+ static final int MAX_BLOCK_SIZE = 1 << (30 - 3);
+
+ protected final IndexOutput indexOutput;
+ protected final long[] blockValues;
+ // This collects metadata specific to the block packed writer being used during the
+ // writing of the block packed data. This cached metadata is then written to the end
+ // of the data file when the block packed writer is finished.
+ protected final RAMIndexOutput blockMetaWriter;
+
+ protected int blockIndex;
+ protected boolean finished;
+
+ AbstractBlockPackedWriter(IndexOutput indexOutput, int blockSize)
+ {
+ checkBlockSize(blockSize, MIN_BLOCK_SIZE, MAX_BLOCK_SIZE);
+ this.indexOutput = indexOutput;
+ this.blockMetaWriter = new RAMIndexOutput("BlockPackedMeta");
+ blockValues = new long[blockSize];
+ }
+
+
+ /**
+ * Append a new long.
+ */
+ public void add(long l) throws IOException
+ {
+ checkNotFinished();
+ if (blockIndex == blockValues.length)
+ {
+ flush();
+ }
+ blockValues[blockIndex++] = l;
+ }
+
+ /**
+ * Flush all buffered data to disk. This instance is not usable anymore
+ * after this method has been called.
+ *
+ * @return a file offset to the block metadata
+ */
+ public long finish() throws IOException
+ {
+ checkNotFinished();
+ if (blockIndex > 0)
+ {
+ flush();
+ }
+ final long fp = indexOutput.getFilePointer();
+ blockMetaWriter.writeTo(indexOutput);
+ finished = true;
+ return fp;
+ }
+
+ protected abstract void flushBlock() throws IOException;
+
+ void writeValues(int numValues, int bitsPerValue) throws IOException
+ {
+ final DirectWriter writer = DirectWriter.getInstance(indexOutput, numValues, bitsPerValue);
+ for (int i = 0; i < numValues; ++i)
+ {
+ writer.add(blockValues[i]);
+ }
+ writer.finish();
+ }
+
+ void writeVLong(IndexOutput out, long i) throws IOException
+ {
+ int k = 0;
+ while ((i & ~0x7FL) != 0L && k++ < 8)
+ {
+ out.writeByte((byte) ((i & 0x7FL) | 0x80L));
+ i >>>= 7;
+ }
+ out.writeByte((byte) i);
+ }
+
+ private void flush() throws IOException
+ {
+ flushBlock();
+ blockIndex = 0;
+ }
+
+ private void checkNotFinished()
+ {
+ if (finished)
+ {
+ throw new IllegalStateException(String.format("[%s] Writer already finished!", indexOutput.getName()));
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedReader.java
new file mode 100644
index 0000000000..fa42b25fb8
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedReader.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.bitpack;
+
+import java.io.IOException;
+
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.LongArray;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.IndexInput;
+
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.checkBlockSize;
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.numBlocks;
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.readVLong;
+import static org.apache.lucene.util.BitUtil.zigZagDecode;
+
+/**
+ * Provides non-blocking, random access to a stream written with {@link BlockPackedWriter}.
+ */
+public class BlockPackedReader implements LongArray.Factory
+{
+ private final FileHandle file;
+ private final int blockShift;
+ private final int blockMask;
+ private final long valueCount;
+ private final byte[] blockBitsPerValue;
+ private final long[] blockOffsets;
+ private final long[] minValues;
+
+ public BlockPackedReader(FileHandle file, NumericValuesMeta meta) throws IOException
+ {
+ this.file = file;
+
+ this.valueCount = meta.valueCount;
+
+ blockShift = checkBlockSize(meta.blockSize, AbstractBlockPackedWriter.MIN_BLOCK_SIZE, AbstractBlockPackedWriter.MAX_BLOCK_SIZE);
+ blockMask = meta.blockSize - 1;
+ int numBlocks = numBlocks(valueCount, meta.blockSize);
+ blockBitsPerValue = new byte[numBlocks];
+ blockOffsets = new long[numBlocks];
+ minValues = new long[numBlocks];
+
+ try (RandomAccessReader reader = this.file.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(meta.blockMetaOffset);
+
+ for (int i = 0; i < numBlocks; ++i)
+ {
+ final int token = in.readByte() & 0xFF;
+ final int bitsPerValue = token >>> BlockPackedWriter.BPV_SHIFT;
+ if (!DirectReaders.SUPPORTED_BITS_PER_VALUE.contains(bitsPerValue))
+ {
+ throw new CorruptIndexException(String.format("Block %d is corrupted. Bits per value is %d. Supported values are %s.",
+ i, bitsPerValue, DirectReaders.SUPPORTED_BITS_PER_VALUE_STRING), in);
+ }
+ if ((token & BlockPackedWriter.MIN_VALUE_EQUALS_0) == 0)
+ {
+ long val = zigZagDecode(1L + readVLong(in));
+ minValues[i] = val;
+ }
+ else
+ {
+ minValues[i] = 0L;
+ }
+
+ blockBitsPerValue[i] = (byte) bitsPerValue;
+
+ if (bitsPerValue > 0)
+ {
+ blockOffsets[i] = in.readVLong();
+ }
+ else
+ {
+ blockOffsets[i] = -1;
+ }
+ }
+ }
+ }
+
+ @Override
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public LongArray open()
+ {
+ IndexInput indexInput = IndexFileUtils.instance.openInput(file);
+ return new AbstractBlockPackedReader(indexInput, blockBitsPerValue, blockShift, blockMask, valueCount)
+ {
+ @Override
+ protected long blockOffsetAt(int block)
+ {
+ return blockOffsets[block];
+ }
+
+ @Override
+ long delta(int block, int idx)
+ {
+ return minValues[block];
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ indexInput.close();
+ }
+ };
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedWriter.java
new file mode 100644
index 0000000000..9dcc29c6ab
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/BlockPackedWriter.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.bitpack;
+
+import java.io.IOException;
+
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.DirectWriter;
+
+import static org.apache.lucene.util.BitUtil.zigZagEncode;
+
+/**
+ * A writer for large sequences of longs.
+ *
+ * Modified copy of {@link org.apache.lucene.util.packed.BlockPackedWriter} to use {@link DirectWriter}
+ * for optimised reads that doesn't require seeking through the whole file to open a thread-exclusive reader.
+ */
+public class BlockPackedWriter extends AbstractBlockPackedWriter
+{
+ static final int BPV_SHIFT = 1;
+ static final int MIN_VALUE_EQUALS_0 = 1;
+
+ public BlockPackedWriter(IndexOutput out, int blockSize)
+ {
+ super(out, blockSize);
+ }
+
+ @Override
+ protected void flushBlock() throws IOException
+ {
+ long min = Long.MAX_VALUE, max = Long.MIN_VALUE;
+ for (int i = 0; i < blockIndex; ++i)
+ {
+ min = Math.min(blockValues[i], min);
+ max = Math.max(blockValues[i], max);
+ }
+
+ long delta = max - min;
+ int bitsRequired = delta == 0 ? 0 : DirectWriter.unsignedBitsRequired(delta);
+
+ int shiftedBitsRequired = (bitsRequired << BPV_SHIFT) | (min == 0 ? MIN_VALUE_EQUALS_0 : 0);
+ blockMetaWriter.writeByte((byte) shiftedBitsRequired);
+
+ if (min != 0)
+ {
+ writeVLong(blockMetaWriter, zigZagEncode(min) - 1);
+ }
+
+ if (bitsRequired > 0)
+ {
+ if (min != 0)
+ {
+ for (int i = 0; i < blockIndex; ++i)
+ {
+ blockValues[i] -= min;
+ }
+ }
+ blockMetaWriter.writeVLong(indexOutput.getFilePointer());
+ writeValues(blockIndex, bitsRequired);
+ }
+ }
+}
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
new file mode 100644
index 0000000000..872a44e02f
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedReader.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1.bitpack;
+
+import java.io.IOException;
+
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.LongArray;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.checkBlockSize;
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.numBlocks;
+
+/**
+ * Provides non-blocking, random access to a stream written with {@link MonotonicBlockPackedWriter}.
+ */
+public class MonotonicBlockPackedReader implements LongArray.Factory
+{
+ private final FileHandle file;
+ private final int blockShift;
+ private final int blockMask;
+ private final long valueCount;
+ private final byte[] blockBitsPerValue;
+ private final PackedLongValues blockOffsets;
+ private final PackedLongValues minValues;
+ private final float[] averages;
+
+ public MonotonicBlockPackedReader(FileHandle file, NumericValuesMeta meta) throws IOException
+ {
+ this.valueCount = meta.valueCount;
+ blockShift = checkBlockSize(meta.blockSize, AbstractBlockPackedWriter.MIN_BLOCK_SIZE, AbstractBlockPackedWriter.MAX_BLOCK_SIZE);
+ blockMask = meta.blockSize - 1;
+ int numBlocks = numBlocks(valueCount, meta.blockSize);
+ PackedLongValues.Builder minValuesBuilder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+ PackedLongValues.Builder blockOffsetsBuilder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+ averages = new float[numBlocks];
+ blockBitsPerValue = new byte[numBlocks];
+ this.file = file;
+
+ try (RandomAccessReader reader = this.file.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+
+ in.seek(meta.blockMetaOffset);
+ for (int i = 0; i < numBlocks; ++i)
+ {
+ minValuesBuilder.add(in.readZLong());
+ averages[i] = Float.intBitsToFloat(in.readInt());
+ final int bitsPerValue = in.readVInt();
+ if (!DirectReaders.SUPPORTED_BITS_PER_VALUE.contains(bitsPerValue))
+ {
+ throw new CorruptIndexException(String.format("Block %d is corrupted. Bits per value is %d. Supported values are %s.",
+ i, bitsPerValue, DirectReaders.SUPPORTED_BITS_PER_VALUE_STRING), in);
+ }
+ blockBitsPerValue[i] = (byte) bitsPerValue;
+ // when bitsPerValue is 0, block offset won't be used
+ blockOffsetsBuilder.add(bitsPerValue == 0 ? -1 : in.readVLong());
+ }
+ }
+
+ blockOffsets = blockOffsetsBuilder.build();
+ minValues = minValuesBuilder.build();
+ }
+
+ @Override
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public LongArray open()
+ {
+ final IndexInput indexInput = IndexFileUtils.instance.openInput(file);
+ return new AbstractBlockPackedReader(indexInput, blockBitsPerValue, blockShift, blockMask, valueCount)
+ {
+ @Override
+ long delta(int block, int idx)
+ {
+ return expected(minValues.get(block), averages[block], idx);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ indexInput.close();
+ }
+
+ @Override
+ protected long blockOffsetAt(int block)
+ {
+ return blockOffsets.get(block);
+ }
+ };
+ }
+
+ public static long expected(long origin, float average, int index)
+ {
+ return origin + (long)(average * index);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedWriter.java
new file mode 100644
index 0000000000..5845aabc77
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/MonotonicBlockPackedWriter.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1.bitpack;
+
+import java.io.IOException;
+
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * A writer for large monotonically increasing sequences of positive longs.
+ *
+ * The writer is optimised for monotonic sequences and stores values as a series of deltas
+ * from an expected value. The expected value is calculated from the minimum value in the block and the average
+ * delta for the block. This means that stored values are generally smaller and can be packed
+ * into a smaller number of bits, allowing for larger block sizes.
+ *
+ * Modified copy of {@link org.apache.lucene.util.packed.MonotonicBlockPackedWriter} to use {@link DirectWriter} for
+ * optimised reads that doesn't require seeking through the whole file to open a thread-exclusive reader.
+ */
+public class MonotonicBlockPackedWriter extends AbstractBlockPackedWriter
+{
+ public MonotonicBlockPackedWriter(IndexOutput out, int blockSize)
+ {
+ super(out, blockSize);
+ }
+
+ @Override
+ public void add(long l) throws IOException
+ {
+ assert l >= 0;
+ super.add(l);
+ }
+
+ @Override
+ protected void flushBlock() throws IOException
+ {
+ final float averageDelta = blockIndex == 1 ? 0f : (float) (blockValues[blockIndex - 1] - blockValues[0]) / (blockIndex - 1);
+ long minimumValue = blockValues[0];
+ // adjust minimumValue so that all deltas will be positive
+ for (int index = 1; index < blockIndex; ++index)
+ {
+ long actual = blockValues[index];
+ long expected = MonotonicBlockPackedReader.expected(minimumValue, averageDelta, index);
+ if (expected > actual)
+ {
+ minimumValue -= (expected - actual);
+ }
+ }
+
+ long maxDelta = 0;
+ for (int i = 0; i < blockIndex; ++i)
+ {
+ blockValues[i] = blockValues[i] - MonotonicBlockPackedReader.expected(minimumValue, averageDelta, i);
+ maxDelta = Math.max(maxDelta, blockValues[i]);
+ }
+
+ blockMetaWriter.writeZLong(minimumValue);
+ blockMetaWriter.writeInt(Float.floatToIntBits(averageDelta));
+ if (maxDelta == 0)
+ {
+ blockMetaWriter.writeVInt(0);
+ }
+ else
+ {
+ final int bitsRequired = DirectWriter.bitsRequired(maxDelta);
+ blockMetaWriter.writeVInt(bitsRequired);
+ blockMetaWriter.writeVLong(indexOutput.getFilePointer());
+ writeValues(blockIndex, bitsRequired);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/virtual/StorageAttachedIndexTables.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesMeta.java
similarity index 52%
copy from src/java/org/apache/cassandra/index/sai/virtual/StorageAttachedIndexTables.java
copy to src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesMeta.java
index 0d4f12678f..9d52150f5f 100644
--- a/src/java/org/apache/cassandra/index/sai/virtual/StorageAttachedIndexTables.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesMeta.java
@@ -15,21 +15,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.cassandra.index.sai.disk.v1.bitpack;
-package org.apache.cassandra.index.sai.virtual;
+import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.IndexOutput;
-import org.apache.cassandra.db.virtual.VirtualTable;
-
-public class StorageAttachedIndexTables
+public class NumericValuesMeta
{
- private StorageAttachedIndexTables()
- {}
+ public final long valueCount;
+ public final int blockSize;
+ public final long blockMetaOffset;
+
+ public NumericValuesMeta(IndexInput input) throws IOException
+ {
+ valueCount = input.readLong();
+ blockSize = input.readInt();
+ blockMetaOffset = input.readVLong();
+ }
- public static Collection<VirtualTable> getAll(String keyspace)
+ public static void write(IndexOutput out, long valueCount, int blockSize, long blockMetaOffset) throws IOException
{
- return Collections.singletonList(new IndexesSystemView(keyspace));
+ out.writeLong(valueCount);
+ out.writeInt(blockSize);
+ out.writeVLong(blockMetaOffset);
}
}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesWriter.java
new file mode 100644
index 0000000000..461c15b7dd
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/bitpack/NumericValuesWriter.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.bitpack;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.MetadataWriter;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.IndexOutput;
+
+@NotThreadSafe
+public class NumericValuesWriter implements Closeable
+{
+ public static final int MONOTONIC_BLOCK_SIZE = 16384;
+ public static final int BLOCK_SIZE = 128;
+
+ private final IndexOutput output;
+ private final AbstractBlockPackedWriter writer;
+ private final MetadataWriter metadataWriter;
+ private final String componentName;
+ private final int blockSize;
+ private long count = 0;
+
+ public NumericValuesWriter(String componentName,
+ IndexOutput indexOutput,
+ MetadataWriter metadataWriter,
+ boolean monotonic) throws IOException
+ {
+ this(componentName, indexOutput, metadataWriter, monotonic, monotonic ? MONOTONIC_BLOCK_SIZE : BLOCK_SIZE);
+ }
+
+ public NumericValuesWriter(IndexDescriptor indexDescriptor,
+ IndexComponent component,
+ MetadataWriter metadataWriter,
+ boolean monotonic,
+ int blockSize) throws IOException
+ {
+ this(indexDescriptor.componentName(component),
+ indexDescriptor.openPerSSTableOutput(component),
+ metadataWriter,
+ monotonic,
+ blockSize);
+ }
+
+ private NumericValuesWriter(String componentName,
+ IndexOutput indexOutput,
+ MetadataWriter metadataWriter,
+ boolean monotonic, int blockSize) throws IOException
+ {
+ SAICodecUtils.writeHeader(indexOutput);
+ this.writer = monotonic ? new MonotonicBlockPackedWriter(indexOutput, blockSize)
+ : new BlockPackedWriter(indexOutput, blockSize);
+ this.output = indexOutput;
+ this.componentName = componentName;
+ this.metadataWriter = metadataWriter;
+ this.blockSize = blockSize;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ try (IndexOutput o = metadataWriter.builder(componentName))
+ {
+ long fp = writer.finish();
+ SAICodecUtils.writeFooter(output);
+
+ NumericValuesMeta.write(o, count, blockSize, fp);
+ }
+ finally
+ {
+ output.close();
+ }
+ }
+
+ public void add(long value) throws IOException
+ {
+ writer.add(value);
+ count++;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/MergePostingList.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/MergePostingList.java
new file mode 100644
index 0000000000..e20986dd71
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/MergePostingList.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.postings;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.PriorityQueue;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileUtils;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Merges multiple {@link PostingList} which individually contain unique items into a single list.
+ * While the individual lists contain unique items, there can be duplicate items between lists so
+ * the class also checks for duplicates and only returns unique items in sorted order.
+ */
+@NotThreadSafe
+public class MergePostingList implements PostingList
+{
+ private final PriorityQueue<PeekablePostingList> postingLists;
+ private final List<PeekablePostingList> temp;
+ private final Closeable onClose;
+ private final long size;
+ private long lastRowId = -1;
+
+ private MergePostingList(PriorityQueue<PeekablePostingList> postingLists, Closeable onClose)
+ {
+ this.temp = new ArrayList<>(postingLists.size());
+ this.onClose = onClose;
+ this.postingLists = postingLists;
+ long totalPostings = 0;
+ for (PostingList postingList : postingLists)
+ {
+ totalPostings += postingList.size();
+ }
+ this.size = totalPostings;
+ }
+
+ public static PostingList merge(PriorityQueue<PeekablePostingList> postings, Closeable onClose)
+ {
+ checkArgument(!postings.isEmpty(), "Cannot merge an empty queue of posting lists");
+ return postings.size() > 1 ? new MergePostingList(postings, onClose) : postings.poll();
+ }
+
+ public static PostingList merge(PriorityQueue<PeekablePostingList> postings)
+ {
+ return merge(postings, () -> FileUtils.close(postings));
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ @Override
+ public long nextPosting() throws IOException
+ {
+ while (!postingLists.isEmpty())
+ {
+ PeekablePostingList head = postingLists.poll();
+ long next = head.nextPosting();
+
+ if (next == END_OF_STREAM)
+ {
+ // skip current posting list
+ continue;
+ }
+
+ if (next > lastRowId)
+ {
+ lastRowId = next;
+ postingLists.add(head);
+ return next;
+ }
+ else if (next == lastRowId)
+ {
+ postingLists.add(head);
+ }
+ }
+
+ return PostingList.END_OF_STREAM;
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ @Override
+ public long advance(long targetRowID) throws IOException
+ {
+ temp.clear();
+
+ while (!postingLists.isEmpty())
+ {
+ PeekablePostingList peekable = postingLists.poll();
+ peekable.advanceWithoutConsuming(targetRowID);
+ if (peekable.peek() != PostingList.END_OF_STREAM)
+ temp.add(peekable);
+ }
+ postingLists.addAll(temp);
+
+ return nextPosting();
+ }
+
+ @Override
+ public long size()
+ {
+ return size;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(onClose);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingListRangeIterator.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingListRangeIterator.java
new file mode 100644
index 0000000000..354663a30d
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingListRangeIterator.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.postings;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.v1.segment.IndexSegmentSearcherContext;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+
+/**
+ * A key iterator based on a {@link PostingList} derived from a single index segment.
+ *
+ * <ol>
+ * <li> fetch next segment row id from posting list or skip to specific segment row id if {@link #skipTo(PrimaryKey)} is called </li>
+ * <li> add {@link IndexSegmentSearcherContext#segmentRowIdOffset} to obtain the sstable row id </li>
+ * <li> produce a {@link PrimaryKey} from {@link PrimaryKeyMap#primaryKeyFromRowId(long)} which is used
+ * to avoid fetching duplicated keys due to partition-level indexing on wide partition schema.
+ * <br/>
+ * Note: in order to reduce disk access in multi-index query, partition keys will only be fetched for intersected tokens
+ * in {@link org.apache.cassandra.index.sai.plan.StorageAttachedIndexSearcher}.
+ * </li>
+ * </ol>
+ *
+ */
+
+@NotThreadSafe
+public class PostingListRangeIterator extends KeyRangeIterator
+{
+ private static final Logger logger = LoggerFactory.getLogger(PostingListRangeIterator.class);
+
+ private final Stopwatch timeToExhaust = Stopwatch.createStarted();
+ private final QueryContext queryContext;
+
+ private final PostingList postingList;
+ private final IndexContext indexContext;
+ private final PrimaryKeyMap primaryKeyMap;
+ private final IndexSegmentSearcherContext searcherContext;
+
+ private boolean needsSkipping = false;
+ private PrimaryKey skipToToken = null;
+
+
+ /**
+ * Create a direct PostingListRangeIterator where the underlying PostingList is materialised
+ * immediately so the posting list size can be used.
+ */
+ public PostingListRangeIterator(IndexContext indexContext,
+ PrimaryKeyMap primaryKeyMap,
+ IndexSegmentSearcherContext searcherContext)
+ {
+ super(searcherContext.minimumKey, searcherContext.maximumKey, searcherContext.count());
+
+ this.indexContext = indexContext;
+ this.primaryKeyMap = primaryKeyMap;
+ this.postingList = searcherContext.postingList;
+ this.searcherContext = searcherContext;
+ this.queryContext = searcherContext.context;
+ }
+
+ @Override
+ protected void performSkipTo(PrimaryKey nextKey)
+ {
+ if (skipToToken != null && skipToToken.compareTo(nextKey) >= 0)
+ return;
+
+ skipToToken = nextKey;
+ needsSkipping = true;
+ }
+
+ @Override
+ protected PrimaryKey computeNext()
+ {
+ try
+ {
+ queryContext.checkpoint();
+
+ // just end the iterator if we don't have a postingList or current segment is skipped
+ if (exhausted())
+ return endOfData();
+
+ long rowId = getNextRowId();
+ if (rowId == PostingList.END_OF_STREAM)
+ return endOfData();
+
+ return primaryKeyMap.primaryKeyFromRowId(rowId);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Unable to provide next token!"), t);
+
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ if (logger.isTraceEnabled())
+ {
+ final long exhaustedInMills = timeToExhaust.stop().elapsed(TimeUnit.MILLISECONDS);
+ logger.trace(indexContext.logMessage("PostingListRangeIterator exhausted after {} ms"), exhaustedInMills);
+ }
+
+ FileUtils.closeQuietly(Arrays.asList(postingList, primaryKeyMap));
+ }
+
+ private boolean exhausted()
+ {
+ return needsSkipping && skipToToken.compareTo(getMaximum()) > 0;
+ }
+
+ /**
+ * reads the next sstable row ID from the underlying posting list, potentially skipping to get there.
+ */
+ private long getNextRowId() throws IOException
+ {
+ long segmentRowId;
+ if (needsSkipping)
+ {
+ long targetRowID = primaryKeyMap.rowIdFromPrimaryKey(skipToToken);
+ // skipToToken is larger than max token in token file
+ if (targetRowID < 0)
+ {
+ return PostingList.END_OF_STREAM;
+ }
+
+ segmentRowId = postingList.advance(targetRowID - searcherContext.segmentRowIdOffset);
+
+ needsSkipping = false;
+ }
+ else
+ {
+ segmentRowId = postingList.nextPosting();
+ }
+
+ return segmentRowId != PostingList.END_OF_STREAM
+ ? segmentRowId + searcherContext.segmentRowIdOffset
+ : PostingList.END_OF_STREAM;
+ }
+}
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
new file mode 100644
index 0000000000..de82ebd01c
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsReader.java
@@ -0,0 +1,366 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1.postings;
+
+
+import java.io.IOException;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.index.sai.postings.OrdinalPostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.LongArray;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.RandomAccessInput;
+
+
+/**
+ * Reads, decompresses and decodes postings lists written by {@link PostingsWriter}.
+ *
+ * Holds exactly one posting block in memory at a time. Does binary search over skip table to find a postings block to
+ * load.
+ */
+@NotThreadSafe
+public class PostingsReader implements OrdinalPostingList
+{
+ private final IndexInput input;
+ private final SeekingRandomAccessInput seekingInput;
+ private final QueryEventListener.PostingListEventListener listener;
+ private final BlocksSummary summary;
+
+ // Current block index
+ private int blockIndex;
+ // Current posting index within block
+ private int postingIndex;
+ private long totalPostingsRead;
+ private long actualPosting;
+
+ private long currentPosition;
+ private DirectReaders.Reader currentFoRValues;
+ private long postingsDecoded = 0;
+
+ @VisibleForTesting
+ public PostingsReader(IndexInput input, long summaryOffset, QueryEventListener.PostingListEventListener listener) throws IOException
+ {
+ this(input, new BlocksSummary(input, summaryOffset), listener);
+ }
+
+ public PostingsReader(IndexInput input, BlocksSummary summary, QueryEventListener.PostingListEventListener listener) throws IOException
+ {
+ this.input = input;
+ this.seekingInput = new SeekingRandomAccessInput(input);
+ this.listener = listener;
+ this.summary = summary;
+
+ reBuffer();
+ }
+
+ @Override
+ public long getOrdinal()
+ {
+ return totalPostingsRead;
+ }
+
+ public static class BlocksSummary
+ {
+ private final IndexInput input;
+ final int blockSize;
+ final int numPostings;
+ final LongArray offsets;
+ final LongArray maxValues;
+
+ public BlocksSummary(IndexInput input, long offset) throws IOException
+ {
+ this.input = input;
+ input.seek(offset);
+ this.blockSize = input.readVInt();
+ //TODO This should need to change because we can potentially end up with postings of more than Integer.MAX_VALUE?
+ this.numPostings = input.readVInt();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(input);
+ int numBlocks = input.readVInt();
+ long maxBlockValuesLength = input.readVLong();
+ long maxBlockValuesOffset = input.getFilePointer() + maxBlockValuesLength;
+
+ byte offsetBitsPerValue = input.readByte();
+ if (!DirectReaders.SUPPORTED_BITS_PER_VALUE.contains((int)offsetBitsPerValue))
+ {
+ throw new CorruptIndexException(
+ String.format("Postings list header is corrupted: Bits per value for block offsets is %s. Supported values are %s.",
+ offsetBitsPerValue, DirectReaders.SUPPORTED_BITS_PER_VALUE_STRING), input);
+ }
+ this.offsets = new LongArrayReader(randomAccessInput, DirectReaders.getReaderForBitsPerValue(offsetBitsPerValue), input.getFilePointer(), numBlocks);
+
+ input.seek(maxBlockValuesOffset);
+ byte valuesBitsPerValue = input.readByte();
+ if (!DirectReaders.SUPPORTED_BITS_PER_VALUE.contains((int)valuesBitsPerValue))
+ {
+ throw new CorruptIndexException(
+ String.format("Postings list header is corrupted: Bits per value for value samples is %s. Supported values are %s.",
+ valuesBitsPerValue, DirectReaders.SUPPORTED_BITS_PER_VALUE_STRING), input);
+ }
+ this.maxValues = new LongArrayReader(randomAccessInput, DirectReaders.getReaderForBitsPerValue(valuesBitsPerValue), input.getFilePointer(), numBlocks);
+ }
+
+ void close()
+ {
+ FileUtils.closeQuietly(input);
+ }
+
+ private static class LongArrayReader implements LongArray
+ {
+ private final RandomAccessInput input;
+ private final DirectReaders.Reader reader;
+ private final long offset;
+ private final int length;
+
+ private LongArrayReader(RandomAccessInput input, DirectReaders.Reader reader, long offset, int length)
+ {
+ this.input = input;
+ this.reader = reader;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ @Override
+ public long get(long idx)
+ {
+ return reader.get(input, offset, idx);
+ }
+
+ @Override
+ public long length()
+ {
+ return length;
+ }
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ listener.postingDecoded(postingsDecoded);
+ FileUtils.closeQuietly(input);
+ summary.close();
+ }
+
+ @Override
+ public long size()
+ {
+ return summary.numPostings;
+ }
+
+ /**
+ * Advances to the first row ID beyond the current that is greater than or equal to the
+ * target, and returns that row ID. Exhausts the iterator and returns {@link #END_OF_STREAM} if
+ * the target is greater than the highest row ID.
+ *
+ * Does binary search over the skip table to find the next block to load into memory.
+ *
+ * Note: Callers must use the return value of this method before calling {@link #nextPosting()}, as calling
+ * that method will return the next posting, not the one to which we have just advanced.
+ *
+ * @param targetRowID target row ID to advance to
+ *
+ * @return first segment row ID which is >= the target row ID or {@link PostingList#END_OF_STREAM} if one does not exist
+ */
+ @Override
+ public long advance(long targetRowID) throws IOException
+ {
+ listener.onAdvance();
+ int block = binarySearchBlocks(targetRowID);
+
+ if (block < 0)
+ {
+ block = -block - 1;
+ }
+
+ if (blockIndex == block + 1)
+ {
+ // we're in the same block, just iterate through
+ return slowAdvance(targetRowID);
+ }
+ assert block > 0;
+ // Even if there was an exact match, block might contain duplicates.
+ // We iterate to the target token from the beginning.
+ lastPosInBlock(block - 1);
+ return slowAdvance(targetRowID);
+ }
+
+ private long slowAdvance(long targetRowID) throws IOException
+ {
+ while (totalPostingsRead < summary.numPostings)
+ {
+ long segmentRowId = peekNext();
+
+ advanceOnePosition(segmentRowId);
+
+ if (segmentRowId >= targetRowID)
+ {
+ return segmentRowId;
+ }
+ }
+ return END_OF_STREAM;
+ }
+
+ // Perform a binary search of the blocks to the find the block index
+ // containing the targetRowID, or, in the case of a duplicate value
+ // crossing blocks, the preceeding block index
+ private int binarySearchBlocks(long targetRowID)
+ {
+ int lowBlockIndex = blockIndex - 1;
+ int highBlockIndex = Math.toIntExact(summary.maxValues.length()) - 1;
+
+ // in current block
+ if (lowBlockIndex <= highBlockIndex && targetRowID <= summary.maxValues.get(lowBlockIndex))
+ return lowBlockIndex;
+
+ while (lowBlockIndex <= highBlockIndex)
+ {
+ int midBlockIndex = lowBlockIndex + ((highBlockIndex - lowBlockIndex) >> 1) ;
+
+ long maxValueOfMidBlock = summary.maxValues.get(midBlockIndex);
+
+ if (maxValueOfMidBlock < targetRowID)
+ {
+ lowBlockIndex = midBlockIndex + 1;
+ }
+ else if (maxValueOfMidBlock > targetRowID)
+ {
+ highBlockIndex = midBlockIndex - 1;
+ }
+ else
+ {
+ // At this point the maximum value of the midway block matches our target.
+ //
+ // This following check is to see if we have a duplicate value in the last entry of the
+ // preceeding block. This check is only going to be successful if the entire current
+ // block is full of duplicates.
+ if (midBlockIndex > 0 && summary.maxValues.get(midBlockIndex - 1) == targetRowID)
+ {
+ // there is a duplicate in the preceeding block so restrict search to finish
+ // at that block
+ highBlockIndex = midBlockIndex - 1;
+ }
+ else
+ {
+ // no duplicates
+ return midBlockIndex;
+ }
+ }
+ }
+ return -(lowBlockIndex + 1); // target not found
+ }
+
+ private void lastPosInBlock(int block)
+ {
+ // blockMaxValues is integer only
+ actualPosting = summary.maxValues.get(block);
+ //upper bound, since we might've advanced to the last block, but upper bound is enough
+ totalPostingsRead += (summary.blockSize - postingIndex) + (block - blockIndex + 1) * (long)summary.blockSize;
+
+ blockIndex = block + 1;
+ postingIndex = summary.blockSize;
+ }
+
+ @Override
+ public long nextPosting() throws IOException
+ {
+ long next = peekNext();
+ if (next != END_OF_STREAM)
+ {
+ advanceOnePosition(next);
+ }
+ return next;
+ }
+
+ private long peekNext() throws IOException
+ {
+ if (totalPostingsRead >= summary.numPostings)
+ {
+ return END_OF_STREAM;
+ }
+ if (postingIndex == summary.blockSize)
+ {
+ reBuffer();
+ }
+
+ return actualPosting + nextFoRValue();
+ }
+
+ private int nextFoRValue()
+ {
+ // currentFoRValues is null when the all the values in the block are the same
+ if (currentFoRValues == null)
+ {
+ return 0;
+ }
+ else
+ {
+ long id = currentFoRValues.get(seekingInput, currentPosition, postingIndex);
+ postingsDecoded++;
+ return Math.toIntExact(id);
+ }
+ }
+
+ private void advanceOnePosition(long nextPosting)
+ {
+ actualPosting = nextPosting;
+ totalPostingsRead++;
+ postingIndex++;
+ }
+
+ private void reBuffer() throws IOException
+ {
+ long pointer = summary.offsets.get(blockIndex);
+
+ input.seek(pointer);
+
+ long left = summary.numPostings - totalPostingsRead;
+ assert left > 0;
+
+ readFoRBlock(input);
+
+ blockIndex++;
+ postingIndex = 0;
+ }
+
+ private void readFoRBlock(IndexInput in) throws IOException
+ {
+ if (blockIndex == 0)
+ actualPosting = in.readVLong();
+
+ byte bitsPerValue = in.readByte();
+
+ currentPosition = in.getFilePointer();
+
+ if (bitsPerValue == 0)
+ {
+ // currentFORValues is null when the all the values in the block are the same
+ currentFoRValues = null;
+ return;
+ }
+ currentFoRValues = DirectReaders.getReaderForBitsPerValue(bitsPerValue);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java
new file mode 100644
index 0000000000..d441942fc4
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java
@@ -0,0 +1,316 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1.postings;
+
+
+import java.io.Closeable;
+import java.io.IOException;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.agrona.collections.LongArrayList;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.DirectWriter;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.Math.max;
+import static org.apache.lucene.codecs.lucene50.Lucene50PostingsFormat.BLOCK_SIZE;
+
+/**
+ * Encodes, compresses and writes postings lists to disk.
+ *
+ * All postings in the posting list are delta encoded, then deltas are divided into blocks for compression.
+ * The deltas are based on the final value of the previous block. For the first block in the posting list
+ * the first value in the block is written as a VLong prior to block delta encodings.
+ * <p>
+ * In packed blocks, longs are encoded with the same bit width (FoR compression). The block size (i.e. number of
+ * longs inside block) is fixed (currently 128). Additionally blocks that are all the same value are encoded in an
+ * optimized way.
+ * </p>
+ * <p>
+ * In VLong blocks, longs are compressed with {@link DataOutput#writeVLong}. The block size is variable.
+ * </p>
+ *
+ * <p>
+ * Packed blocks are favoured, meaning when the postings are long enough, {@link PostingsWriter} will try
+ * to encode most data as a packed block. Take a term with 259 postings as an example, the first 256 postings are encoded
+ * as two packed blocks, while the remaining 3 are encoded as one VLong block.
+ * </p>
+ * <p>
+ * Each posting list ends with a block summary containing metadata and a skip table, written right after all postings
+ * blocks. Skip interval is the same as block size, and each skip entry points to the end of each block.
+ * Skip table consist of block offsets and last values of each block, compressed as two FoR blocks.
+ * </p>
+ *
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+========================+=====+==============+===============+===============+=====+========================+========+
+ * | HEADER | POSTINGS LIST (TERM 1) | ... | POSTINGS LIST (TERM N) | FOOTER |
+ * +========+========================+=====+==============+===============+===============+=====+========================+========+
+ * | FIRST VALUE| FOR BLOCK (1)| ... | FOR BLOCK (N)| BLOCK SUMMARY |
+ * +---------------------------+-----+--------------+---------------+------------+
+ * | BLOCK SIZE | |
+ * | LIST SIZE | SKIP TABLE |
+ * +---------------+------------+
+ * | BLOCKS POS.|
+ * | MAX VALUES |
+ * +------------+
+ *
+ * </pre>
+ */
+@NotThreadSafe
+public class PostingsWriter implements Closeable
+{
+ private static final String POSTINGS_MUST_BE_SORTED_ERROR_MSG = "Postings must be sorted ascending, got [%s] after [%s]";
+
+ private final IndexOutput dataOutput;
+ private final int blockSize;
+ private final long[] deltaBuffer;
+ private final LongArrayList blockOffsets = new LongArrayList();
+ private final LongArrayList blockMaximumPostings = new LongArrayList();
+ private final RAMIndexOutput inMemoryOutput = new RAMIndexOutput("blockOffsets");
+
+ private final long startOffset;
+
+ private int bufferUpto;
+ private long firstPosting = Long.MIN_VALUE;
+ private long lastPosting = Long.MIN_VALUE;
+ private long maxDelta;
+ private long totalPostings;
+
+ public PostingsWriter(IndexDescriptor indexDescriptor, IndexContext indexContext) throws IOException
+ {
+ this(indexDescriptor, indexContext, BLOCK_SIZE);
+ }
+
+ @VisibleForTesting
+ PostingsWriter(IndexDescriptor indexDescriptor, IndexContext indexContext, int blockSize) throws IOException
+ {
+ this(indexDescriptor.openPerIndexOutput(IndexComponent.POSTING_LISTS, indexContext, true), blockSize);
+ }
+
+ private PostingsWriter(IndexOutput dataOutput, int blockSize) throws IOException
+ {
+ this.blockSize = blockSize;
+ this.dataOutput = dataOutput;
+ startOffset = dataOutput.getFilePointer();
+ deltaBuffer = new long[blockSize];
+ SAICodecUtils.writeHeader(dataOutput);
+ }
+
+ /**
+ * @return current file pointer
+ */
+ public long getFilePointer()
+ {
+ return dataOutput.getFilePointer();
+ }
+
+ /**
+ * @return file pointer where index structure begins
+ */
+ public long getStartOffset()
+ {
+ return startOffset;
+ }
+
+ /**
+ * write footer to the postings
+ */
+ public void complete() throws IOException
+ {
+ SAICodecUtils.writeFooter(dataOutput);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ dataOutput.close();
+ }
+
+ /**
+ * Encodes, compresses and flushes given posting list to disk.
+ *
+ * @param postings posting list to write to disk
+ *
+ * @return file offset to the summary block of this posting list
+ */
+ public long write(PostingList postings) throws IOException
+ {
+ checkArgument(postings != null, "Expected non-null posting list.");
+ checkArgument(postings.size() > 0, "Expected non-empty posting list.");
+
+ lastPosting = Long.MIN_VALUE;
+ resetBlockCounters();
+ blockOffsets.clear();
+ blockMaximumPostings.clear();
+
+ long posting;
+ // When postings list are merged, we don't know exact size, just an upper bound.
+ // We need to count how many postings we added to the block ourselves.
+ int size = 0;
+ while ((posting = postings.nextPosting()) != PostingList.END_OF_STREAM)
+ {
+ writePosting(posting);
+ size++;
+ totalPostings++;
+ }
+
+ assert size > 0 : "No postings were written";
+
+ finish();
+
+ final long summaryOffset = dataOutput.getFilePointer();
+ writeSummary(size);
+ return summaryOffset;
+ }
+
+ public long getTotalPostings()
+ {
+ return totalPostings;
+ }
+
+ private void writePosting(long posting) throws IOException
+ {
+ if (lastPosting == Long.MIN_VALUE)
+ {
+ firstPosting = posting;
+ deltaBuffer[bufferUpto++] = 0;
+ }
+ else
+ {
+ if (posting < lastPosting)
+ throw new IllegalArgumentException(String.format(POSTINGS_MUST_BE_SORTED_ERROR_MSG, posting, lastPosting));
+ long delta = posting - lastPosting;
+ maxDelta = max(maxDelta, delta);
+ deltaBuffer[bufferUpto++] = delta;
+ }
+ lastPosting = posting;
+
+ if (bufferUpto == blockSize)
+ {
+ addBlockToSkipTable();
+ writePostingsBlock();
+ resetBlockCounters();
+ }
+ }
+
+ private void finish() throws IOException
+ {
+ if (bufferUpto > 0)
+ {
+ addBlockToSkipTable();
+ writePostingsBlock();
+ }
+ }
+
+ private void resetBlockCounters()
+ {
+ firstPosting = Long.MIN_VALUE;
+ bufferUpto = 0;
+ maxDelta = 0;
+ }
+
+ private void addBlockToSkipTable()
+ {
+ blockOffsets.add(dataOutput.getFilePointer());
+ blockMaximumPostings.add(lastPosting);
+ }
+
+ private void writeSummary(int exactSize) throws IOException
+ {
+ dataOutput.writeVInt(blockSize);
+ dataOutput.writeVInt(exactSize);
+ writeSkipTable();
+ }
+
+ private void writeSkipTable() throws IOException
+ {
+ assert blockOffsets.size() == blockMaximumPostings.size();
+ dataOutput.writeVInt(blockOffsets.size());
+
+ // compressing offsets in memory first, to know the exact length (with padding)
+ inMemoryOutput.reset();
+
+ writeSortedFoRBlock(blockOffsets, inMemoryOutput);
+ dataOutput.writeVLong(inMemoryOutput.getFilePointer());
+ inMemoryOutput.writeTo(dataOutput);
+ writeSortedFoRBlock(blockMaximumPostings, dataOutput);
+ }
+
+ private void writePostingsBlock() throws IOException
+ {
+ final int bitsPerValue = maxDelta == 0 ? 0 : DirectWriter.unsignedBitsRequired(maxDelta);
+
+ assert DirectReaders.SUPPORTED_BITS_PER_VALUE.contains(bitsPerValue) :
+ "Unsupported bits per value of " + bitsPerValue + " bits. Supported values are: " + DirectReaders.SUPPORTED_BITS_PER_VALUE_STRING;
+
+ // If we have a first posting, indicating that this is the first block in the posting list
+ // then write it prior to the deltas.
+ if (firstPosting != Long.MIN_VALUE)
+ dataOutput.writeVLong(firstPosting);
+
+ dataOutput.writeByte((byte) bitsPerValue);
+ if (bitsPerValue > 0)
+ {
+ final DirectWriter writer = DirectWriter.getInstance(dataOutput, blockSize, bitsPerValue);
+ for (int index = 0; index < bufferUpto; ++index)
+ {
+ writer.add(deltaBuffer[index]);
+ }
+ if (bufferUpto < blockSize)
+ {
+ // Pad the rest of the block with 0, so we don't write invalid
+ // values from previous blocks
+ for (int index = bufferUpto; index < blockSize; index++)
+ {
+ writer.add(0);
+ }
+ }
+ writer.finish();
+ }
+ }
+
+ private void writeSortedFoRBlock(LongArrayList values, IndexOutput output) throws IOException
+ {
+ final long maxValue = values.getLong(values.size() - 1);
+
+ assert values.size() > 0;
+ final int bitsPerValue = maxValue == 0 ? 0 : DirectWriter.unsignedBitsRequired(maxValue);
+ output.writeByte((byte) bitsPerValue);
+ if (bitsPerValue > 0)
+ {
+ final DirectWriter writer = DirectWriter.getInstance(output, values.size(), bitsPerValue);
+ for (int i = 0; i < values.size(); ++i)
+ {
+ writer.add(values.getLong(i));
+ }
+ writer.finish();
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcher.java
new file mode 100644
index 0000000000..bb22ff492b
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcher.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1.segment;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingListRangeIterator;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+
+/**
+ * Abstract reader for individual segments of an on-disk index.
+ *
+ * Accepts shared resources (token/offset file readers), and uses them to perform lookups against on-disk data
+ * structures.
+ */
+public abstract class IndexSegmentSearcher implements Closeable
+{
+ final PrimaryKeyMap.Factory primaryKeyMapFactory;
+ final PerColumnIndexFiles indexFiles;
+ final SegmentMetadata metadata;
+ final IndexContext indexContext;
+
+ IndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory,
+ PerColumnIndexFiles perIndexFiles,
+ SegmentMetadata segmentMetadata,
+ IndexContext indexContext)
+ {
+ this.primaryKeyMapFactory = primaryKeyMapFactory;
+ this.indexFiles = perIndexFiles;
+ this.metadata = segmentMetadata;
+ this.indexContext = indexContext;
+ }
+
+ public static IndexSegmentSearcher open(PrimaryKeyMap.Factory primaryKeyMapFactory,
+ PerColumnIndexFiles indexFiles,
+ SegmentMetadata segmentMetadata,
+ IndexContext indexContext) throws IOException
+ {
+ return new LiteralIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, indexContext);
+ }
+
+ /**
+ * @return memory usage of underlying on-disk data structure
+ */
+ public abstract long indexFileCacheSize();
+
+ /**
+ * Search on-disk index synchronously.
+ *
+ * @param expression to filter on disk index
+ * @param queryContext to track per sstable cache and per query metrics
+ *
+ * @return {@link KeyRangeIterator} with matches for the given expression
+ */
+ public abstract KeyRangeIterator search(Expression expression, QueryContext queryContext) throws IOException;
+
+ KeyRangeIterator toIterator(PostingList postingList, QueryContext queryContext) throws IOException
+ {
+ if (postingList == null)
+ return KeyRangeIterator.empty();
+
+ IndexSegmentSearcherContext searcherContext = new IndexSegmentSearcherContext(metadata.minKey,
+ metadata.maxKey,
+ metadata.rowIdOffset,
+ queryContext,
+ PeekablePostingList.makePeekable(postingList));
+
+ return new PostingListRangeIterator(indexContext, primaryKeyMapFactory.newPerSSTablePrimaryKeyMap(), searcherContext);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcherContext.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcherContext.java
new file mode 100644
index 0000000000..5b3c59b0ec
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/IndexSegmentSearcherContext.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1.segment;
+
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+
+public class IndexSegmentSearcherContext
+{
+ public final QueryContext context;
+ public final PeekablePostingList postingList;
+
+ public final PrimaryKey minimumKey;
+ public final PrimaryKey maximumKey;
+ public final long segmentRowIdOffset;
+
+ public IndexSegmentSearcherContext(PrimaryKey minimumKey,
+ PrimaryKey maximumKey,
+ long segmentRowIdOffset,
+ QueryContext context,
+ PeekablePostingList postingList)
+ {
+ this.context = context;
+ this.postingList = postingList;
+
+ this.segmentRowIdOffset = segmentRowIdOffset;
+
+ this.minimumKey = minimumKey;
+
+ // use segment's metadata for the range iterator, may not be accurate, but should not matter to performance.
+ this.maximumKey = maximumKey;
+ }
+
+ public long count()
+ {
+ return postingList.size();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentSearcher.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentSearcher.java
new file mode 100644
index 0000000000..fb45943934
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentSearcher.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.sai.disk.v1.segment;
+
+import java.io.IOException;
+import java.util.Map;
+
+import com.google.common.base.MoreObjects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+
+/**
+ * Executes {@link Expression}s against the trie-based terms dictionary for an individual index segment.
+ */
+public class LiteralIndexSegmentSearcher extends IndexSegmentSearcher
+{
+ private static final Logger logger = LoggerFactory.getLogger(LiteralIndexSegmentSearcher.class);
+
+ private final LiteralIndexSegmentTermsReader reader;
+ private final QueryEventListener.TrieIndexEventListener perColumnEventListener;
+
+ LiteralIndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory,
+ PerColumnIndexFiles perIndexFiles,
+ SegmentMetadata segmentMetadata,
+ IndexContext indexContext) throws IOException
+ {
+ super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, indexContext);
+
+ long root = metadata.getIndexRoot(IndexComponent.TERMS_DATA);
+ assert root >= 0;
+
+ perColumnEventListener = (QueryEventListener.TrieIndexEventListener)indexContext.getColumnQueryMetrics();
+
+ Map<String,String> map = metadata.componentMetadatas.get(IndexComponent.TERMS_DATA).attributes;
+ String footerPointerString = map.get(SAICodecUtils.FOOTER_POINTER);
+ long footerPointer = footerPointerString == null ? -1 : Long.parseLong(footerPointerString);
+
+ reader = new LiteralIndexSegmentTermsReader(indexContext,
+ indexFiles.termsData(),
+ indexFiles.postingLists(),
+ root, footerPointer);
+ }
+
+ @Override
+ public long indexFileCacheSize()
+ {
+ // trie has no pre-allocated memory.
+ return 0;
+ }
+
+ @Override
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public KeyRangeIterator search(Expression expression, QueryContext queryContext) throws IOException
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("Searching on expression '{}'..."), expression);
+
+ if (!expression.getOp().isEquality())
+ throw new IllegalArgumentException(indexContext.logMessage("Unsupported expression: " + expression));
+
+ final ByteComparable term = ByteComparable.fixedLength(expression.lower.value.encoded);
+ QueryEventListener.TrieIndexEventListener listener = MulticastQueryEventListeners.of(queryContext, perColumnEventListener);
+ PostingList postingList = reader.exactMatch(term, listener, queryContext);
+ return toIterator(postingList, queryContext);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("indexContext", indexContext)
+ .toString();
+ }
+
+ @Override
+ public void close()
+ {
+ reader.close();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentTermsReader.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentTermsReader.java
new file mode 100644
index 0000000000..abca7c95cb
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/LiteralIndexSegmentTermsReader.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.segment;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+import org.apache.lucene.store.IndexInput;
+
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.validate;
+
+/**
+ * Synchronous reader of terms dictionary and postings lists to produce a {@link PostingList} with matching row ids.
+ *
+ * {@link #exactMatch(ByteComparable, QueryEventListener.TrieIndexEventListener, QueryContext)} does:
+ * <ul>
+ * <li>{@link TermQuery#lookupPostingsOffset(ByteComparable)}: does term dictionary lookup to find the posting list file
+ * position</li>
+ * <li>{@link TermQuery#getPostingsReader(long)}: reads posting list block summary and initializes posting read which
+ * reads the first block of the posting list into memory</li>
+ * </ul>
+ */
+public class LiteralIndexSegmentTermsReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(LiteralIndexSegmentTermsReader.class);
+
+ private final IndexContext indexContext;
+ private final FileHandle termDictionaryFile;
+ private final FileHandle postingsFile;
+ private final long termDictionaryRoot;
+
+ public LiteralIndexSegmentTermsReader(IndexContext indexContext,
+ FileHandle termsData,
+ FileHandle postingLists,
+ long root,
+ long termsFooterPointer) throws IOException
+ {
+ this.indexContext = indexContext;
+ termDictionaryFile = termsData;
+ postingsFile = postingLists;
+ termDictionaryRoot = root;
+
+ try (final IndexInput indexInput = IndexFileUtils.instance.openInput(termDictionaryFile))
+ {
+ validate(indexInput, termsFooterPointer);
+ }
+
+ try (final IndexInput indexInput = IndexFileUtils.instance.openInput(postingsFile))
+ {
+ validate(indexInput);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(termDictionaryFile);
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ public PostingList exactMatch(ByteComparable term, QueryEventListener.TrieIndexEventListener perQueryEventListener, QueryContext context)
+ {
+ perQueryEventListener.onSegmentHit();
+ return new TermQuery(term, perQueryEventListener, context).execute();
+ }
+
+ @VisibleForTesting
+ public class TermQuery
+ {
+ private final IndexInput postingsInput;
+ private final IndexInput postingsSummaryInput;
+ private final QueryEventListener.TrieIndexEventListener listener;
+ private final long lookupStartTime;
+ private final QueryContext context;
+ private final ByteComparable term;
+
+ TermQuery(ByteComparable term, QueryEventListener.TrieIndexEventListener listener, QueryContext context)
+ {
+ this.listener = listener;
+ postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ this.term = term;
+ lookupStartTime = Clock.Global.nanoTime();
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ long postingOffset = lookupPostingsOffset(term);
+ if (postingOffset == PostingList.OFFSET_NOT_FOUND)
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+
+ context.checkpoint();
+
+ // when posting is found, resources will be closed when posting reader is closed.
+ return getPostingsReader(postingOffset);
+ }
+ catch (Throwable e)
+ {
+ if (!(e instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Failed to execute term query"), e);
+
+ closeOnException();
+ throw Throwables.cleaned(e);
+ }
+ }
+
+ private void closeOnException()
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ public long lookupPostingsOffset(ByteComparable term)
+ {
+ try (TrieTermsDictionaryReader reader = new TrieTermsDictionaryReader(termDictionaryFile.instantiateRebufferer(null), termDictionaryRoot))
+ {
+ final long offset = reader.exactMatch(term);
+
+ listener.onTraversalComplete(Clock.Global.nanoTime() - lookupStartTime, TimeUnit.NANOSECONDS);
+
+ if (offset == TrieTermsDictionaryReader.NOT_FOUND)
+ return PostingList.OFFSET_NOT_FOUND;
+
+ return offset;
+ }
+ }
+
+ public PostingsReader getPostingsReader(long offset) throws IOException
+ {
+ PostingsReader.BlocksSummary header = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+
+ return new PostingsReader(postingsInput, header, listener.postingListEventListener());
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/Segment.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/Segment.java
new file mode 100644
index 0000000000..a36a3504dc
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/Segment.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.v1.segment;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.SSTableContext;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.io.util.FileUtils;
+
+/**
+ * Each segment represents an on-disk index structure (kdtree/terms/postings) flushed by memory limit or token boundaries.
+ * It also helps to reduce resource consumption for read requests as only segments that intersect with read request data
+ * range need to be loaded.
+ */
+public class Segment implements Closeable
+{
+ private final Token minKey;
+ private final Token.KeyBound minKeyBound;
+ private final Token maxKey;
+ private final Token.KeyBound maxKeyBound;
+
+ // per sstable
+ final PrimaryKeyMap.Factory primaryKeyMapFactory;
+ // per-segment
+ public final SegmentMetadata metadata;
+
+ private final IndexSegmentSearcher index;
+
+ public Segment(IndexContext indexContext, SSTableContext sstableContext, PerColumnIndexFiles indexFiles, SegmentMetadata metadata) throws IOException
+ {
+ this.minKey = metadata.minKey.token();
+ this.minKeyBound = minKey.minKeyBound();
+ this.maxKey = metadata.maxKey.token();
+ this.maxKeyBound = maxKey.maxKeyBound();
+
+ this.primaryKeyMapFactory = sstableContext.primaryKeyMapFactory;
+ this.metadata = metadata;
+
+ this.index = IndexSegmentSearcher.open(primaryKeyMapFactory, indexFiles, metadata, indexContext);
+ }
+
+ @VisibleForTesting
+ public Segment(Token minKey, Token maxKey)
+ {
+ this.primaryKeyMapFactory = null;
+ this.metadata = null;
+ this.minKey = minKey;
+ this.minKeyBound = minKey.minKeyBound();
+ this.maxKey = maxKey;
+ this.maxKeyBound = maxKey.maxKeyBound();
+ this.index = null;
+ }
+
+ /**
+ * @return true if current segment intersects with query key range
+ */
+ public boolean intersects(AbstractBounds<PartitionPosition> keyRange)
+ {
+ if (keyRange instanceof Range && ((Range<?>)keyRange).isWrapAround())
+ return keyRange.contains(minKeyBound) || keyRange.contains(maxKeyBound);
+
+ int cmp = keyRange.right.getToken().compareTo(minKey);
+ // if right is minimum, it means right is the max token and bigger than maxKey.
+ // if right bound is less than minKey, no intersection
+ if (!keyRange.right.isMinimum() && (!keyRange.inclusiveRight() && cmp == 0 || cmp < 0))
+ return false;
+
+ // if left bound is bigger than maxKey, no intersection
+ return keyRange.isStartInclusive() || keyRange.left.getToken().compareTo(maxKey) < 0;
+ }
+
+ public long indexFileCacheSize()
+ {
+ return index == null ? 0 : index.indexFileCacheSize();
+ }
+
+ /**
+ * Search on-disk index synchronously
+ *
+ * @param expression to filter on disk index
+ * @param context to track per sstable cache and per query metrics
+
+ * @return range iterator that matches given expression
+ */
+ public KeyRangeIterator search(Expression expression, QueryContext context) throws IOException
+ {
+ return index.search(expression, context);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(index);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Segment{metadata=%s}", metadata);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java
new file mode 100644
index 0000000000..dd8666213d
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.index.sai.disk.v1.segment;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.memory.RAMStringIndexer;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.trie.LiteralIndexWriter;
+import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.utils.FastByteOperations;
+import org.apache.lucene.util.BytesRefBuilder;
+
+/**
+ * Creates an on-heap index data structure to be flushed to an SSTable index.
+ */
+@NotThreadSafe
+public abstract class SegmentBuilder
+{
+ private static final Logger logger = LoggerFactory.getLogger(SegmentBuilder.class);
+
+ // Served as safe net in case memory limit is not triggered or when merger merges small segments..
+ public static final long LAST_VALID_SEGMENT_ROW_ID = (Integer.MAX_VALUE / 2) - 1L;
+ private static long testLastValidSegmentRowId = -1;
+
+ /** The number of column indexes being built globally. (Starts at one to avoid divide by zero.) */
+ private static final AtomicInteger ACTIVE_BUILDER_COUNT = new AtomicInteger(0);
+
+ /** Minimum flush size, dynamically updated as segment builds are started and completed/aborted. */
+ private static volatile long minimumFlushBytes;
+ private final AbstractType<?> termComparator;
+ private final NamedMemoryLimiter limiter;
+ private final long lastValidSegmentRowID;
+ private boolean flushed = false;
+ private boolean active = true;
+ // segment metadata
+ private long minSSTableRowId = -1;
+ private long maxSSTableRowId = -1;
+ private long segmentRowIdOffset = 0;
+
+ // in token order
+ private PrimaryKey minKey;
+ private PrimaryKey maxKey;
+ // in termComparator order
+ private ByteBuffer minTerm;
+ private ByteBuffer maxTerm;
+
+ long totalBytesAllocated;
+ int rowCount = 0;
+ int maxSegmentRowId = -1;
+
+ public static class RAMStringSegmentBuilder extends SegmentBuilder
+ {
+ final RAMStringIndexer ramIndexer;
+
+ final BytesRefBuilder stringBuffer = new BytesRefBuilder();
+
+ public RAMStringSegmentBuilder(AbstractType<?> termComparator, NamedMemoryLimiter limiter)
+ {
+ super(termComparator, limiter);
+
+ ramIndexer = new RAMStringIndexer();
+ totalBytesAllocated = ramIndexer.estimatedBytesUsed();
+ }
+
+ public boolean isEmpty()
+ {
+ return ramIndexer.isEmpty();
+ }
+
+ protected long addInternal(ByteBuffer term, int segmentRowId)
+ {
+ copyBufferToBytesRef(term, stringBuffer);
+ return ramIndexer.add(stringBuffer.get(), segmentRowId);
+ }
+
+ @Override
+ protected SegmentMetadata.ComponentMetadataMap flushInternal(IndexDescriptor indexDescriptor, IndexContext indexContext) throws IOException
+ {
+ try (LiteralIndexWriter writer = new LiteralIndexWriter(indexDescriptor, indexContext))
+ {
+ return writer.writeCompleteSegment(ramIndexer.getTermsWithPostings());
+ }
+ }
+
+ private void copyBufferToBytesRef(ByteBuffer buffer, BytesRefBuilder stringBuffer)
+ {
+ int length = buffer.remaining();
+ stringBuffer.clear();
+ stringBuffer.grow(length);
+ FastByteOperations.copy(buffer, buffer.position(), stringBuffer.bytes(), 0, length);
+ stringBuffer.setLength(length);
+ }
+ }
+
+ public static int getActiveBuilderCount()
+ {
+ return ACTIVE_BUILDER_COUNT.get();
+ }
+
+ private SegmentBuilder(AbstractType<?> termComparator, NamedMemoryLimiter limiter)
+ {
+ this.termComparator = termComparator;
+ this.limiter = limiter;
+ this.lastValidSegmentRowID = testLastValidSegmentRowId >= 0 ? testLastValidSegmentRowId : LAST_VALID_SEGMENT_ROW_ID;
+
+ minimumFlushBytes = limiter.limitBytes() / ACTIVE_BUILDER_COUNT.incrementAndGet();
+ }
+
+ public SegmentMetadata flush(IndexDescriptor indexDescriptor, IndexContext indexContext) throws IOException
+ {
+ assert !flushed : "Cannot flush an already flushed segment";
+ flushed = true;
+
+ if (getRowCount() == 0)
+ {
+ logger.warn(indexContext.logMessage("No rows to index during flush of SSTable {}."), indexDescriptor.sstableDescriptor);
+ return null;
+ }
+
+ SegmentMetadata.ComponentMetadataMap indexMetas = flushInternal(indexDescriptor, indexContext);
+
+ return new SegmentMetadata(segmentRowIdOffset, rowCount, minSSTableRowId, maxSSTableRowId, minKey, maxKey, minTerm, maxTerm, indexMetas);
+ }
+
+ public long add(ByteBuffer term, PrimaryKey key, long sstableRowId)
+ {
+ assert !flushed : "Cannot add to a flushed segment.";
+ assert sstableRowId >= maxSSTableRowId;
+ minSSTableRowId = minSSTableRowId < 0 ? sstableRowId : minSSTableRowId;
+ maxSSTableRowId = sstableRowId;
+
+ assert maxKey == null || maxKey.compareTo(key) <= 0;
+ if (minKey == null)
+ minKey = key;
+ maxKey = key;
+
+ minTerm = TypeUtil.min(term, minTerm, termComparator);
+ maxTerm = TypeUtil.max(term, maxTerm, termComparator);
+
+ if (rowCount == 0)
+ {
+ // use first global rowId in the segment as segment rowId offset
+ segmentRowIdOffset = sstableRowId;
+ }
+
+ rowCount++;
+
+ // segmentRowIdOffset should encode sstableRowId into Integer
+ int segmentRowId = castToSegmentRowId(sstableRowId, segmentRowIdOffset);
+ maxSegmentRowId = Math.max(maxSegmentRowId, segmentRowId);
+
+ long bytesAllocated = addInternal(term, segmentRowId);
+ totalBytesAllocated += bytesAllocated;
... 18343 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org