diff --git a/.build/build-resolver.xml b/.build/build-resolver.xml
index c9a47c4f96..29031b33a1 100644
--- a/.build/build-resolver.xml
+++ b/.build/build-resolver.xml
@@ -253,10 +253,6 @@
-
-
-
-
diff --git a/lib/harry-core-0.0.2-CASSANDRA-18768.jar b/lib/harry-core-0.0.2-CASSANDRA-18768.jar
deleted file mode 100644
index 292db01f06..0000000000
Binary files a/lib/harry-core-0.0.2-CASSANDRA-18768.jar and /dev/null differ
diff --git a/modules/accord b/modules/accord
index 3562bb3c9c..f78d1da27b 160000
--- a/modules/accord
+++ b/modules/accord
@@ -1 +1 @@
-Subproject commit 3562bb3c9ce4e9eecdf65e236e968ef3ee9e0a86
+Subproject commit f78d1da27b09f89417dd29bde0529f12cd744e3d
diff --git a/src/java/org/apache/cassandra/config/AccordSpec.java b/src/java/org/apache/cassandra/config/AccordSpec.java
index ab80ec4b32..d6fb1a5011 100644
--- a/src/java/org/apache/cassandra/config/AccordSpec.java
+++ b/src/java/org/apache/cassandra/config/AccordSpec.java
@@ -70,4 +70,5 @@ public class AccordSpec
*/
public TransactionalMode default_transactional_mode = TransactionalMode.off;
public boolean ephemeralReadEnabled = false;
+ public boolean state_cache_listener_jfr_enabled = true;
}
diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
index c0c739ec82..1e30b74437 100644
--- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
+++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
@@ -212,6 +212,7 @@ public enum CassandraRelevantProperties
*/
DRAIN_EXECUTOR_TIMEOUT_MS("cassandra.drain_executor_timeout_ms", convertToString(TimeUnit.MINUTES.toMillis(5))),
DROP_OVERSIZED_READ_REPAIR_MUTATIONS("cassandra.drop_oversized_readrepair_mutations"),
+ DTEST_ACCORD_ENABLED("jvm_dtest.accord.enabled", "true"),
DTEST_API_LOG_TOPOLOGY("cassandra.dtest.api.log.topology"),
/** This property indicates if the code is running under the in-jvm dtest framework */
DTEST_IS_IN_JVM_DTEST("org.apache.cassandra.dtest.is_in_jvm_dtest"),
@@ -572,7 +573,7 @@ public enum CassandraRelevantProperties
TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA("cassandra.unsafe_boot_with_clustermetadata", null),
TCM_USE_ATOMIC_LONG_PROCESSOR("cassandra.test.use_atomic_long_processor", "false"),
TCM_USE_NO_OP_REPLICATOR("cassandra.test.use_no_op_replicator", "false"),
-
+ TEST_ACCORD_STORE_THREAD_CHECKS_ENABLED("cassandra.test.accord.store.thread_checks_enabled", "true"),
TEST_BBFAILHELPER_ENABLED("test.bbfailhelper.enabled"),
TEST_BLOB_SHARED_SEED("cassandra.test.blob.shared.seed", "42"),
TEST_BYTEMAN_TRANSFORMATIONS_DEBUG("cassandra.test.byteman.transformations.debug"),
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index e32496a172..b026ad5286 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -5370,6 +5370,11 @@ public class DatabaseDescriptor
conf.accord.shard_durability_cycle = new DurationSpec.IntSecondsBound(seconds);
}
+ public static boolean getAccordStateCacheListenerJFREnabled()
+ {
+ return conf.accord.state_cache_listener_jfr_enabled;
+ }
+
public static boolean getForceNewPreparedStatementBehaviour()
{
return conf.force_new_prepared_statement_behaviour;
diff --git a/src/java/org/apache/cassandra/index/accord/CheckpointIntervalArrayIndex.java b/src/java/org/apache/cassandra/index/accord/CheckpointIntervalArrayIndex.java
new file mode 100644
index 0000000000..411c3fb5ec
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/CheckpointIntervalArrayIndex.java
@@ -0,0 +1,721 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.zip.CRC32C;
+import java.util.zip.Checksum;
+
+import accord.utils.AsymmetricComparator;
+import accord.utils.CheckpointIntervalArray;
+import accord.utils.CheckpointIntervalArrayBuilder;
+import accord.utils.CheckpointIntervalArrayBuilder.Accessor;
+import accord.utils.SortedArrays;
+import org.apache.cassandra.index.accord.IndexDescriptor.IndexComponent;
+import org.apache.cassandra.io.util.ChecksumedRandomAccessReader;
+import org.apache.cassandra.io.util.ChecksumedSequentialWriter;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.Throwables;
+
+import static accord.utils.CheckpointIntervalArrayBuilder.Links.LINKS;
+import static accord.utils.CheckpointIntervalArrayBuilder.Strategy.ACCURATE;
+
+//TODO (now): Add support for variable length tokens; this is needed for Ordered partitioner (which we plan to support)
+public class CheckpointIntervalArrayIndex
+{
+ private static final Accessor LIST_INTERVAL_ACCESSOR = new Accessor<>()
+ {
+ @Override
+ public int size(Interval[] intervals)
+ {
+ return intervals.length;
+ }
+
+ @Override
+ public Interval get(Interval[] intervals, int index)
+ {
+ return intervals[index];
+ }
+
+ @Override
+ public byte[] start(Interval[] intervals, int index)
+ {
+ return intervals[index].start;
+ }
+
+ @Override
+ public byte[] start(Interval interval)
+ {
+ return interval.start;
+ }
+
+ @Override
+ public byte[] end(Interval[] intervals, int index)
+ {
+ return intervals[index].end;
+ }
+
+ @Override
+ public byte[] end(Interval interval)
+ {
+ return interval.end;
+ }
+
+ @Override
+ public Comparator keyComparator()
+ {
+ return (a, b) -> ByteArrayUtil.compareUnsigned(a, 0, b, 0, a.length);
+ }
+
+ @Override
+ public int binarySearch(Interval[] intervals, int from, int to, byte[] find, AsymmetricComparator comparator, SortedArrays.Search op)
+ {
+ return SortedArrays.binarySearch(intervals, from, to, find, comparator, op);
+ }
+ };
+ public static final Supplier CHECKSUM_SUPPLIER = CRC32C::new;
+
+ //TODO (performance): rather than row structure, would column structure be better? Sorted tokens tend to have prefix relationships
+ // so could compress the data more. The negative here is the binary search might cost more and the scan after the random access needs end + value...
+ // Its also possible to do a hybrid structure where either start/end are column and the other one is row based...
+ //TODO (performance): store min/max values so could filter based off metadata without having to walk the tree first? This means that the metadata
+ // doesn't need to be stored in-memory 100% of the time and only when a file "could" match. The perf here would trade read costs for less memory.
+ //TODO (fault tolerence): right now there is no checksumming outside of the header, so a corruption in the middle
+ // could lead to weird behavior... since this structure is fixed lenght it "should" only lead to mismatches or binary
+ // search going the wrong direction...
+ //TODO (fault tolerence): maybe replace readStart/End with readRecord and extrat the value from there, this makes it so it would be trivial to add a checksum per-record.
+ // Given the migration from SAI work, we can now remove the TableId from the data (16 bytes) so a 4 byte footer wouldn't be a big cost. We also compute the checksum on read/write
+ // right now, just ignore the value... the performance is currently better than with SAI (less overhead as we are not generic), so the checksumming costs are effectivally 0.
+ public static class SortedListWriter
+ {
+ private final int bytesPerKey, bytesPerValue;
+
+ public SortedListWriter(int bytesPerKey, int bytesPerValue)
+ {
+ this.bytesPerKey = bytesPerKey;
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ public long write(ChecksumedSequentialWriter out, Interval[] sortedIntervals, Callback callback) throws IOException
+ {
+ long treeFilePointer = out.getFilePointer();
+ // write header
+ out.resetChecksum(); // reset checksum so the header is isolated
+ out.writeUnsignedVInt32(bytesPerKey);
+ out.writeUnsignedVInt32(bytesPerValue);
+ out.writeUnsignedVInt32(sortedIntervals.length);
+ out.writeInt(out.getValue32AndResetChecksum());
+
+ // write values
+ callback.preWalk(sortedIntervals);
+ int count = 0;
+ for (Interval it : sortedIntervals)
+ {
+ validate(count, it);
+
+ out.resetChecksum();
+ out.write(it.start, 0, it.start.length);
+ out.write(it.end, 0, it.end.length);
+ out.write(it.value, 0, it.value.length);
+ out.writeInt(out.getValue32());
+ callback.onWrite(count, it);
+ count++;
+ }
+ //TODO (now): don't need as this was here only for SAI. Offset/position are the same now
+ return count == 0 ? -1 : treeFilePointer;
+ }
+
+ private void validate(int numIntervals, Interval it)
+ {
+ if (it.start.length != bytesPerKey)
+ throw new IllegalArgumentException("Interval " + numIntervals + "'s start value is size " + it.start.length + ", but expected " + bytesPerKey);
+ if (it.end.length != bytesPerKey)
+ throw new IllegalArgumentException("Interval " + numIntervals + "'s end value is size " + it.end.length + ", but expected " + bytesPerKey);
+ if (it.value.length != bytesPerValue)
+ throw new IllegalArgumentException("Interval " + numIntervals + "'s value is size " + it.value.length + ", but expected " + bytesPerValue);
+ }
+
+ public interface Callback
+ {
+ default void preWalk(Interval[] sortedIntervals) throws IOException
+ {
+ }
+
+ default void onWrite(int index, Interval interval) throws IOException
+ {
+ }
+ }
+ }
+
+ public static class SortedListReader implements Closeable
+ {
+ private final FileHandle fh;
+ private final long firstRecordOffset;
+ private final int bytesPerKey, bytesPerValue, recordSize;
+ private final int count;
+
+ public SortedListReader(FileHandle fh, long pos)
+ {
+ this.fh = fh;
+
+ try (RandomAccessReader reader = fh.createReader();
+ ChecksumedRandomAccessReader in = new ChecksumedRandomAccessReader(reader, CHECKSUM_SUPPLIER))
+ {
+ if (pos != -1)
+ in.seek(pos);
+
+ bytesPerKey = in.readUnsignedVInt32();
+ bytesPerValue = in.readUnsignedVInt32();
+ recordSize = bytesPerKey * 2 + bytesPerValue + Integer.BYTES;
+ count = in.readUnsignedVInt32();
+ int actualChecksum = in.getValue32AndResetChecksum();
+ int expectedChecksum = in.readInt();
+ assert actualChecksum == expectedChecksum;
+ firstRecordOffset = reader.getFilePointer();
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(fh);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ FileUtils.closeQuietly(fh);
+ }
+
+ public enum SeekReason
+ {BINARY_SEARCH, GET, SCAN}
+
+ private boolean maybeSeek(ChecksumedRandomAccessReader indexInput, Stats stats, SeekReason reason, long target) throws IOException
+ {
+ if (indexInput.getFilePointer() != target)
+ {
+ indexInput.seek(target);
+ switch (reason)
+ {
+ case SCAN:
+ stats.seekForScan++;
+ break;
+ case GET:
+ stats.seekForGet++;
+ break;
+ case BINARY_SEARCH:
+ stats.seekForBinarySearch++;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown reason: " + reason);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public byte[] getRecord(ChecksumedRandomAccessReader indexInput, Stats stats, SeekReason reason, byte[] recordBuffer, int pos) throws IOException
+ {
+ maybeSeek(indexInput, stats, reason, fileOffsetStart(pos));
+ return getCurrentRecord(indexInput, stats, recordBuffer);
+ }
+
+ public byte[] getCurrentRecord(ChecksumedRandomAccessReader indexInput, Stats stats, byte[] recordBuffer) throws IOException
+ {
+ stats.bytesRead += recordBuffer.length + Integer.BYTES;
+ indexInput.resetChecksum();
+ indexInput.readFully(recordBuffer, 0, recordBuffer.length);
+ int actualChecksum = indexInput.getValue32();
+ int expectedChecksum = indexInput.readInt();
+ assert actualChecksum == expectedChecksum;
+ return recordBuffer;
+ }
+
+ public byte[] readStart(ChecksumedRandomAccessReader indexInput, Stats stats, byte[] recordBuffer, byte[] keyBuffer, int pos) throws IOException
+ {
+ getRecord(indexInput, stats, SeekReason.GET, recordBuffer, pos);
+ copyStart(recordBuffer, keyBuffer);
+ return keyBuffer;
+ }
+
+ public byte[] readEnd(ChecksumedRandomAccessReader indexInput, Stats stats, byte[] recordBuffer, byte[] keyBuffer, int pos) throws IOException
+ {
+ getRecord(indexInput, stats, SeekReason.GET, recordBuffer, pos);
+ copyEnd(recordBuffer, keyBuffer);
+ return keyBuffer;
+ }
+
+ public byte[] copyStart(byte[] recordBuffer, byte[] keyBuffer)
+ {
+ System.arraycopy(recordBuffer, 0, keyBuffer, 0, keyBuffer.length);
+ return keyBuffer;
+ }
+
+ public byte[] copyEnd(byte[] recordBuffer, byte[] keyBuffer)
+ {
+ System.arraycopy(recordBuffer, bytesPerKey, keyBuffer, 0, bytesPerKey);
+ return keyBuffer;
+ }
+
+ public int binarySearch(ChecksumedRandomAccessReader indexInput, Stats stats, byte[] recordBuffer, int from, int to, byte[] find, AsymmetricComparator comparator, SortedArrays.Search op) throws IOException
+ {
+ int found = -1;
+ while (from < to)
+ {
+ int i = (from + to) >>> 1;
+ int c = comparator.compare(find, getRecord(indexInput, stats, SeekReason.BINARY_SEARCH, recordBuffer, i));
+ if (c < 0)
+ {
+ to = i;
+ }
+ else if (c > 0)
+ {
+ from = i + 1;
+ }
+ else
+ {
+ switch (op)
+ {
+ default:
+ throw new IllegalStateException("Unknown search operation: " + op);
+ case FAST:
+ return i;
+
+ case CEIL:
+ to = found = i;
+ break;
+
+ case FLOOR:
+ found = i;
+ from = i + 1;
+ }
+ }
+ }
+ // return -(low + 1); // key not found.
+ return found >= 0 ? found : -1 - to;
+ }
+
+ public Interval copyTo(byte[] record, Interval buffer)
+ {
+ buffer.start = Arrays.copyOfRange(record, 0, bytesPerKey);
+ buffer.end = Arrays.copyOfRange(record, bytesPerKey, bytesPerKey * 2);
+ buffer.value = Arrays.copyOfRange(record, bytesPerKey * 2, record.length);
+ return buffer;
+ }
+
+ private long fileOffsetStart(int offset)
+ {
+ if (offset >= count)
+ throw new IndexOutOfBoundsException("Start is from (0, " + count + "]; attempted to access " + offset);
+ return firstRecordOffset + (offset * recordSize);
+ }
+
+ private long fileOffsetEnd(int offset)
+ {
+ if (offset >= count)
+ throw new IndexOutOfBoundsException("Start is from (0, " + count + "]; attempted to access " + offset);
+ return firstRecordOffset + (offset * recordSize) + bytesPerKey;
+ }
+
+ private long fileOffsetValue(int offset)
+ {
+ if (offset >= count)
+ throw new IndexOutOfBoundsException("Start is from (0, " + count + "]; attempted to access " + offset);
+ return firstRecordOffset + (offset * recordSize) + bytesPerKey * 2;
+ }
+ }
+
+ public static class CheckpointWriter implements SortedListWriter.Callback, Closeable
+ {
+ private final ChecksumedSequentialWriter out;
+ private final long offset;
+ private final long position; //TODO (now): don't need as this was here only for SAI. Offset/position are the same now
+ private long length = -1;
+
+ public CheckpointWriter(ChecksumedSequentialWriter out)
+ {
+ this.out = out;
+ this.offset = position = out.getFilePointer();
+ }
+
+ @Override
+ public void preWalk(Interval[] sortedIntervals) throws IOException
+ {
+ class Checkpoints
+ {
+ final int[] bounds, headers, lists;
+ final int maxScanAndCheckpointMatches;
+
+ Checkpoints(int[] bounds, int[] headers, int[] lists, int maxScanAndCheckpointMatches)
+ {
+ this.bounds = bounds;
+ this.headers = headers;
+ this.lists = lists;
+ this.maxScanAndCheckpointMatches = maxScanAndCheckpointMatches;
+ }
+ }
+ Checkpoints c = new CheckpointIntervalArrayBuilder<>(LIST_INTERVAL_ACCESSOR, sortedIntervals, ACCURATE, LINKS).build((ignore, bounds, headers, lists, max) -> new Checkpoints(bounds, headers, lists, max));
+ out.resetChecksum(); // reset checksum so it only covers this metadata
+ out.writeUnsignedVInt32(c.maxScanAndCheckpointMatches);
+ write(c.bounds);
+ write(c.headers);
+ write(c.lists);
+ out.writeInt(out.getValue32AndResetChecksum());
+ }
+
+ private void write(int[] array) throws IOException
+ {
+ out.writeUnsignedVInt32(array.length);
+ for (int i = 0; i < array.length; i++)
+ out.writeVInt32(array[i]);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ length = out.getFilePointer() - offset;
+ out.close();
+ }
+ }
+
+ //TODO (performance): the current format assumes random list access is cheap, which isn't true for a disk index.
+ // This format was chosen as a place holder for now so we don't drift from the in-memory logic; in the original paper
+ // a new sorted list is used for each checkpoint, which then makes the access a sequential scan rather than random access.
+ public static class CheckpointReader implements Closeable
+ {
+ private final FileHandle fh;
+ private final int[] bounds, headers, lists;
+ private final int maxScanAndCheckpointMatches;
+
+ public CheckpointReader(FileHandle fh, long pos)
+ {
+ this.fh = fh;
+ try (RandomAccessReader reader = fh.createReader();
+ ChecksumedRandomAccessReader input = new ChecksumedRandomAccessReader(reader, CHECKSUM_SUPPLIER))
+ {
+ if (pos != -1)
+ input.seek(pos);
+
+ input.resetChecksum(); // reset checksum so it only covers this metadata
+ maxScanAndCheckpointMatches = input.readUnsignedVInt32();
+ bounds = readArray(input);
+ headers = readArray(input);
+ lists = readArray(input);
+ int actualChecksum = input.getValue32AndResetChecksum();
+ int expectedChecksum = input.readInt();
+ assert actualChecksum == expectedChecksum;
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(fh);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ private static int[] readArray(ChecksumedRandomAccessReader input) throws IOException
+ {
+ int size = input.readUnsignedVInt32();
+ int[] array = new int[size];
+ for (int i = 0; i < size; i++)
+ array[i] = input.readVInt32();
+ return array;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ FileUtils.closeQuietly(fh);
+ }
+ }
+
+ public static class SegmentWriter
+ {
+ private final IndexDescriptor id;
+ private final SortedListWriter writer;
+
+ public SegmentWriter(IndexDescriptor id, int bytesPerKey, int bytesPerValue)
+ {
+ this.id = id;
+ this.writer = new SortedListWriter(bytesPerKey, bytesPerValue);
+ }
+
+ public EnumMap write(Interval[] sortedIntervals) throws IOException
+ {
+ EnumMap metas = new EnumMap<>(IndexComponent.class);
+ try (ChecksumedSequentialWriter treeOutput = ChecksumedSequentialWriter.open(id.fileFor(IndexComponent.CINTIA_SORTED_LIST), true, CHECKSUM_SUPPLIER);
+ CheckpointWriter checkpointWriter = new CheckpointWriter(ChecksumedSequentialWriter.open(id.fileFor(IndexComponent.CINTIA_CHECKPOINTS), true, CHECKSUM_SUPPLIER)))
+ {
+ // The SSTable component file is opened in append mode, so our offset is the current file pointer.
+ long sortedOffset = treeOutput.getFilePointer();
+ long sortedPosition = writer.write(treeOutput, sortedIntervals, checkpointWriter);
+
+ // If the treePosition is less than 0 then we didn't write any values out and the index is empty
+ if (sortedPosition < 0)
+ return metas;
+ //TODO (now): currently does SAI header so offset isn't correct here and need position
+ metas.put(IndexComponent.CINTIA_SORTED_LIST, new Segment.ComponentMetadata(sortedPosition, treeOutput.getFilePointer()));
+ metas.put(IndexComponent.CINTIA_CHECKPOINTS, new Segment.ComponentMetadata(checkpointWriter.position, checkpointWriter.out.getFilePointer()));
+ }
+ return metas;
+ }
+ }
+
+ public static class Stats
+ {
+ int seekForGet, seekForBinarySearch, seekForScan;
+ long durationNs, bytesRead, matches;
+
+ @Override
+ public String toString()
+ {
+ return "Stats{" +
+ "seeks={Get=" + seekForGet +
+ ", BinarySearch=" + seekForBinarySearch +
+ ", Scan=" + seekForScan +
+ "}, bytesRead=" + bytesRead +
+ ", matches=" + matches +
+ ", duration_micro=" + TimeUnit.NANOSECONDS.toMicros(durationNs) +
+ '}';
+ }
+ }
+
+ public static class SegmentSearcher implements Closeable
+ {
+ private final SortedListReader reader;
+ private final CheckpointReader checkpoints;
+
+ public SegmentSearcher(FileHandle sortedListFile,
+ long sortedListPosition,
+ FileHandle checkpointFile,
+ long checkpointPosition)
+ {
+ this.reader = new SortedListReader(sortedListFile, sortedListPosition);
+ this.checkpoints = new CheckpointReader(checkpointFile, checkpointPosition);
+ }
+
+ public Stats intersects(byte[] start, byte[] end, Consumer callback) throws IOException
+ {
+ byte[] keyBuffer = new byte[reader.bytesPerKey];
+ byte[] recordBuffer = new byte[reader.recordSize - Integer.BYTES];
+ Stats stats = new Stats();
+ long startNanos = Clock.Global.nanoTime();
+ try (ChecksumedRandomAccessReader indexInput = new ChecksumedRandomAccessReader(reader.fh.createReader(), CHECKSUM_SUPPLIER))
+ {
+ Interval buffer = new Interval();
+ Accessor accessor = new Accessor<>()
+ {
+ @Override
+ public int size(ChecksumedRandomAccessReader indexInput)
+ {
+ return reader.count;
+ }
+
+ @Override
+ public byte[] get(ChecksumedRandomAccessReader indexInput, int index)
+ {
+ try
+ {
+ return reader.getRecord(indexInput, stats, SortedListReader.SeekReason.GET, recordBuffer, index);
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public byte[] start(ChecksumedRandomAccessReader indexInput, int index)
+ {
+ try
+ {
+ return reader.readStart(indexInput, stats, recordBuffer, keyBuffer, index);
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public byte[] start(byte[] bytes)
+ {
+ return reader.copyStart(bytes, keyBuffer);
+ }
+
+ @Override
+ public byte[] end(ChecksumedRandomAccessReader indexInput, int index)
+ {
+ try
+ {
+ return reader.readEnd(indexInput, stats, recordBuffer, keyBuffer, index);
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public byte[] end(byte[] bytes)
+ {
+ return reader.copyEnd(bytes, keyBuffer);
+ }
+
+ @Override
+ public Comparator keyComparator()
+ {
+ return (a, b) -> ByteArrayUtil.compareUnsigned(a, 0, b, 0, a.length);
+ }
+
+ @Override
+ public int binarySearch(ChecksumedRandomAccessReader indexInput, int from, int to, byte[] find, AsymmetricComparator comparator, SortedArrays.Search op)
+ {
+ try
+ {
+ return reader.binarySearch(indexInput, stats, recordBuffer, from, to, find, comparator, op);
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ }
+ };
+ CheckpointIntervalArray searcher = new CheckpointIntervalArray<>(accessor, indexInput, checkpoints.bounds, checkpoints.headers, checkpoints.lists, checkpoints.maxScanAndCheckpointMatches);
+
+ searcher.forEach(start, end, (i1, i2, i3, i4, index) -> {
+ stats.matches++;
+ callback.accept(reader.copyTo(accessor.get(indexInput, index), buffer));
+ }, (i1, i2, i3, i4, startIdx, endIdx) -> {
+ try
+ {
+ reader.maybeSeek(indexInput, stats, SortedListReader.SeekReason.SCAN, reader.fileOffsetStart(startIdx));
+ for (int i = startIdx; i < endIdx; i++)
+ {
+ stats.matches++;
+ reader.getCurrentRecord(indexInput, stats, recordBuffer);
+ callback.accept(reader.copyTo(recordBuffer, buffer));
+ }
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+ }, 0, 0, 0, 0, 0);
+ }
+ finally
+ {
+ stats.durationNs = Clock.Global.nanoTime() - startNanos;
+ }
+ return stats;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(checkpoints);
+ FileUtils.closeQuietly(reader);
+ }
+ }
+
+ public static class Interval implements Comparable
+ {
+ public byte[] start, end, value; // mutable to avoid allocating Interval for every element
+
+ public Interval()
+ {
+ }
+
+ public Interval(byte[] start, byte[] end, byte[] value)
+ {
+ this.start = start;
+ this.end = end;
+ this.value = value;
+ }
+
+ public Interval(Interval other)
+ {
+ this.start = other.start;
+ this.end = other.end;
+ this.value = other.value;
+ }
+
+ @Override
+ public int compareTo(Interval b)
+ {
+ int rc = compareStart(b);
+ if (rc == 0)
+ rc = ByteArrayUtil.compareUnsigned(value, 0, b.value, 0, value.length);
+ return rc;
+ }
+
+ public int compareStart(Interval b)
+ {
+ int rc = ByteArrayUtil.compareUnsigned(start, 0, b.start, 0, start.length);
+ if (rc == 0)
+ rc = ByteArrayUtil.compareUnsigned(end, 0, b.end, 0, end.length);
+ return rc;
+ }
+
+ public int compareEnd(Interval b)
+ {
+ int rc = ByteArrayUtil.compareUnsigned(end, 0, b.end, 0, end.length);
+ if (rc == 0)
+ rc = ByteArrayUtil.compareUnsigned(start, 0, b.start, 0, start.length);
+ return rc;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Interval interval = (Interval) o;
+ return Arrays.equals(start, interval.start) && Arrays.equals(end, interval.end) && Arrays.equals(value, interval.value);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = Arrays.hashCode(start);
+ result = 31 * result + Arrays.hashCode(end);
+ result = 31 * result + Arrays.hashCode(value);
+ return result;
+ }
+
+ public boolean intersects(byte[] start, byte[] end)
+ {
+ if (ByteArrayUtil.compareUnsigned(this.start, end) >= 0)
+ return false;
+ if (ByteArrayUtil.compareUnsigned(this.end, start) <= 0)
+ return false;
+ return true;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/Group.java b/src/java/org/apache/cassandra/index/accord/Group.java
new file mode 100644
index 0000000000..e8e4d33294
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/Group.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.accord;
+
+import java.util.Objects;
+
+import org.apache.cassandra.schema.TableId;
+
+public class Group implements Comparable
+{
+ public final int storeId;
+ public final TableId tableId;
+
+ public Group(int storeId, TableId tableId)
+ {
+ this.storeId = storeId;
+ this.tableId = tableId;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Group group = (Group) o;
+ return storeId == group.storeId && Objects.equals(tableId, group.tableId);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(storeId, tableId);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Group{" +
+ "storeId=" + storeId +
+ ", tableId=" + tableId +
+ '}';
+ }
+
+ @Override
+ public int compareTo(Group o)
+ {
+ int rc = Integer.compare(storeId, o.storeId);
+ if (rc == 0)
+ rc = tableId.compareTo(o.tableId);
+ return rc;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/IndexDescriptor.java b/src/java/org/apache/cassandra/index/accord/IndexDescriptor.java
new file mode 100644
index 0000000000..b549751a3f
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/IndexDescriptor.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.accord;
+
+import java.util.Collection;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.dht.IPartitioner;
+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;
+
+public class IndexDescriptor
+{
+ public enum Version
+ {
+ v1("aa", c -> defaultFileNameFormat(c, "aa"));
+
+ public static final Version CURRENT = v1;
+
+ public final String versionString;
+ public final Function fileNameFormatter;
+
+ Version(String versionString, Function fileNameFormatter)
+ {
+ this.versionString = versionString;
+ this.fileNameFormatter = fileNameFormatter;
+ }
+
+
+ private static String defaultFileNameFormat(IndexComponent indexComponent, String version)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(IndexComponent.DESCRIPTOR).append(IndexComponent.SEPARATOR)
+ .append(version).append(IndexComponent.SEPARATOR)
+ .append(indexComponent.name).append(Descriptor.EXTENSION);
+
+ return sb.toString();
+ }
+ }
+
+ public enum IndexComponent
+ {
+ CINTIA_SORTED_LIST("CintiaSortedList", (byte) 1),
+ CINTIA_CHECKPOINTS("CintiaCheckpoints", (byte) 2),
+ SEGMENT("Segement", (byte) 3),
+ METADATA("Metadata", (byte) 4);
+
+ public static final String DESCRIPTOR = "ACCORD";
+ public static final String SEPARATOR = "+";
+
+ public final String name;
+ public final Component.Type type;
+ public final byte value;
+
+ IndexComponent(String name, byte value)
+ {
+ this.name = name;
+ this.type = componentType(name);
+ this.value = value;
+ }
+
+ private static Component.Type componentType(String name)
+ {
+ String componentName = DESCRIPTOR + SEPARATOR + name;
+ String repr = Pattern.quote(DESCRIPTOR + SEPARATOR)
+ + ".*"
+ + Pattern.quote(SEPARATOR + name + ".db");
+ return Component.Type.create(componentName, repr, true, null);
+ }
+
+ public static IndexComponent fromByte(byte b)
+ {
+ switch (b)
+ {
+ case 1: return CINTIA_SORTED_LIST;
+ case 2: return CINTIA_CHECKPOINTS;
+ case 3: return SEGMENT;
+ case 4: return METADATA;
+ default:throw new IllegalArgumentException("Unknow byte: " + b);
+ }
+ }
+ }
+
+ public final Version version;
+ public final Descriptor sstableDescriptor;
+ public final IPartitioner partitioner;
+ public final ClusteringComparator clusteringComparator;
+
+ public IndexDescriptor(Version version, Descriptor sstableDescriptor, IPartitioner partitioner, ClusteringComparator clusteringComparator)
+ {
+ this.version = version;
+ this.sstableDescriptor = sstableDescriptor;
+ this.partitioner = partitioner;
+ this.clusteringComparator = clusteringComparator;
+ }
+
+ public static IndexDescriptor create(SSTableReader sstable)
+ {
+ for (Version version : Version.values())
+ {
+ IndexDescriptor id = new IndexDescriptor(version, sstable.descriptor, sstable.getPartitioner(), sstable.metadata().comparator);
+ if (id.isIndexBuildComplete())
+ return id;
+ }
+ return new IndexDescriptor(Version.CURRENT, sstable.descriptor, sstable.getPartitioner(), sstable.metadata().comparator);
+ }
+
+ public static IndexDescriptor create(Descriptor descriptor, IPartitioner partitioner, ClusteringComparator comparator)
+ {
+ return new IndexDescriptor(Version.CURRENT, descriptor, partitioner, comparator);
+ }
+
+ public boolean isIndexBuildComplete()
+ {
+ return hasComponent(IndexComponent.METADATA);
+ }
+
+ public boolean hasComponent(IndexComponent indexComponent)
+ {
+ return fileFor(indexComponent).exists();
+ }
+
+ public File fileFor(IndexComponent indexComponent)
+ {
+ Component c = indexComponent.type.createComponent(version.fileNameFormatter.apply(indexComponent));
+ return sstableDescriptor.fileFor(c);
+ }
+
+ public void deleteIndex()
+ {
+ Stream.of(IndexComponent.values()).map(this::fileFor).forEach(File::deleteIfExists);
+ }
+
+ public Collection getLiveSSTableComponents()
+ {
+ return Stream.of(IndexComponent.values())
+ .map(c -> c.type.createComponent(version.fileNameFormatter.apply(c)))
+ .filter(c -> sstableDescriptor.fileFor(c).exists())
+ .collect(Collectors.toList());
+ }
+
+ public Collection getLiveComponents()
+ {
+ return Stream.of(IndexComponent.values())
+ .filter(c -> fileFor(c).exists())
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/IndexMetrics.java b/src/java/org/apache/cassandra/index/accord/IndexMetrics.java
new file mode 100644
index 0000000000..2956082235
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/IndexMetrics.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.accord;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.codahale.metrics.Timer;
+import org.apache.cassandra.metrics.CassandraMetricsRegistry;
+import org.apache.cassandra.metrics.DefaultNameFactory;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+// Stolen from org.apache.cassandra.index.sai.metrics.AbstractMetrics
+public class IndexMetrics
+{
+ private static final String TYPE = "RouteIndex";
+ private static final String SCOPE = "IndexMetrics";
+
+ private final List tracked = new ArrayList<>();
+
+ private final String ks;
+ private final String table;
+ private final String indexName;
+ public final Timer memtableIndexWriteLatency;
+
+ public IndexMetrics(RouteIndex index)
+ {
+ this.ks = index.baseCfs().getKeyspaceName();
+ this.table = index.baseCfs().name;
+ this.indexName = index.getIndexMetadata().name;
+ memtableIndexWriteLatency = Metrics.timer(createMetricName("MemtableIndexWriteLatency"));
+ }
+
+ public void release()
+ {
+ tracked.forEach(Metrics::remove);
+ tracked.clear();
+ }
+
+ private CassandraMetricsRegistry.MetricName createMetricName(String name)
+ {
+ String metricScope = ks + '.' + table;
+ if (indexName != null)
+ {
+ metricScope += '.' + indexName;
+ }
+ metricScope += '.' + SCOPE + '.' + name;
+
+ CassandraMetricsRegistry.MetricName metricName = new CassandraMetricsRegistry.MetricName(DefaultNameFactory.GROUP_NAME,
+ TYPE, name, metricScope, createMBeanName(name, SCOPE));
+ tracked.add(metricName);
+ return metricName;
+ }
+
+ private String createMBeanName(String name, String scope)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append(DefaultNameFactory.GROUP_NAME);
+ builder.append(":type=").append(TYPE);
+ builder.append(',').append("keyspace=").append(ks);
+ builder.append(',').append("table=").append(table);
+ if (indexName != null)
+ builder.append(',').append("index=").append(indexName);
+ builder.append(',').append("scope=").append(scope);
+ builder.append(',').append("name=").append(name);
+ return builder.toString();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/MemtableIndex.java b/src/java/org/apache/cassandra/index/accord/MemtableIndex.java
new file mode 100644
index 0000000000..0f0125861b
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/MemtableIndex.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.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.concurrent.atomic.LongAdder;
+
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.schema.TableId;
+
+public class MemtableIndex
+{
+ private final RangeMemoryIndex memoryIndex = new RangeMemoryIndex();
+ private final LongAdder writeCount = new LongAdder();
+ private final LongAdder estimatedMemoryUsed = new LongAdder();
+
+ public long writeCount()
+ {
+ return writeCount.sum();
+ }
+
+ public long estimatedMemoryUsed()
+ {
+ return estimatedMemoryUsed.sum();
+ }
+
+ public boolean isEmpty()
+ {
+ return memoryIndex.isEmpty();
+ }
+
+ public long index(DecoratedKey key, Clustering> clustering, ByteBuffer value)
+ {
+ if (value == null || value.remaining() == 0)
+ return 0;
+ long size = memoryIndex.add(key, clustering, value);
+ writeCount.increment();
+ estimatedMemoryUsed.add(size);
+ return size;
+ }
+
+ public Segment write(IndexDescriptor id) throws IOException
+ {
+ return memoryIndex.write(id);
+ }
+
+ public Collection search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
+ {
+ return memoryIndex.search(storeId, tableId, start, startInclusive, end, endInclusive);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/MemtableIndexManager.java b/src/java/org/apache/cassandra/index/accord/MemtableIndexManager.java
new file mode 100644
index 0000000000..1078b4fc04
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/MemtableIndexManager.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.nio.ByteBuffer;
+import java.util.NavigableSet;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.schema.TableId;
+
+public interface MemtableIndexManager
+{
+ long index(DecoratedKey key, Row row, Memtable mt);
+
+ MemtableIndex getPendingMemtableIndex(LifecycleNewTracker tracker);
+
+ void discardMemtable(Memtable memtable);
+
+ void renewMemtable(Memtable renewed);
+
+ NavigableSet search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive);
+}
diff --git a/src/java/org/apache/cassandra/index/accord/OrderedRouteSerializer.java b/src/java/org/apache/cassandra/index/accord/OrderedRouteSerializer.java
new file mode 100644
index 0000000000..f9e5c22b47
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/OrderedRouteSerializer.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.service.accord.api.AccordRoutingKey;
+import org.apache.cassandra.service.accord.serializers.AccordRoutingKeyByteSource;
+
+public class OrderedRouteSerializer
+{
+ private static final AccordRoutingKeyByteSource.FixedLength SERIALIZER = AccordRoutingKeyByteSource.fixedLength(DatabaseDescriptor.getPartitioner());
+
+ public static ByteBuffer serializeRoutingKey(AccordRoutingKey key)
+ {
+ return ByteBuffer.wrap(SERIALIZER.serialize(key));
+ }
+
+ public static byte[] serializeRoutingKeyNoTable(AccordRoutingKey key)
+ {
+ return SERIALIZER.serializeNoTable(key);
+ }
+
+ public static byte[] unwrap(AccordRoutingKey key)
+ {
+ return SERIALIZER.serialize(key);
+ }
+
+ public static AccordRoutingKey deserializeRoutingKey(ByteBuffer bb)
+ {
+ try
+ {
+ return SERIALIZER.fromComparableBytes(ByteBufferAccessor.instance, bb);
+ }
+ catch (IOException e)
+ {
+ throw new UnsupportedOperationException(e);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/RangeMemoryIndex.java b/src/java/org/apache/cassandra/index/accord/RangeMemoryIndex.java
new file mode 100644
index 0000000000..5581708171
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/RangeMemoryIndex.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import javax.annotation.concurrent.GuardedBy;
+
+import accord.primitives.Routable;
+import accord.primitives.Route;
+import accord.primitives.Unseekable;
+import org.apache.cassandra.cache.IMeasurableMemory;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.service.accord.AccordKeyspace;
+import org.apache.cassandra.service.accord.TokenRange;
+import org.apache.cassandra.service.accord.api.AccordRoutingKey;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.RTree;
+import org.apache.cassandra.utils.RangeTree;
+
+public class RangeMemoryIndex
+{
+ @GuardedBy("this")
+ private final Map> map = new HashMap<>();
+ @GuardedBy("this")
+ private final Map groupMetadata = new HashMap<>();
+
+ private static class Metadata
+ {
+ public byte[] minTerm, maxTerm;
+ }
+
+ private static RangeTree createRangeTree()
+ {
+ return new RTree<>((a, b) -> ByteArrayUtil.compareUnsigned(a, 0, b, 0, a.length), new RangeTree.Accessor<>()
+ {
+ @Override
+ public byte[] start(Range range)
+ {
+ return range.start;
+ }
+
+ @Override
+ public byte[] end(Range range)
+ {
+ return range.end;
+ }
+
+ @Override
+ public boolean contains(byte[] start, byte[] end, byte[] bytes)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean intersects(Range range, byte[] start, byte[] end)
+ {
+ return range.intersects(start, end);
+ }
+
+ @Override
+ public boolean intersects(Range left, Range right)
+ {
+ return left.intersects(right.start, right.end);
+ }
+ });
+ }
+
+ public synchronized long add(DecoratedKey key, Clustering> clustering, ByteBuffer value)
+ {
+ Route> route;
+ try
+ {
+ route = AccordKeyspace.deserializeRouteOrNull(value);
+ }
+ catch (IOException e)
+ {
+ throw new UncheckedIOException(e);
+ }
+
+ return add(key, route);
+ }
+
+
+ public synchronized long add(DecoratedKey key, Route> route)
+ {
+ if (route.domain() != Routable.Domain.Range)
+ return 0;
+ long sum = 0;
+ for (Unseekable keyOrRange : route)
+ sum += add(key, keyOrRange);
+ return sum;
+ }
+
+ protected long add(DecoratedKey key, Unseekable keyOrRange)
+ {
+ if (keyOrRange.domain() != Routable.Domain.Range)
+ throw new IllegalArgumentException("Unexpected domain: " + keyOrRange.domain());
+ TokenRange ts = (TokenRange) keyOrRange;
+
+ int storeId = AccordKeyspace.CommandRows.getStoreId(key);
+ TableId tableId = ts.table();
+ Group group = new Group(storeId, tableId);
+ byte[] start = OrderedRouteSerializer.serializeRoutingKeyNoTable((AccordRoutingKey) ts.start());
+ byte[] end = OrderedRouteSerializer.serializeRoutingKeyNoTable((AccordRoutingKey) ts.end());
+ Range range = new Range(start, end);
+ map.computeIfAbsent(group, ignore -> createRangeTree()).add(range, key);
+ Metadata metadata = groupMetadata.computeIfAbsent(group, ignore -> new Metadata());
+
+ metadata.minTerm = metadata.minTerm == null ? start : ByteArrayUtil.compareUnsigned(metadata.minTerm, 0, start, 0, metadata.minTerm.length) > 0 ? start : metadata.minTerm;
+ metadata.maxTerm = metadata.maxTerm == null ? end : ByteArrayUtil.compareUnsigned(metadata.maxTerm, 0, end, 0, metadata.maxTerm.length) < 0 ? end : metadata.maxTerm;
+ return TableId.EMPTY_SIZE + range.unsharedHeapSize();
+ }
+
+ public NavigableSet search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
+ {
+ RangeTree rangesToPks = map.get(new Group(storeId, tableId));
+ if (rangesToPks == null || rangesToPks.isEmpty())
+ return Collections.emptyNavigableSet();
+ TreeMap> matches = search(rangesToPks, start, end);
+ if (matches.isEmpty())
+ return Collections.emptyNavigableSet();
+ TreeSet pks = new TreeSet<>();
+ matches.values().forEach(s -> s.forEach(d -> pks.add(d.getKey())));
+ return pks;
+ }
+
+ private TreeMap> search(RangeTree tokensToPks, byte[] start, byte[] end)
+ {
+
+ TreeMap> matches = new TreeMap<>();
+ tokensToPks.search(new Range(start, end), e -> matches.computeIfAbsent(e.getKey(), ignore -> new HashSet<>()).add(e.getValue()));
+ return matches;
+ }
+
+ public synchronized boolean isEmpty()
+ {
+ return map.isEmpty();
+ }
+
+ public Segment write(IndexDescriptor id) throws IOException
+ {
+ if (map.isEmpty())
+ throw new AssertionError("Unable to write empty index");
+ Map output = new HashMap<>();
+
+ List groups = new ArrayList<>(map.keySet());
+ groups.sort(Comparator.naturalOrder());
+
+ for (Group group : groups)
+ {
+ RangeTree submap = map.get(group);
+ if (submap.isEmpty()) // is this possible? put here for safty so list is never empty
+ continue;
+ Metadata metadata = groupMetadata.get(group);
+
+ //TODO (performance): if the RangeTree can return the data in sorted order, then this local can become faster
+ // Right now the code is based off RTree, which is undefined order, so we must iterate then sort; in testing this is a good chunk of the time of this method
+ List list = submap.stream()
+ .map(e -> new CheckpointIntervalArrayIndex.Interval(e.getKey().start, e.getKey().end, ByteBufferUtil.getArray(e.getValue().getKey())))
+ .sorted(Comparator.naturalOrder())
+ .collect(Collectors.toList());
+
+ CheckpointIntervalArrayIndex.SegmentWriter writer = new CheckpointIntervalArrayIndex.SegmentWriter(id, list.get(0).start.length, list.get(0).value.length);
+ EnumMap meta = writer.write(list.toArray(CheckpointIntervalArrayIndex.Interval[]::new));
+ if (meta.isEmpty()) // don't include empty segments
+ continue;
+ output.put(group, new Segment.Metadata(meta, metadata.minTerm, metadata.maxTerm));
+ }
+
+ return new Segment(output);
+ }
+
+ private static class Range implements Comparable, IMeasurableMemory
+ {
+ private static final long EMPTY_SIZE = ObjectSizes.measure(new Range(null, null));
+
+ private final byte[] start, end;
+
+ private Range(byte[] start, byte[] end)
+ {
+ this.start = start;
+ this.end = end;
+ }
+
+ @Override
+ public int compareTo(Range other)
+ {
+ int rc = ByteArrayUtil.compareUnsigned(start, 0, other.start, 0, start.length);
+ if (rc == 0)
+ rc = ByteArrayUtil.compareUnsigned(end, 0, other.end, 0, end.length);
+ return rc;
+ }
+
+ @Override
+ public long unsharedHeapSize()
+ {
+ return EMPTY_SIZE + ObjectSizes.sizeOfArray(start) * 2;
+ }
+
+ public boolean intersects(byte[] start, byte[] end)
+ {
+ if (ByteArrayUtil.compareUnsigned(this.start, 0, end, 0, end.length) >= 0)
+ return false;
+ if (ByteArrayUtil.compareUnsigned(this.end, 0, start, 0, start.length) <= 0)
+ return false;
+ return true;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/RouteIndex.java b/src/java/org/apache/cassandra/index/accord/RouteIndex.java
new file mode 100644
index 0000000000..8dcaf2067c
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/RouteIndex.java
@@ -0,0 +1,604 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NavigableSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.cql3.statements.schema.IndexTarget;
+import org.apache.cassandra.db.CassandraWriteContext;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.ReadCommand;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.RegularAndStaticColumns;
+import org.apache.cassandra.db.WriteContext;
+import org.apache.cassandra.db.compaction.CompactionManager;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.db.lifecycle.Tracker;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.EncodingStats;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.IndexRegistry;
+import org.apache.cassandra.index.TargetParser;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+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.ColumnMetadata;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.service.accord.AccordKeyspace;
+import org.apache.cassandra.service.accord.api.AccordRoutingKey;
+import org.apache.cassandra.utils.AbstractIterator;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.FutureCombiner;
+
+public class RouteIndex implements Index, INotificationConsumer
+{
+ public enum RegisterStatus
+ {PENDING, REGISTERED, UNREGISTERED}
+
+ private static final Logger logger = LoggerFactory.getLogger(RouteIndex.class);
+
+ private static final Component.Type type = Component.Type.createSingleton("AccordRoute", "AccordRoute.*.db", true, null);
+
+ private final ColumnFamilyStore baseCfs;
+ private final ColumnMetadata column;
+ private final IndexMetadata indexMetadata;
+ private final IndexMetrics indexMetrics;
+ private final MemtableIndexManager memtableIndexManager;
+ private final SSTableManager sstableManager;
+ // Tracks whether we've started the index build on initialization.
+ private volatile boolean initBuildStarted = false;
+ private volatile RegisterStatus registerStatus = RegisterStatus.PENDING;
+
+ public RouteIndex(ColumnFamilyStore baseCfs, IndexMetadata indexMetadata)
+ {
+ if (!SchemaConstants.ACCORD_KEYSPACE_NAME.equals(baseCfs.getKeyspaceName()))
+ throw new IllegalArgumentException("Route index is only allowed for accord commands table; given " + baseCfs().metadata());
+ if (!AccordKeyspace.COMMANDS.equals(baseCfs.name))
+ throw new IllegalArgumentException("Route index is only allowed for accord commands table; given " + baseCfs().metadata());
+
+ TableMetadata tableMetadata = baseCfs.metadata();
+ Pair target = TargetParser.parse(tableMetadata, indexMetadata);
+ if (!AccordKeyspace.CommandsColumns.route.name.equals(target.left.name))
+ throw new IllegalArgumentException("Attempted to index the wrong column; needed " + AccordKeyspace.CommandsColumns.route.name + " but given " + target.left.name);
+
+ if (target.right != IndexTarget.Type.VALUES)
+ throw new IllegalArgumentException("Attempted to index " + AccordKeyspace.CommandsColumns.route.name + " with index type " + target.right + "; only " + IndexTarget.Type.VALUES + " is supported");
+
+ this.baseCfs = baseCfs;
+ this.indexMetadata = indexMetadata;
+ this.memtableIndexManager = new RouteMemtableIndexManager(this);
+ this.sstableManager = new RouteSSTableManager();
+ this.indexMetrics = new IndexMetrics(this);
+ this.column = target.left;
+
+ Tracker tracker = baseCfs.getTracker();
+ tracker.subscribe(this);
+ }
+
+ public ColumnMetadata column()
+ {
+ return column;
+ }
+
+ public IndexMetrics indexMetrics()
+ {
+ return indexMetrics;
+ }
+
+ public ColumnFamilyStore baseCfs()
+ {
+ return baseCfs;
+ }
+
+ @Override
+ public IndexMetadata getIndexMetadata()
+ {
+ return indexMetadata;
+ }
+
+ @Override
+ public boolean shouldBuildBlocking()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isSSTableAttached()
+ {
+ return true;
+ }
+
+ @Override
+ public Optional getBackingTable()
+ {
+ return Optional.empty();
+ }
+
+ @Override
+ public Set getComponents()
+ {
+ return Collections.singleton(type.getSingleton());
+ }
+
+ @Override
+ public Callable> getInitializationTask()
+ {
+ //TODO (now): in SAI startup doesn't validate... what are the downstream issues this can face? Corrupt indexes not being detected?
+ boolean starting = StorageService.instance.isStarting();
+ return () -> {
+ if (baseCfs.indexManager.isIndexQueryable(this))
+ {
+ initBuildStarted = true;
+ return null;
+ }
+
+ // stop in-progress compaction tasks to prevent compacted sstable not being indexed.
+ 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;
+
+ List nonIndexed = findNonIndexedSSTables(baseCfs, sstableManager);
+
+ if (nonIndexed.isEmpty())
+ return null;
+
+ // split sorted sstables into groups with similar size and build each group in separate compaction thread
+ List> groups = StorageAttachedIndex.groupBySize(nonIndexed, DatabaseDescriptor.getConcurrentIndexBuilders());
+ List> futures = new ArrayList<>();
+
+ for (List group : groups)
+ {
+ futures.add(CompactionManager.instance.submitIndexBuild(new RouteSecondaryIndexBuilder(this, sstableManager, group, false, true)));
+ }
+
+ return FutureCombiner.allOf(futures).get();
+ };
+ }
+
+ private List findNonIndexedSSTables(ColumnFamilyStore baseCfs, SSTableManager manager)
+ {
+ Set sstables = baseCfs.getLiveSSTables();
+
+ // Initialize the SSTable indexes w/ valid existing components...
+ manager.onSSTableChanged(Collections.emptyList(), sstables);
+
+ // ...then identify and rebuild the SSTable indexes that are missing.
+ List nonIndexed = new ArrayList<>();
+
+ for (SSTableReader sstable : sstables)
+ {
+ if (!sstable.isMarkedCompacted() && !manager.isIndexComplete(sstable))
+ {
+ nonIndexed.add(sstable);
+ }
+ }
+
+ return nonIndexed;
+ }
+
+
+ @Override
+ public boolean isQueryable(Status status)
+ {
+ // consider unknown status as queryable, because gossip may not be up-to-date for newly joining nodes.
+ return status == Status.BUILD_SUCCEEDED || status == Status.UNKNOWN;
+ }
+
+ public RegisterStatus registerStatus()
+ {
+ return registerStatus;
+ }
+
+ @Override
+ public synchronized void register(IndexRegistry registry)
+ {
+ registry.registerIndex(this);
+ registerStatus = RegisterStatus.REGISTERED;
+ }
+
+ @Override
+ public synchronized void unregister(IndexRegistry registry)
+ {
+ Index.super.unregister(registry);
+ registerStatus = RegisterStatus.UNREGISTERED;
+ }
+
+ @Override
+ public Callable> getTruncateTask(long truncatedAt)
+ {
+ /*
+ * index files will be removed as part of base sstable lifecycle in {@link LogTransaction#delete(java.io.File)}
+ * asynchronously, but we need to mark the index queryable because if the truncation is during the initial
+ * build of the index it won't get marked queryable by the build.
+ */
+ return () -> {
+ logger.info("Making index queryable during table truncation");
+ baseCfs.indexManager.makeIndexQueryable(this, Status.BUILD_SUCCEEDED);
+ return null;
+ };
+ }
+
+ @Override
+ public Callable> getBlockingFlushTask()
+ {
+ return null; // storage-attached indexes are flushed alongside memtable
+ }
+
+ @Override
+ public Callable> getMetadataReloadTask(IndexMetadata indexMetadata)
+ {
+ return null;
+ }
+
+ @Override
+ public Callable> getInvalidateTask()
+ {
+ return () -> null;
+ }
+
+ @Override
+ public void validate(PartitionUpdate update, ClientState state) throws InvalidRequestException
+ {
+ // only internal can write... so it must be valid no?
+ }
+
+
+ //TODO (now): flesh this stuff out...
+
+
+ @Override
+ public SSTableFlushObserver getFlushObserver(Descriptor descriptor,
+ LifecycleNewTracker tracker)
+ {
+ // mimics org.apache.cassandra.index.sai.disk.v1.V1OnDiskFormat.newPerColumnIndexWriter
+ IndexDescriptor id = IndexDescriptor.create(descriptor, baseCfs.getPartitioner(), baseCfs.metadata().comparator);
+ if (tracker.opType() != OperationType.FLUSH || !initBuildStarted)
+ {
+ return new RouteIndexFormat.SSTableIndexWriter(this, id);
+ }
+ else
+ {
+ return new RouteIndexFormat.MemtableRouteIndexWriter(id, memtableIndexManager.getPendingMemtableIndex(tracker));
+ }
+ }
+
+ @Override
+ public Indexer indexerFor(DecoratedKey key,
+ RegularAndStaticColumns columns,
+ long nowInSec,
+ WriteContext ctx,
+ IndexTransaction.Type transactionType,
+ Memtable memtable)
+ {
+ // since we are attached we only care about update
+ if (transactionType != IndexTransaction.Type.UPDATE)
+ return null;
+ return new Indexer()
+ {
+ @Override
+ public void insertRow(Row row)
+ {
+ long size = memtableIndexManager.index(key, row, memtable);
+ if (size > 0)
+ memtable.markExtraOnHeapUsed(size, CassandraWriteContext.fromContext(ctx).getGroup());
+ }
+
+ @Override
+ public void updateRow(Row oldRowData, Row newRowData)
+ {
+ insertRow(newRowData);
+ }
+ };
+ }
+
+ @Override
+ public boolean supportsExpression(ColumnMetadata column, Operator operator)
+ {
+ // disallow all queries, in order to interact with this index you must bypass CQL
+ return false;
+ }
+
+ @Override
+ public RowFilter getPostIndexQueryFilter(RowFilter filter)
+ {
+ return RowFilter.none();
+ }
+
+ @Override
+ public Searcher searcherFor(ReadCommand command)
+ {
+ List expressions = command.rowFilter().getExpressions().stream().collect(Collectors.toList());
+ if (expressions.isEmpty())
+ return null;
+ ByteBuffer start = null;
+ boolean startInclusive = true;
+ ByteBuffer end = null;
+ boolean endInclusive = true;
+ Integer storeId = null;
+ for (RowFilter.Expression e : expressions)
+ {
+ if (e.column() == AccordKeyspace.CommandsColumns.route)
+ {
+ switch (e.operator())
+ {
+ case GT:
+ start = e.getIndexValue();
+ startInclusive = false;
+ break;
+ case GTE:
+ start = e.getIndexValue();
+ startInclusive = true;
+ break;
+ case LT:
+ end = e.getIndexValue();
+ endInclusive = false;
+ break;
+ case LTE:
+ end = e.getIndexValue();
+ endInclusive = true;
+ break;
+ default:
+ return null;
+ }
+ }
+ else if (e.column() == AccordKeyspace.CommandsColumns.store_id && e.operator() == Operator.EQ)
+ {
+ storeId = Int32Type.instance.compose(e.getIndexValue());
+ }
+ }
+ if (start == null || end == null || storeId == null)
+ return null;
+ int finalStoreId = storeId;
+ ByteBuffer finalStart = start;
+ boolean finalStartInclusive = startInclusive;
+ ByteBuffer finalEnd = end;
+ boolean finalEndInclusive = endInclusive;
+ return new Searcher()
+ {
+ @Override
+ public ReadCommand command()
+ {
+ return command;
+ }
+
+ @Override
+ public UnfilteredPartitionIterator search(ReadExecutionController executionController)
+ {
+ // find all partitions from memtable / sstable
+ NavigableSet partitions = search(finalStoreId, finalStart, finalStartInclusive, finalEnd, finalEndInclusive);
+ // do SinglePartitionReadCommand per partition
+ return new SearchIterator(executionController, command, partitions);
+ }
+
+ NavigableSet search(int storeId,
+ ByteBuffer startTableWithToken, boolean startInclusive,
+ ByteBuffer endTableWithToken, boolean endInclusive)
+ {
+ TableId tableId;
+ byte[] start;
+ {
+
+ AccordRoutingKey route = OrderedRouteSerializer.deserializeRoutingKey(startTableWithToken);
+ tableId = route.table();
+ start = OrderedRouteSerializer.serializeRoutingKeyNoTable(route);
+ }
+ byte[] end = OrderedRouteSerializer.serializeRoutingKeyNoTable(OrderedRouteSerializer.deserializeRoutingKey(endTableWithToken));
+ NavigableSet matches = sstableManager.search(storeId, tableId, start, startInclusive, end, endInclusive);
+ matches.addAll(memtableIndexManager.search(storeId, tableId, start, startInclusive, end, endInclusive));
+ return matches;
+ }
+ };
+ }
+
+ private class SearchIterator extends AbstractIterator implements UnfilteredPartitionIterator
+ {
+ private final ReadExecutionController executionController;
+ private final ReadCommand command;
+ private final TableMetadata metadata;
+ private final Iterator partitions;
+
+ private SearchIterator(ReadExecutionController executionController, ReadCommand command, NavigableSet partitions)
+ {
+ this.executionController = executionController;
+ this.command = command;
+ this.metadata = command.metadata();
+ this.partitions = partitions.iterator();
+ }
+
+ @Override
+ public TableMetadata metadata()
+ {
+ return metadata;
+ }
+
+ @Override
+ protected UnfilteredRowIterator computeNext()
+ {
+ if (!partitions.hasNext())
+ return endOfData();
+ DecoratedKey pk = metadata.partitioner.decorateKey(partitions.next());
+ return new UnfilteredRowIterator()
+ {
+ @Override
+ public DeletionTime partitionLevelDeletion()
+ {
+ return DeletionTime.LIVE;
+ }
+
+ @Override
+ public EncodingStats stats()
+ {
+ return EncodingStats.NO_STATS;
+ }
+
+ @Override
+ public TableMetadata metadata()
+ {
+ return metadata;
+ }
+
+ @Override
+ public boolean isReverseOrder()
+ {
+ return false;
+ }
+
+ @Override
+ public RegularAndStaticColumns columns()
+ {
+ return RegularAndStaticColumns.NONE;
+ }
+
+ @Override
+ public DecoratedKey partitionKey()
+ {
+ return pk;
+ }
+
+ @Override
+ public Row staticRow()
+ {
+ return null;
+ }
+
+ @Override
+ public void close()
+ {
+
+ }
+
+ private Row row = BTreeRow.emptyRow(Clustering.EMPTY);
+
+ @Override
+ public boolean hasNext()
+ {
+ return row != null;
+ }
+
+ @Override
+ public Unfiltered next()
+ {
+ Row row = this.row;
+ this.row = null;
+ return row;
+ }
+ };
+ }
+
+ @Override
+ public void close()
+ {
+
+ }
+ }
+
+ @Override
+ public void handleNotification(INotification notification, Object sender)
+ {
+ // unfortunately, we can only check the type of notification via instanceof :(
+ if (notification instanceof SSTableAddedNotification)
+ {
+ SSTableAddedNotification notice = (SSTableAddedNotification) notification;
+ sstableManager.onSSTableChanged(Collections.emptySet(), notice.added);
+ }
+ else if (notification instanceof SSTableListChangedNotification)
+ {
+ SSTableListChangedNotification notice = (SSTableListChangedNotification) notification;
+ sstableManager.onSSTableChanged(notice.removed, notice.added);
+ }
+ else if (notification instanceof MemtableRenewedNotification)
+ {
+ memtableIndexManager.renewMemtable(((MemtableRenewedNotification) notification).renewed);
+ }
+ else if (notification instanceof MemtableDiscardedNotification)
+ {
+ memtableIndexManager.discardMemtable(((MemtableDiscardedNotification) notification).memtable);
+ }
+ }
+
+ //TODO (coverage): everything below here never triggered...
+
+ @Override
+ public boolean dependsOn(ColumnMetadata column)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public AbstractType> customExpressionValueType()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getEstimatedResultRows()
+ {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/RouteIndexFormat.java b/src/java/org/apache/cassandra/index/accord/RouteIndexFormat.java
new file mode 100644
index 0000000000..bd722ca9f0
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/RouteIndexFormat.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Supplier;
+import java.util.zip.CRC32C;
+import java.util.zip.Checksum;
+
+import com.google.common.collect.Maps;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.index.accord.IndexDescriptor.IndexComponent;
+import org.apache.cassandra.io.sstable.SSTableFlushObserver;
+import org.apache.cassandra.io.util.ChecksumedRandomAccessReader;
+import org.apache.cassandra.io.util.ChecksumedSequentialWriter;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.serializers.UUIDSerializer;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.Throwables;
+
+import static org.apache.cassandra.utils.Clock.Global.nowInSeconds;
+
+// A route index consists of a few files: cintia_sorted_list, cintia_checkpoints, and metadata
+// metadata stores the segement mappings and stats needed for search selection
+public class RouteIndexFormat
+{
+ public static final Supplier CHECKSUM_SUPPLIER = CRC32C::new;
+
+ public interface Writer extends SSTableFlushObserver
+ {
+
+ }
+
+ public static class SSTableIndexWriter extends MemtableRouteIndexWriter
+ {
+ private final RouteIndex index;
+ private DecoratedKey current;
+
+ public SSTableIndexWriter(RouteIndex index, IndexDescriptor id)
+ {
+ super(id, new MemtableIndex());
+ this.index = index;
+ }
+
+ @Override
+ public void startPartition(DecoratedKey key, long keyPosition, long keyPositionForSASI)
+ {
+ this.current = key;
+ }
+
+ @Override
+ public void nextUnfilteredCluster(Unfiltered unfiltered)
+ {
+ // there is some duplication from org.apache.cassandra.index.accord.RouteMemtableIndexManager.index
+ // should this be cleaned up?
+ if (!unfiltered.isRow())
+ return;
+ Row row = (Row) unfiltered;
+ // simplified version of org.apache.cassandra.index.sai.utils.IndexTermType.valueOf
+ Cell> cell = row.getCell(index.column());
+ ByteBuffer value = cell == null || !cell.isLive(nowInSeconds()) ? null : cell.buffer();
+ indexer.index(current, row.clustering(), value);
+ }
+ }
+
+ public static class MemtableRouteIndexWriter implements Writer
+ {
+ private final IndexDescriptor id;
+ protected final MemtableIndex indexer;
+
+ public MemtableRouteIndexWriter(IndexDescriptor id, MemtableIndex indexer)
+ {
+ this.id = id;
+ this.indexer = indexer;
+ }
+
+
+ @Override
+ public void begin()
+ {
+ // no-op
+ }
+
+ @Override
+ public void startPartition(DecoratedKey key, long keyPosition, long keyPositionForSASI)
+ {
+ // no-op
+ }
+
+ @Override
+ public void staticRow(Row staticRow)
+ {
+ // no-op
+ }
+
+ @Override
+ public void nextUnfilteredCluster(Unfiltered unfiltered)
+ {
+ // no-op
+ }
+
+ @Override
+ public void complete()
+ {
+ try
+ {
+ if (!indexer.isEmpty())
+ {
+ Segment segment = indexer.write(id);
+ appendSegment(id, segment);
+ }
+ else
+ {
+ // nothing to see here... need to still mark the SSTable as indexed, so need an empty segment
+ appendSegment(id, Segment.EMPTY);
+ }
+ }
+ catch (IOException e)
+ {
+ abort(e);
+ throw Throwables.unchecked(e);
+ }
+ }
+
+ @Override
+ public void abort(Throwable accumulator)
+ {
+ id.deleteIndex();
+ }
+
+ public void abort(Throwable accumulator, boolean fromIndex)
+ {
+ abort(accumulator);
+ // If the abort was from an index error, propagate the error upstream so index builds, compactions, and
+ // flushes can handle it correctly.
+ if (fromIndex)
+ throw Throwables.unchecked(accumulator);
+ }
+ }
+
+ static List readSegements(Map index) throws IOException
+ {
+ List segments = new ArrayList<>();
+
+ try (var metaReader = new ChecksumedRandomAccessReader(index.get(IndexComponent.METADATA).createReader(), CHECKSUM_SUPPLIER);
+ var segmentReader = new ChecksumedRandomAccessReader(index.get(IndexComponent.SEGMENT).createReader(), CHECKSUM_SUPPLIER))
+ {
+ while (metaReader.getFilePointer() < metaReader.length())
+ {
+ metaReader.resetChecksum();
+ long startPointer = metaReader.readUnsignedVInt();
+ long endPointer = metaReader.readUnsignedVInt();
+ int groupSize = metaReader.readUnsignedVInt32();
+ int segmentChecksum = metaReader.readInt();
+ int metadataChecksum = metaReader.getValue32AndResetChecksum();
+ int actualChecksum = metaReader.readInt();
+ assert actualChecksum == metadataChecksum;
+
+ segmentReader.resetChecksum();
+ segmentReader.seek(startPointer);
+ Map groups = Maps.newHashMapWithExpectedSize(groupSize);
+ for (int i = 0; i < groupSize; i++)
+ {
+ int storeId = segmentReader.readVInt32();
+ TableId tableId = TableId.fromUUID(new UUID(segmentReader.readLong(), segmentReader.readLong()));
+ Group group = new Group(storeId, tableId);
+ int metaSize = segmentReader.readUnsignedVInt32();
+ EnumMap metas = new EnumMap<>(IndexComponent.class);
+ for (int j = 0; j < metaSize; j++)
+ {
+ IndexComponent c = IndexComponent.fromByte(segmentReader.readByte());
+ metas.put(c, new Segment.ComponentMetadata(segmentReader.readUnsignedVInt(), segmentReader.readUnsignedVInt()));
+ }
+ byte[] minTerm = ByteArrayUtil.readWithVIntLength(segmentReader);
+ byte[] maxTerm = ByteArrayUtil.readWithVIntLength(segmentReader);
+ Segment.Metadata existing = groups.put(group, new Segment.Metadata(metas, minTerm, maxTerm));
+ assert existing == null;
+ }
+ int actualSegmentChecksum = segmentReader.getValue32AndResetChecksum();
+ assert actualSegmentChecksum == segmentChecksum;
+ assert segmentReader.getFilePointer() == endPointer;
+ segments.add(new Segment(groups));
+ }
+ }
+ return segments;
+ }
+
+ static void appendSegment(IndexDescriptor id, Segment segment) throws IOException
+ {
+ List groups = new ArrayList<>(segment.groups.keySet());
+ groups.sort(Comparator.naturalOrder());
+
+ try (var segmentWriter = ChecksumedSequentialWriter.open(id.fileFor(IndexComponent.SEGMENT), true, CHECKSUM_SUPPLIER);
+ var metadataWriter = ChecksumedSequentialWriter.open(id.fileFor(IndexComponent.METADATA), true, CHECKSUM_SUPPLIER))
+ {
+ long startPointer = segmentWriter.getFilePointer();
+ for (Group group : groups)
+ {
+ Segment.Metadata metadata = segment.groups.get(group);
+ writeGroup(segmentWriter, group, metadata);
+ }
+ long endPointer = segmentWriter.getFilePointer();
+
+ int checksum = segmentWriter.getValue32AndResetChecksum();
+ metadataWriter.writeUnsignedVInt(startPointer);
+ metadataWriter.writeUnsignedVInt(endPointer);
+ metadataWriter.writeUnsignedVInt32(segment.groups.size());
+ metadataWriter.writeInt(checksum);
+ metadataWriter.writeInt(metadataWriter.getValue32AndResetChecksum());
+ }
+ }
+
+ private static void writeGroup(ChecksumedSequentialWriter seq, Group group, Segment.Metadata metadata) throws IOException
+ {
+ seq.writeVInt32(group.storeId);
+ seq.write(UUIDSerializer.instance.serialize(group.tableId.asUUID()));
+ seq.writeUnsignedVInt32(metadata.metas.size());
+ for (Map.Entry e : metadata.metas.entrySet())
+ {
+ seq.writeByte(e.getKey().value);
+ seq.writeUnsignedVInt(e.getValue().offset);
+ seq.writeUnsignedVInt(e.getValue().endOffset);
+ }
+ ByteArrayUtil.writeWithVIntLength(metadata.minTerm, seq);
+ ByteArrayUtil.writeWithVIntLength(metadata.maxTerm, seq);
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/RouteMemtableIndexManager.java b/src/java/org/apache/cassandra/index/accord/RouteMemtableIndexManager.java
new file mode 100644
index 0000000000..f61dd14e90
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/RouteMemtableIndexManager.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.accord;
+
+import java.nio.ByteBuffer;
+import java.util.NavigableSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+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.schema.TableId;
+
+import static org.apache.cassandra.utils.Clock.Global.nanoTime;
+import static org.apache.cassandra.utils.Clock.Global.nowInSeconds;
+
+public class RouteMemtableIndexManager implements MemtableIndexManager
+{
+ private final ConcurrentMap liveMemtableIndexMap = new ConcurrentHashMap<>();
+ private final RouteIndex index;
+
+ public RouteMemtableIndexManager(RouteIndex index)
+ {
+ this.index = index;
+ }
+
+ @Override
+ public long index(DecoratedKey key, Row row, Memtable mt)
+ {
+ if (row.isStatic())
+ return 0;
+ //TODO (performance): we dropped jdk8 and this was fixed in jdk8... so do we need to do this still?
+ 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());
+
+ long start = nanoTime();
+
+ long bytes = 0;
+
+ // simplified version of org.apache.cassandra.index.sai.utils.IndexTermType.valueOf
+ Cell> cell = row.getCell(index.column());
+ ByteBuffer value = cell == null || !cell.isLive(nowInSeconds()) ? null : cell.buffer();
+
+ bytes += target.index(key, row.clustering(), value);
+ index.indexMetrics().memtableIndexWriteLatency.update(nanoTime() - start, TimeUnit.NANOSECONDS);
+ return bytes;
+ }
+
+ @Override
+ public MemtableIndex getPendingMemtableIndex(LifecycleNewTracker tracker)
+ {
+ return liveMemtableIndexMap.keySet().stream()
+ .filter(m -> tracker.equals(m.getFlushTransaction()))
+ .findFirst()
+ .map(liveMemtableIndexMap::get)
+ .orElse(null);
+ }
+
+ @Override
+ public void discardMemtable(Memtable memtable)
+ {
+ liveMemtableIndexMap.remove(memtable);
+ }
+
+ @Override
+ public void renewMemtable(Memtable renewed)
+ {
+ for (Memtable memtable : liveMemtableIndexMap.keySet())
+ {
+ // remove every index but the one that corresponds to the post-truncate Memtable
+ if (renewed != memtable)
+ {
+ liveMemtableIndexMap.remove(memtable);
+ }
+ }
+ }
+
+ @Override
+ public NavigableSet search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
+ {
+ TreeSet matches = new TreeSet<>();
+ liveMemtableIndexMap.values().forEach(m -> matches.addAll(m.search(storeId, tableId, start, startInclusive, end, endInclusive)));
+ return matches;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/RouteSSTableManager.java b/src/java/org/apache/cassandra/index/accord/RouteSSTableManager.java
new file mode 100644
index 0000000000..94bb3b124b
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/RouteSSTableManager.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.TreeSet;
+
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.TableId;
+
+public class RouteSSTableManager implements SSTableManager
+{
+ private final Map sstables = new HashMap<>();
+
+ @Override
+ public synchronized void onSSTableChanged(Collection removed, Iterable added)
+ {
+ //TODO (performance): most added tables will have 0 segmenets, so exclude those from search
+ removed.forEach(s -> {
+ SSTableIndex index = sstables.remove(s);
+ if (index != null)
+ {
+ index.close();
+ index.id.deleteIndex();
+ }
+ });
+
+ List notComplete = null;
+ for (SSTableReader sstable : added)
+ {
+ IndexDescriptor id = IndexDescriptor.create(sstable);
+ if (!id.isIndexBuildComplete())
+ {
+ if (notComplete == null) notComplete = new ArrayList<>();
+ notComplete.add(sstable);
+ continue;
+ }
+ try
+ {
+ sstables.put(sstable, SSTableIndex.create(id));
+ }
+ catch (IOException e)
+ {
+ notComplete.add(sstable);
+ }
+ }
+ if (notComplete != null)
+ throw new IllegalArgumentException("SStables were added without an index... " + notComplete);
+ }
+
+ @Override
+ public synchronized boolean isIndexComplete(SSTableReader reader)
+ {
+ return sstables.containsKey(reader);
+ }
+
+ @Override
+ public synchronized NavigableSet search(int storeId, TableId tableId,
+ byte[] start, boolean startInclusive,
+ byte[] end, boolean endInclusive)
+ {
+ Group group = new Group(storeId, tableId);
+ TreeSet matches = new TreeSet<>();
+ for (SSTableIndex index : sstables.values())
+ matches.addAll(index.search(group, start, startInclusive, end, endInclusive));
+ return matches;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/RouteSecondaryIndexBuilder.java b/src/java/org/apache/cassandra/index/accord/RouteSecondaryIndexBuilder.java
new file mode 100644
index 0000000000..8caf981353
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/RouteSecondaryIndexBuilder.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.util.Collections;
+import java.util.List;
+
+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.io.sstable.KeyIterator;
+import org.apache.cassandra.io.sstable.SSTableFlushObserver;
+import org.apache.cassandra.io.sstable.SSTableIdentityIterator;
+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.Ref;
+
+import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
+
+public class RouteSecondaryIndexBuilder extends SecondaryIndexBuilder
+{
+ private static final Logger logger = LoggerFactory.getLogger(RouteSecondaryIndexBuilder.class);
+
+ private final TimeUUID compactionId = nextTimeUUID();
+ private final RouteIndex index;
+ private final TableMetadata metadata;
+ private final Tracker tracker;
+ private final SSTableManager sstableManager;
+ private final List sstables;
+ private final boolean isFullRebuild;
+ private final boolean isInitialBuild;
+ private final long totalSizeInBytes;
+ private long bytesProcessed = 0;
+
+ public RouteSecondaryIndexBuilder(RouteIndex index,
+ SSTableManager sstableManager,
+ List sstables,
+ boolean isFullRebuild,
+ boolean isInitialBuild)
+ {
+ this.index = index;
+ this.metadata = index.baseCfs().metadata();
+ this.tracker = index.baseCfs().getTracker();
+ this.sstableManager = sstableManager;
+ this.sstables = sstables;
+ this.isFullRebuild = isFullRebuild;
+ this.isInitialBuild = isInitialBuild;
+ this.totalSizeInBytes = sstables.stream().mapToLong(SSTableReader::uncompressedLength).sum();
+ }
+
+ @Override
+ public CompactionInfo getCompactionInfo()
+ {
+ return new CompactionInfo(metadata,
+ OperationType.INDEX_BUILD,
+ bytesProcessed,
+ totalSizeInBytes,
+ compactionId,
+ sstables);
+ }
+
+ @Override
+ public void build()
+ {
+ if (!validateIndexes())
+ return;
+ for (SSTableReader sstable : sstables)
+ {
+ if (indexSSTable(sstable))
+ return;
+ }
+ }
+
+ /**
+ * @return true if index build should be stopped
+ */
+ private boolean indexSSTable(SSTableReader sstable)
+ {
+ logger.debug("Starting index build on {}", sstable.descriptor);
+
+ RouteIndexFormat.SSTableIndexWriter indexWriter = null;
+
+ Ref extends SSTableReader> ref = sstable.tryRef();
+ if (ref == null)
+ {
+ logger.warn("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))
+ {
+ // remove existing per column index files instead of overwriting
+ IndexDescriptor indexDescriptor = IndexDescriptor.create(sstable);
+ indexDescriptor.deleteIndex();
+
+ indexWriter = new RouteIndexFormat.SSTableIndexWriter(index, indexDescriptor);
+ indexWriter.begin();
+
+ long previousBytesRead = 0;
+
+ try (KeyIterator keys = sstable.keyIterator())
+ {
+ while (keys.hasNext())
+ {
+ if (isStopRequested())
+ {
+ logger.debug("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(indexDescriptor, indexWriter, sstable);
+ }
+
+ return false;
+ }
+ catch (Throwable t)
+ {
+ if (indexWriter != null)
+ {
+ indexWriter.abort(t, true);
+ }
+
+ if (t instanceof InterruptedException)
+ {
+ logger.warn("Interrupted while building indexes on SSTable {}", 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("Stop requested while building initial indexes on SSTable {}.", sstable.descriptor);
+ throw Throwables.unchecked(t);
+ }
+ else
+ {
+ logger.info("Stop requested while building indexes on SSTable {}.", sstable.descriptor);
+ return true;
+ }
+ }
+ else
+ {
+ logger.error("Unable to build indexes on SSTable {}. Cause: {}.", sstable, t.getMessage());
+ throw Throwables.unchecked(t);
+ }
+ }
+ finally
+ {
+ ref.release();
+ }
+ }
+
+ private void completeSSTable(IndexDescriptor indexDescriptor, SSTableFlushObserver indexWriter, SSTableReader sstable)
+ {
+ indexWriter.complete();
+
+ if (!validateIndexes())
+ {
+ logger.debug("dropped during index build");
+ return;
+ }
+
+ // register custom index components into existing sstables
+ sstable.registerComponents(indexDescriptor.getLiveSSTableComponents(), tracker);
+ sstableManager.onSSTableChanged(Collections.emptyList(), Collections.singleton(sstable));
+ }
+
+ /**
+ * 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 boolean validateIndexes()
+ {
+ switch (index.registerStatus())
+ {
+ case PENDING: throw new IllegalStateException("Unable to build indexes if the index has not been registered");
+ case REGISTERED: return true;
+ case UNREGISTERED: break;
+ default: throw new AssertionError("Unknown status: " + index.registerStatus());
+ }
+
+ if (isFullRebuild)
+ throw new RuntimeException(String.format("%s are dropped, will stop index build.", index.getIndexMetadata().name));
+
+ return false;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/RoutesSearcher.java b/src/java/org/apache/cassandra/index/accord/RoutesSearcher.java
new file mode 100644
index 0000000000..1ade3b8a4a
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/RoutesSearcher.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.accord;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import accord.primitives.TxnId;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DataRange;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.PartitionRangeReadCommand;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.service.accord.AccordKeyspace;
+import org.apache.cassandra.service.accord.TokenRange;
+import org.apache.cassandra.service.accord.api.AccordRoutingKey;
+import org.apache.cassandra.utils.CloseableIterator;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class RoutesSearcher
+{
+ private final ColumnFamilyStore cfs = Keyspace.open("system_accord").getColumnFamilyStore("commands");
+ private final Index index = cfs.indexManager.getIndexByName("route");;
+ private final ColumnMetadata route = AccordKeyspace.CommandsColumns.route;
+ private final ColumnMetadata store_id = AccordKeyspace.CommandsColumns.store_id;
+ private final ColumnMetadata txn_id = AccordKeyspace.CommandsColumns.txn_id;
+ private final ColumnFilter columnFilter = ColumnFilter.selectionBuilder().add(store_id).add(txn_id).build();
+ private final DataLimits limits = DataLimits.NONE;
+ private final DataRange dataRange = DataRange.allData(cfs.getPartitioner());
+
+ private CloseableIterator searchKeysAccord(int store, AccordRoutingKey start, AccordRoutingKey end)
+ {
+ RowFilter rowFilter = RowFilter.create(false);
+ rowFilter.add(route, Operator.GT, OrderedRouteSerializer.serializeRoutingKey(start));
+ rowFilter.add(route, Operator.LTE, OrderedRouteSerializer.serializeRoutingKey(end));
+ rowFilter.add(store_id, Operator.EQ, Int32Type.instance.decompose(store));
+
+ PartitionRangeReadCommand cmd = PartitionRangeReadCommand.create(cfs.metadata(),
+ FBUtilities.nowInSeconds(),
+ columnFilter,
+ rowFilter,
+ limits,
+ dataRange);
+ Index.Searcher s = index.searcherFor(cmd);
+ try (ReadExecutionController controler = cmd.executionController())
+ {
+ UnfilteredPartitionIterator partitionIterator = s.search(controler);
+ return new CloseableIterator()
+ {
+ private final Entry entry = new Entry();
+ @Override
+ public void close()
+ {
+ partitionIterator.close();
+ }
+
+ @Override
+ public boolean hasNext()
+ {
+ return partitionIterator.hasNext();
+ }
+
+ @Override
+ public Entry next()
+ {
+ UnfilteredRowIterator next = partitionIterator.next();
+ ByteBuffer[] partitionKeyComponents = AccordKeyspace.CommandRows.splitPartitionKey(next.partitionKey());
+ entry.store_id = AccordKeyspace.CommandRows.getStoreId(partitionKeyComponents);
+ entry.txnId = AccordKeyspace.CommandRows.getTxnId(partitionKeyComponents);
+ return entry;
+ }
+ };
+ }
+ }
+
+ public Set intersects(int store, TokenRange range)
+ {
+ return intersects(store, (AccordRoutingKey) range.start(), (AccordRoutingKey) range.end());
+ }
+
+ public Set intersects(int store, AccordRoutingKey start, AccordRoutingKey end)
+ {
+ HashSet set = new HashSet();
+ try (CloseableIterator it = searchKeysAccord(store, start, end))
+ {
+ while (it.hasNext())
+ {
+ Entry next = it.next();
+ if (next.store_id != store) continue; // the index should filter out, but just in case...
+ set.add(next.txnId);
+ }
+ }
+ return set.isEmpty() ? Collections.emptySet() : set;
+ }
+
+ private static final class Entry
+ {
+ public int store_id;
+ public TxnId txnId;
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/SSTableIndex.java b/src/java/org/apache/cassandra/index/accord/SSTableIndex.java
new file mode 100644
index 0000000000..fceaf8e943
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/SSTableIndex.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.index.accord.CheckpointIntervalArrayIndex.SegmentSearcher;
+import org.apache.cassandra.index.accord.IndexDescriptor.IndexComponent;
+import org.apache.cassandra.io.FSReadError;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.concurrent.RefCounted;
+import org.apache.cassandra.utils.concurrent.SharedCloseableImpl;
+
+public class SSTableIndex extends SharedCloseableImpl
+{
+ public final IndexDescriptor id;
+ private final Map files;
+ private final List segments;
+
+ private SSTableIndex(IndexDescriptor id,
+ Map files,
+ List segments,
+ Cleanup cleanup)
+ {
+ super(cleanup);
+ this.id = id;
+ this.files = files;
+ this.segments = segments;
+ }
+
+ public SSTableIndex(SSTableIndex copy)
+ {
+ super(copy);
+ this.id = copy.id;
+ this.files = copy.files;
+ this.segments = copy.segments;
+ }
+
+ @Override
+ public SSTableIndex sharedCopy()
+ {
+ return new SSTableIndex(this);
+ }
+
+ public static SSTableIndex create(IndexDescriptor id) throws IOException
+ {
+ Map files = new EnumMap<>(IndexComponent.class);
+ for (IndexComponent c : id.getLiveComponents())
+ files.put(c, new FileHandle.Builder(id.fileFor(c)).mmapped(true).complete());
+ List segments = RouteIndexFormat.readSegements(files);
+ files.remove(IndexComponent.SEGMENT).close();
+ files.remove(IndexComponent.METADATA).close();
+ Cleanup cleanup = new Cleanup(files);
+ return new SSTableIndex(id, files, segments, cleanup);
+ }
+
+ public Collection extends ByteBuffer> search(Group group, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
+ {
+ List matches = segments.stream().filter(s -> {
+ Segment.Metadata metadata = s.groups.get(group);
+ if (metadata == null) return false;
+ if (ByteArrayUtil.compareUnsigned(metadata.minTerm, end) >= 0)
+ return false;
+ if (ByteArrayUtil.compareUnsigned(metadata.maxTerm, start) <= 0)
+ return false;
+ return true;
+ })
+ .collect(Collectors.toList());
+ if (matches.isEmpty()) return Collections.emptyList();
+ if (matches.size() == 1) return search(matches.get(0), group, start, startInclusive, end, endInclusive);
+ Set found = new HashSet<>();
+ for (Segment s : matches)
+ found.addAll(search(s, group, start, startInclusive, end, endInclusive));
+ return found;
+ }
+
+ private Collection extends ByteBuffer> search(Segment segment, Group group, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
+ {
+ Set matches = new HashSet<>();
+ Segment.Metadata metadata = Objects.requireNonNull(segment.groups.get(group), () -> "Unknown group: " + group);
+ try
+ {
+ SegmentSearcher searcher = new SegmentSearcher(fileFor(IndexComponent.CINTIA_SORTED_LIST), metadata.metas.get(IndexComponent.CINTIA_SORTED_LIST).offset,
+ fileFor(IndexComponent.CINTIA_CHECKPOINTS), metadata.metas.get(IndexComponent.CINTIA_CHECKPOINTS).offset);
+ searcher.intersects(start, end, interval -> matches.add(ByteBuffer.wrap(interval.value)));
+ }
+ catch (IOException e)
+ {
+ throw new FSReadError(e, id.fileFor(IndexComponent.CINTIA_SORTED_LIST));
+ }
+ return matches;
+ }
+
+ private FileHandle fileFor(IndexComponent c)
+ {
+ return Objects.requireNonNull(files.get(c), () -> "Unknown component: " + c);
+ }
+
+ private static class Cleanup implements RefCounted.Tidy
+ {
+ private final Map files;
+
+ private Cleanup(Map files)
+ {
+ this.files = files;
+ }
+
+ @Override
+ public void tidy() throws Exception
+ {
+ for (IndexComponent c : IndexComponent.values())
+ {
+ FileHandle fh = files.remove(c);
+ if (fh == null) continue;
+ fh.close();
+ }
+ }
+
+ @Override
+ public String name()
+ {
+ return "SSTableIndex Cleanup";
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/index/accord/SSTableManager.java b/src/java/org/apache/cassandra/index/accord/SSTableManager.java
new file mode 100644
index 0000000000..090951670f
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/SSTableManager.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.index.accord;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.NavigableSet;
+
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.TableId;
+
+public interface SSTableManager
+{
+ void onSSTableChanged(Collection removed, Iterable added);
+ boolean isIndexComplete(SSTableReader reader);
+
+ NavigableSet search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive);
+}
diff --git a/src/java/org/apache/cassandra/index/accord/Segment.java b/src/java/org/apache/cassandra/index/accord/Segment.java
new file mode 100644
index 0000000000..b6d0ffa085
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/accord/Segment.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.accord;
+
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.Map;
+
+public class Segment
+{
+ public static final Segment EMPTY = new Segment(Collections.emptyMap());
+
+ public final Map groups;
+
+ public Segment(Map groups)
+ {
+ this.groups = groups;
+ }
+
+ public static class ComponentMetadata
+ {
+ public final long offset, endOffset;
+
+ public ComponentMetadata(long offset, long endOffset)
+ {
+ this.offset = offset;
+ this.endOffset = endOffset;
+ }
+ }
+
+ public static class Metadata
+ {
+ public final EnumMap metas;
+ public final byte[] minTerm, maxTerm;
+
+ public Metadata(EnumMap metas, byte[] minTerm, byte[] maxTerm)
+ {
+ this.metas = metas;
+ this.minTerm = minTerm;
+ this.maxTerm = maxTerm;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/io/util/Checksumed.java b/src/java/org/apache/cassandra/io/util/Checksumed.java
new file mode 100644
index 0000000000..61120ed25e
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/Checksumed.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.io.util;
+
+import java.util.zip.Checksum;
+
+public interface Checksumed
+{
+ Checksum checksum();
+
+ default long getAndResetChecksum()
+ {
+ Checksum c = checksum();
+ long v = c.getValue();
+ c.reset();
+ return v;
+ }
+
+ default int getValue32()
+ {
+ long v = checksum().getValue();
+ if (Long.numberOfLeadingZeros(v) < 32)
+ throw new IllegalStateException("Checksum is larger than 32 bytes!");
+ return (int) v;
+ }
+
+ default int getValue32AndResetChecksum()
+ {
+ Checksum c = checksum();
+ long v = c.getValue();
+ if (Long.numberOfLeadingZeros(v) < 32)
+ throw new IllegalStateException("Checksum is larger than 32 bytes!");
+ c.reset();
+ return (int) v;
+ }
+
+ default void resetChecksum()
+ {
+ checksum().reset();
+ }
+}
diff --git a/src/java/org/apache/cassandra/io/util/ChecksumedDataInputPlus.java b/src/java/org/apache/cassandra/io/util/ChecksumedDataInputPlus.java
new file mode 100644
index 0000000000..8b1d701d4a
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChecksumedDataInputPlus.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.io.util;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+import java.util.zip.Checksum;
+
+public class ChecksumedDataInputPlus implements DataInputPlus, Checksumed
+{
+ private final DataInputPlus delegate;
+ private final Checksum checksum;
+
+ public ChecksumedDataInputPlus(DataInputPlus delegate, Checksum checksum)
+ {
+ this.delegate = delegate;
+ this.checksum = checksum;
+ }
+
+ public ChecksumedDataInputPlus(DataInputPlus delegate, Supplier fn)
+ {
+ this(delegate, fn.get());
+ }
+
+ public DataInputPlus delegate()
+ {
+ return delegate;
+ }
+
+ @Override
+ public Checksum checksum()
+ {
+ return checksum;
+ }
+
+ @Override
+ public void readFully(byte[] b) throws IOException
+ {
+ delegate().readFully(b);
+ checksum.update(b);
+ }
+
+ @Override
+ public void readFully(byte[] b, int off, int len) throws IOException
+ {
+ delegate().readFully(b, off, len);
+ checksum.update(b, off, len);
+ }
+
+ @Override
+ public int skipBytes(int n) throws IOException
+ {
+ int skipped = delegate().skipBytes(n);
+ checksum.reset();
+ return skipped;
+ }
+
+ @Override
+ public int readUnsignedByte() throws IOException
+ {
+ int value = delegate().readUnsignedByte();
+ checksum.update(value);
+ return value;
+ }
+
+ private byte writeBuffer[] = new byte[8];
+
+ @Override
+ public long readLong() throws IOException
+ {
+ long v = delegate().readLong();
+ writeBuffer[0] = (byte)(v >>> 56);
+ writeBuffer[1] = (byte)(v >>> 48);
+ writeBuffer[2] = (byte)(v >>> 40);
+ writeBuffer[3] = (byte)(v >>> 32);
+ writeBuffer[4] = (byte)(v >>> 24);
+ writeBuffer[5] = (byte)(v >>> 16);
+ writeBuffer[6] = (byte)(v >>> 8);
+ writeBuffer[7] = (byte)(v >>> 0);
+ checksum.update(writeBuffer);
+ return v;
+ }
+
+ @Override
+ public String readLine() throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/java/org/apache/cassandra/io/util/ChecksumedDataOutputPlus.java b/src/java/org/apache/cassandra/io/util/ChecksumedDataOutputPlus.java
new file mode 100644
index 0000000000..83f571e476
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChecksumedDataOutputPlus.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.io.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.function.Supplier;
+import java.util.zip.Checksum;
+
+public class ChecksumedDataOutputPlus implements DataOutputPlus, Checksumed
+{
+ private final DataOutputPlus delegate;
+ private final Checksum checksum;
+
+ public ChecksumedDataOutputPlus(DataOutputPlus delegate, Checksum checksum)
+ {
+ this.delegate = delegate;
+ this.checksum = checksum;
+ }
+
+ public ChecksumedDataOutputPlus(DataOutputPlus delegate, Supplier fn)
+ {
+ this(delegate, fn.get());
+ }
+
+ public DataOutputPlus delegate()
+ {
+ return delegate;
+ }
+
+ @Override
+ public Checksum checksum()
+ {
+ return checksum;
+ }
+
+ @Override
+ public void write(ByteBuffer buffer) throws IOException
+ {
+ checksum.update(buffer.duplicate());
+ delegate().write(buffer);
+ }
+
+ @Override
+ public void write(int b) throws IOException
+ {
+ checksum.update(b);
+ delegate().write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException
+ {
+ checksum.update(b);
+ delegate().write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ checksum.update(b, off, len);
+ delegate().write(b, off, len);
+ }
+
+ @Override
+ public void writeByte(int v) throws IOException
+ {
+ checksum.update(v);
+ delegate().writeByte(v);
+ }
+
+ private byte writeBuffer[] = new byte[8];
+
+ @Override
+ public void writeLong(long v) throws IOException
+ {
+ writeBuffer[0] = (byte)(v >>> 56);
+ writeBuffer[1] = (byte)(v >>> 48);
+ writeBuffer[2] = (byte)(v >>> 40);
+ writeBuffer[3] = (byte)(v >>> 32);
+ writeBuffer[4] = (byte)(v >>> 24);
+ writeBuffer[5] = (byte)(v >>> 16);
+ writeBuffer[6] = (byte)(v >>> 8);
+ writeBuffer[7] = (byte)(v >>> 0);
+ checksum.update(writeBuffer);
+ delegate().writeLong(v);
+ }
+
+ @Override
+ public void writeUTF(String s) throws IOException
+ {
+ throw new UnsupportedOperationException("TODO");
+ }
+}
diff --git a/src/java/org/apache/cassandra/io/util/ChecksumedFileDataInput.java b/src/java/org/apache/cassandra/io/util/ChecksumedFileDataInput.java
new file mode 100644
index 0000000000..5bdb0b5644
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChecksumedFileDataInput.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.io.util;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+import java.util.zip.Checksum;
+
+public class ChecksumedFileDataInput extends ChecksumedDataInputPlus implements FileDataInput
+{
+ public ChecksumedFileDataInput(FileDataInput delegate, Checksum checksum)
+ {
+ super(delegate, checksum);
+ }
+
+ public ChecksumedFileDataInput(FileDataInput delegate, Supplier fn)
+ {
+ super(delegate, fn);
+ }
+
+ @Override
+ public FileDataInput delegate()
+ {
+ return (FileDataInput) super.delegate();
+ }
+
+ @Override
+ public String getPath()
+ {
+ return delegate().getPath();
+ }
+
+ @Override
+ public boolean isEOF() throws IOException
+ {
+ return delegate().isEOF();
+ }
+
+ @Override
+ public long bytesRemaining() throws IOException
+ {
+ return delegate().bytesRemaining();
+ }
+
+ @Override
+ public void seek(long pos) throws IOException
+ {
+ resetChecksum();
+ delegate().seek(pos);
+ }
+
+ @Override
+ public long getFilePointer()
+ {
+ return delegate().getFilePointer();
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ resetChecksum();
+ delegate().close();
+ }
+
+ @Override
+ public DataPosition mark()
+ {
+ resetChecksum();
+ return delegate().mark();
+ }
+
+ @Override
+ public void reset(DataPosition mark) throws IOException
+ {
+ resetChecksum();
+ delegate().reset(mark);
+ }
+
+ @Override
+ public long bytesPastMark(DataPosition mark)
+ {
+ return delegate().bytesPastMark(mark);
+ }
+}
diff --git a/src/java/org/apache/cassandra/io/util/ChecksumedRandomAccessReader.java b/src/java/org/apache/cassandra/io/util/ChecksumedRandomAccessReader.java
new file mode 100644
index 0000000000..5b3b820740
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChecksumedRandomAccessReader.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.io.util;
+
+import java.util.function.Supplier;
+import java.util.zip.Checksum;
+
+public class ChecksumedRandomAccessReader extends ChecksumedFileDataInput
+{
+ public ChecksumedRandomAccessReader(RandomAccessReader delegate, Checksum checksum)
+ {
+ super(delegate, checksum);
+ }
+
+ public ChecksumedRandomAccessReader(RandomAccessReader delegate, Supplier fn)
+ {
+ super(delegate, fn);
+ }
+
+ @Override
+ public RandomAccessReader delegate()
+ {
+ return (RandomAccessReader) super.delegate();
+ }
+
+ public long length()
+ {
+ return delegate().length();
+ }
+}
diff --git a/src/java/org/apache/cassandra/io/util/ChecksumedSequentialWriter.java b/src/java/org/apache/cassandra/io/util/ChecksumedSequentialWriter.java
new file mode 100644
index 0000000000..e14d99c45f
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/util/ChecksumedSequentialWriter.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.io.util;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+import java.util.zip.Checksum;
+
+import org.apache.cassandra.utils.concurrent.Transactional;
+
+public class ChecksumedSequentialWriter extends ChecksumedDataOutputPlus implements Transactional
+{
+ private static final SequentialWriterOption DEFAULT_WRITER_OPTIONS = SequentialWriterOption.newBuilder().finishOnClose(true).build();
+
+ public ChecksumedSequentialWriter(SequentialWriter delegate, Checksum checksum)
+ {
+ super(delegate, checksum);
+ }
+
+ public ChecksumedSequentialWriter(SequentialWriter delegate, Supplier fn)
+ {
+ super(delegate, fn);
+ }
+
+ public static ChecksumedSequentialWriter open(File file, boolean append, Supplier fn) throws IOException
+ {
+ SequentialWriter writer = new SequentialWriter(file, DEFAULT_WRITER_OPTIONS);
+ if (append)
+ writer.skipBytes(file.length());
+ return new ChecksumedSequentialWriter(writer, fn);
+ }
+
+ @Override
+ public SequentialWriter delegate()
+ {
+ return (SequentialWriter) super.delegate();
+ }
+
+ public long getFilePointer()
+ {
+ return delegate().position();
+ }
+
+ @Override
+ public Throwable commit(Throwable accumulate)
+ {
+ return delegate().commit(accumulate);
+ }
+
+ @Override
+ public Throwable abort(Throwable accumulate)
+ {
+ return delegate().abort(accumulate);
+ }
+
+ @Override
+ public void prepareToCommit()
+ {
+ delegate().prepareToCommit();
+ }
+
+ @Override
+ public void close()
+ {
+ delegate().close();
+ }
+}
diff --git a/src/java/org/apache/cassandra/io/util/DataInputPlus.java b/src/java/org/apache/cassandra/io/util/DataInputPlus.java
index d117c7fe89..428e6eb698 100644
--- a/src/java/org/apache/cassandra/io/util/DataInputPlus.java
+++ b/src/java/org/apache/cassandra/io/util/DataInputPlus.java
@@ -18,6 +18,7 @@
package org.apache.cassandra.io.util;
import java.io.DataInput;
+import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
@@ -98,6 +99,81 @@ public interface DataInputPlus extends DataInput
throw new EOFException("EOF after " + skipped + " bytes out of " + n);
}
+ @Override
+ default byte readByte() throws IOException
+ {
+ return (byte) readUnsignedByte();
+ }
+
+ @Override
+ default boolean readBoolean() throws IOException
+ {
+ return readUnsignedByte() != 0;
+ }
+
+ @Override
+ default short readShort() throws IOException
+ {
+ int ch1 = readUnsignedByte();
+ int ch2 = readUnsignedByte();
+ if ((ch1 | ch2) < 0)
+ throw new EOFException();
+ return (short)((ch1 << 8) + (ch2 << 0));
+ }
+
+ @Override
+ default int readUnsignedShort() throws IOException
+ {
+ int ch1 = readUnsignedByte();
+ int ch2 = readUnsignedByte();
+ if ((ch1 | ch2) < 0)
+ throw new EOFException();
+ return (ch1 << 8) + (ch2 << 0);
+ }
+
+ @Override
+ default char readChar() throws IOException
+ {
+ int ch1 = readUnsignedByte();
+ int ch2 = readUnsignedByte();
+ if ((ch1 | ch2) < 0)
+ throw new EOFException();
+ return (char)((ch1 << 8) + (ch2 << 0));
+ }
+
+ @Override
+ default int readInt() throws IOException
+ {
+ int ch1 = readUnsignedByte();
+ int ch2 = readUnsignedByte();
+ int ch3 = readUnsignedByte();
+ int ch4 = readUnsignedByte();
+ if ((ch1 | ch2 | ch3 | ch4) < 0)
+ throw new EOFException();
+ return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
+ }
+
+ @Override
+ long readLong() throws IOException;
+
+ @Override
+ default float readFloat() throws IOException
+ {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ @Override
+ default double readDouble() throws IOException
+ {
+ return Double.longBitsToDouble(readLong());
+ }
+
+ @Override
+ default String readUTF() throws IOException
+ {
+ return DataInputStream.readUTF(this);
+ }
+
/**
* Wrapper around an InputStream that provides no buffering but can decode varints
*/
diff --git a/src/java/org/apache/cassandra/io/util/DataOutputPlus.java b/src/java/org/apache/cassandra/io/util/DataOutputPlus.java
index 483ee5e1dc..6adf9a0662 100644
--- a/src/java/org/apache/cassandra/io/util/DataOutputPlus.java
+++ b/src/java/org/apache/cassandra/io/util/DataOutputPlus.java
@@ -193,4 +193,65 @@ public interface DataOutputPlus extends DataOutput
{
throw new UnsupportedOperationException();
}
+
+ @Override
+ default void writeBoolean(boolean v) throws IOException
+ {
+ write(v ? 1 : 0);
+ }
+
+ @Override
+ default void writeShort(int v) throws IOException
+ {
+ write((v >>> 8) & 0xFF);
+ write((v >>> 0) & 0xFF);
+ }
+
+ @Override
+ default void writeChar(int v) throws IOException
+ {
+ write((v >>> 8) & 0xFF);
+ write((v >>> 0) & 0xFF);
+ }
+
+ @Override
+ default void writeInt(int v) throws IOException
+ {
+ write((v >>> 24) & 0xFF);
+ write((v >>> 16) & 0xFF);
+ write((v >>> 8) & 0xFF);
+ write((v >>> 0) & 0xFF);
+ }
+
+ @Override
+ default void writeFloat(float v) throws IOException
+ {
+ writeInt(Float.floatToIntBits(v));
+ }
+
+ @Override
+ default void writeDouble(double v) throws IOException
+ {
+ writeLong(Double.doubleToLongBits(v));
+ }
+
+ @Override
+ default void writeBytes(String s) throws IOException
+ {
+ int len = s.length();
+ for (int i = 0 ; i < len ; i++) {
+ write((byte)s.charAt(i));
+ }
+ }
+
+ @Override
+ default void writeChars(String s) throws IOException
+ {
+ int len = s.length();
+ for (int i = 0 ; i < len ; i++) {
+ int v = s.charAt(i);
+ write((v >>> 8) & 0xFF);
+ write((v >>> 0) & 0xFF);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/journal/Journal.java b/src/java/org/apache/cassandra/journal/Journal.java
index 844f660796..eae190a15e 100644
--- a/src/java/org/apache/cassandra/journal/Journal.java
+++ b/src/java/org/apache/cassandra/journal/Journal.java
@@ -195,7 +195,10 @@ public class Journal implements Shutdownable
@Override
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
{
- return false;
+ boolean r = true;
+ r &= allocator.awaitTermination(timeout, units);
+ r &= closer.awaitTermination(timeout, units);
+ return r;
}
/**
diff --git a/src/java/org/apache/cassandra/locator/ReplicaLayout.java b/src/java/org/apache/cassandra/locator/ReplicaLayout.java
index 2e111dc9aa..f961b4051d 100644
--- a/src/java/org/apache/cassandra/locator/ReplicaLayout.java
+++ b/src/java/org/apache/cassandra/locator/ReplicaLayout.java
@@ -24,6 +24,8 @@ import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.LocalPartitioner;
+import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.TableId;
@@ -405,6 +407,10 @@ public abstract class ReplicaLayout>
static EndpointsForToken forLocalStrategyToken(ClusterMetadata metadata, AbstractReplicationStrategy replicationStrategy, Token t)
{
- return replicationStrategy.calculateNaturalReplicas(t, metadata).forToken(t);
+ if (!(t instanceof LocalPartitioner.LocalToken))
+ return replicationStrategy.calculateNaturalReplicas(t, metadata).forToken(t);
+
+ // local tokens use a different partitioner than the global one... so update the ranges
+ return EndpointsForToken.of(t, new Replica(FBUtilities.getBroadcastAddressAndPort(), new Range<>(t, t), true));
}
}
diff --git a/src/java/org/apache/cassandra/schema/SchemaProvider.java b/src/java/org/apache/cassandra/schema/SchemaProvider.java
index daef867951..844acfe311 100644
--- a/src/java/org/apache/cassandra/schema/SchemaProvider.java
+++ b/src/java/org/apache/cassandra/schema/SchemaProvider.java
@@ -35,6 +35,7 @@ import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.KeyspaceNotDefinedException;
import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.UnknownTableException;
import org.apache.cassandra.io.sstable.Descriptor;
@@ -135,6 +136,13 @@ public interface SchemaProvider
@Nullable
TableMetadata getTableMetadata(TableId id);
+ @Nullable
+ default IPartitioner getTablePartitioner(TableId id)
+ {
+ TableMetadata metadata = getTableMetadata(id);
+ return metadata == null ? null : metadata.partitioner;
+ }
+
@Nullable
default TableMetadataRef getTableMetadataRef(TableId id)
{
diff --git a/src/java/org/apache/cassandra/schema/TableId.java b/src/java/org/apache/cassandra/schema/TableId.java
index c042c905ee..def82d1c72 100644
--- a/src/java/org/apache/cassandra/schema/TableId.java
+++ b/src/java/org/apache/cassandra/schema/TableId.java
@@ -36,6 +36,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.tcm.serialization.MetadataSerializer;
import org.apache.cassandra.tcm.serialization.Version;
import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
import org.apache.cassandra.utils.Pair;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -50,7 +51,8 @@ import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
public class TableId implements Comparable
{
public static final long MAGIC = 1956074401491665062L;
- // TODO: should this be a TimeUUID?
+ public static final long EMPTY_SIZE = ObjectSizes.measureDeep(new UUID(0, 0));
+
private final UUID id;
private TableId(UUID id)
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCachingState.java b/src/java/org/apache/cassandra/service/accord/AccordCachingState.java
index d07dcfc87b..304befe83a 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordCachingState.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordCachingState.java
@@ -121,6 +121,11 @@ public class AccordCachingState extends IntrusiveLinkedListNode
return status().isComplete();
}
+ int lastQueriedEstimatedSizeOnHeap()
+ {
+ return lastQueriedEstimatedSizeOnHeap;
+ }
+
int estimatedSizeOnHeap(ToLongFunction estimator)
{
shouldUpdateSize = false; // TODO (expected): probably not the safest place to clear need to compute size
@@ -600,6 +605,12 @@ public class AccordCachingState extends IntrusiveLinkedListNode
return isDone();
}
+ @Override
+ public V get()
+ {
+ return current;
+ }
+
@Override
public State complete()
{
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java
index 4b0722606b..2a67ba656d 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java
@@ -18,8 +18,7 @@
package org.apache.cassandra.service.accord;
-import java.util.Collections;
-import java.util.List;
+import java.util.IdentityHashMap;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
@@ -27,23 +26,19 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
-
import javax.annotation.Nullable;
+import accord.api.Key;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.api.Agent;
import accord.api.DataStore;
-import accord.api.Key;
import accord.api.ProgressLog;
import accord.local.CommandsForKey;
-import accord.impl.CommandsSummary;
import accord.impl.TimestampsForKey;
import accord.local.Command;
import accord.local.CommandStore;
@@ -52,37 +47,28 @@ import accord.local.NodeTimeService;
import accord.local.PreLoadContext;
import accord.local.RedundantBefore;
import accord.local.SafeCommandStore;
-import accord.local.SaveStatus;
-import accord.local.SerializerSupport;
import accord.local.SerializerSupport.MessageProvider;
import accord.messages.Message;
-import accord.primitives.AbstractKeys;
-import accord.primitives.AbstractRanges;
-import accord.primitives.Ballot;
-import accord.primitives.Range;
import accord.primitives.Ranges;
-import accord.primitives.Routable;
import accord.primitives.RoutableKey;
-import accord.primitives.Routables;
-import accord.primitives.Seekables;
import accord.primitives.Timestamp;
import accord.primitives.TxnId;
import accord.utils.Invariants;
import accord.utils.ReducingRangeMap;
import accord.utils.async.AsyncChain;
import accord.utils.async.AsyncChains;
-import accord.utils.async.Observable;
import org.apache.cassandra.cache.CacheSize;
import org.apache.cassandra.concurrent.ExecutorPlus;
import org.apache.cassandra.concurrent.Stage;
-import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.metrics.AccordStateCacheMetrics;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.async.AsyncOperation;
import org.apache.cassandra.service.accord.async.ExecutionOrder;
+import org.apache.cassandra.service.accord.events.CacheEvents;
import org.apache.cassandra.utils.Clock;
-import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
@@ -91,8 +77,12 @@ public class AccordCommandStore extends CommandStore implements CacheSize
{
private static final Logger logger = LoggerFactory.getLogger(AccordCommandStore.class);
+ private static final boolean CHECK_THREADS = CassandraRelevantProperties.TEST_ACCORD_STORE_THREAD_CHECKS_ENABLED.getBoolean();
+
private static long getThreadId(ExecutorService executor)
{
+ if (!CHECK_THREADS)
+ return 0;
try
{
return executor.submit(() -> Thread.currentThread().getId()).get();
@@ -109,7 +99,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
private final long threadId;
public final String loggingId;
- private final AccordJournal journal;
+ private final IJournal journal;
private final ExecutorService executor;
private final ExecutionOrder executionOrder;
private final AccordStateCache stateCache;
@@ -119,7 +109,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
private AsyncOperation> currentOperation = null;
private AccordSafeCommandStore current = null;
private long lastSystemTimestampMicros = Long.MIN_VALUE;
- private CommandsForRanges commandsForRanges = new CommandsForRanges();
+ private final CommandsForRangesLoader commandsForRangesLoader;
public AccordCommandStore(int id,
NodeTimeService time,
@@ -127,12 +117,79 @@ public class AccordCommandStore extends CommandStore implements CacheSize
DataStore dataStore,
ProgressLog.Factory progressLogFactory,
EpochUpdateHolder epochUpdateHolder,
- AccordJournal journal,
+ IJournal journal,
AccordStateCacheMetrics cacheMetrics)
{
this(id, time, agent, dataStore, progressLogFactory, epochUpdateHolder, journal, Stage.READ.executor(), Stage.MUTATION.executor(), cacheMetrics);
}
+ private static void registerJfrListener(int id, AccordStateCache.Instance instance, String name)
+ {
+ if (!DatabaseDescriptor.getAccordStateCacheListenerJFREnabled())
+ return;
+ instance.register(new AccordStateCache.Listener() {
+ private final IdentityHashMap, CacheEvents.Evict> pendingEvicts = new IdentityHashMap<>();
+
+ @Override
+ public void onAdd(AccordCachingState state)
+ {
+ CacheEvents.Add add = new CacheEvents.Add();
+ CacheEvents.Evict evict = new CacheEvents.Evict();
+ if (!add.isEnabled())
+ return;
+ add.begin();
+ evict.begin();
+ add.store = evict.store = id;
+ add.instance = evict.instance = name;
+ add.key = evict.key = state.key().toString();
+ updateMutable(instance, state, add);
+ add.commit();
+ pendingEvicts.put(state, evict);
+ }
+
+ @Override
+ public void onRelease(AccordCachingState state)
+ {
+
+ }
+
+ @Override
+ public void onEvict(AccordCachingState state)
+ {
+ CacheEvents.Evict event = pendingEvicts.remove(state);
+ if (event == null) return;
+ updateMutable(instance, state, event);
+ event.commit();
+ }
+ });
+ }
+
+ private static void updateMutable(AccordStateCache.Instance, ?, ?> instance, AccordCachingState, ?> state, CacheEvents event)
+ {
+ event.status = state.state().status().name();
+
+ event.lastQueriedEstimatedSizeOnHeap = state.lastQueriedEstimatedSizeOnHeap();
+
+ event.instanceAllocated = instance.weightedSize();
+ AccordStateCache.Stats stats = instance.stats();
+ event.instanceStatsQueries = stats.queries;
+ event.instanceStatsHits = stats.hits;
+ event.instanceStatsMisses = stats.misses;
+
+ event.globalSize = instance.size();
+ event.globalReferenced = instance.globalReferencedEntries();
+ event.globalUnreferenced = instance.globalUnreferencedEntries();
+ event.globalCapacity = instance.capacity();
+ event.globalAllocated = instance.globalAllocated();
+
+ stats = instance.globalStats();
+ event.globalStatsQueries = stats.queries;
+ event.globalStatsHits = stats.hits;
+ event.globalStatsMisses = stats.misses;
+
+ event.update();
+ }
+
@VisibleForTesting
public AccordCommandStore(int id,
NodeTimeService time,
@@ -140,7 +197,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
DataStore dataStore,
ProgressLog.Factory progressLogFactory,
EpochUpdateHolder epochUpdateHolder,
- AccordJournal journal,
+ IJournal journal,
ExecutorPlus loadExecutor,
ExecutorPlus saveExecutor,
AccordStateCacheMetrics cacheMetrics)
@@ -160,6 +217,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
this::saveCommand,
this::validateCommand,
AccordObjectSizes::command);
+ registerJfrListener(id, commandCache, "Command");
timestampsForKeyCache =
stateCache.instance(Key.class,
AccordSafeTimestampsForKey.class,
@@ -168,6 +226,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
this::saveTimestampsForKey,
this::validateTimestampsForKey,
AccordObjectSizes::timestampsForKey);
+ registerJfrListener(id, timestampsForKeyCache, "TimestampsForKey");
commandsForKeyCache =
stateCache.instance(Key.class,
AccordSafeCommandsForKey.class,
@@ -177,6 +236,9 @@ public class AccordCommandStore extends CommandStore implements CacheSize
this::validateCommandsForKey,
AccordObjectSizes::commandsForKey,
AccordCachingState::new);
+ registerJfrListener(id, commandsForKeyCache, "CommandsForKey");
+
+ this.commandsForRangesLoader = new CommandsForRangesLoader(this);
AccordKeyspace.loadCommandStoreMetadata(id, ((rejectBefore, durableBefore, redundantBefore, bootstrapBeganAt, safeToRead) -> {
executor.submit(() -> {
@@ -194,7 +256,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
}));
executor.execute(() -> CommandStore.register(this));
- executor.execute(this::loadRangesToCommands);
}
static Factory factory(AccordJournal journal, AccordStateCacheMetrics cacheMetrics)
@@ -203,66 +264,16 @@ public class AccordCommandStore extends CommandStore implements CacheSize
new AccordCommandStore(id, time, agent, dataStore, progressLogFactory, rangesForEpoch, journal, cacheMetrics);
}
- private void loadRangesToCommands()
+ public CommandsForRangesLoader diskCommandsForRanges()
{
- AsyncPromise future = new AsyncPromise<>();
- AccordKeyspace.findAllCommandsByDomain(id, Routable.Domain.Range, ImmutableSet.of("txn_id", "status", "accepted_ballot", "execute_at"), new Observable()
- {
- private CommandsForRanges.Builder builder = new CommandsForRanges.Builder();
-
- @Override
- public void onNext(UntypedResultSet.Row row) throws Exception
- {
- TxnId txnId = AccordKeyspace.deserializeTxnId(row);
- SaveStatus status = AccordKeyspace.deserializeStatus(row);
- Timestamp executeAt = AccordKeyspace.deserializeExecuteAtOrNull(row);
- Ballot accepted = AccordKeyspace.deserializeAcceptedOrNull(row);
-
- MessageProvider messageProvider = journal.makeMessageProvider(txnId);
-
- SerializerSupport.TxnAndDeps txnAndDeps = SerializerSupport.extractTxnAndDeps(unsafeRangesForEpoch(), status, accepted, messageProvider);
- Seekables, ?> keys = txnAndDeps.txn.keys();
- if (keys.domain() != Routable.Domain.Range)
- throw new AssertionError(String.format("Txn keys are not range for %s", txnAndDeps.txn));
- Ranges ranges = (Ranges) keys;
-
- List dependsOn = txnAndDeps.deps == null ? Collections.emptyList() : txnAndDeps.deps.txnIds();
- builder.put(txnId, ranges, status, executeAt, dependsOn);
- }
-
- @Override
- public void onError(Throwable t)
- {
- builder = null;
- future.tryFailure(t);
- }
-
- @Override
- public void onCompleted()
- {
- CommandsForRanges result = this.builder.build();
- builder = null;
- future.trySuccess(result);
- }
- });
- try
- {
- commandsForRanges = future.get();
- logger.debug("Loaded {} intervals", commandsForRanges.size());
- }
- catch (InterruptedException e)
- {
- throw new UncheckedInterruptedException(e);
- }
- catch (ExecutionException e)
- {
- throw new RuntimeException(e.getCause());
- }
+ return commandsForRangesLoader;
}
@Override
public boolean inStore()
{
+ if (!CHECK_THREADS)
+ return true;
return Thread.currentThread().getId() == threadId;
}
@@ -298,6 +309,8 @@ public class AccordCommandStore extends CommandStore implements CacheSize
public void checkNotInStoreThread()
{
+ if (!CHECK_THREADS)
+ return;
Invariants.checkState(!inStore());
}
@@ -320,7 +333,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
{
return commandsForKeyCache;
}
-
Command loadCommand(TxnId txnId)
{
return AccordKeyspace.loadCommand(this, txnId);
@@ -467,13 +479,16 @@ public class AccordCommandStore extends CommandStore implements CacheSize
public AccordSafeCommandStore beginOperation(PreLoadContext preLoadContext,
Map commands,
NavigableMap timestampsForKeys,
- NavigableMap commandsForKeys)
+ NavigableMap commandsForKeys,
+ @Nullable AccordSafeCommandsForRanges commandsForRanges)
{
Invariants.checkState(current == null);
commands.values().forEach(AccordSafeState::preExecute);
commandsForKeys.values().forEach(AccordSafeState::preExecute);
timestampsForKeys.values().forEach(AccordSafeState::preExecute);
- current = new AccordSafeCommandStore(preLoadContext, commands, timestampsForKeys, commandsForKeys, this);
+ if (commandsForRanges != null)
+ commandsForRanges.preExecute();
+ current = new AccordSafeCommandStore(preLoadContext, commands, timestampsForKeys, commandsForKeys, commandsForRanges, this);
return current;
}
@@ -489,46 +504,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
current = null;
}
- O mapReduceForRange(Routables> keysOrRanges, Ranges slice, BiFunction map, O accumulate)
- {
- keysOrRanges = keysOrRanges.slice(slice, Routables.Slice.Minimal);
- switch (keysOrRanges.domain())
- {
- case Key:
- {
- AbstractKeys keys = (AbstractKeys) keysOrRanges;
- for (CommandsSummary summary : commandsForRanges.search(keys))
- accumulate = map.apply(summary, accumulate);
- }
- break;
- case Range:
- {
- AbstractRanges ranges = (AbstractRanges) keysOrRanges;
- for (Range range : ranges)
- {
- CommandsSummary summary = commandsForRanges.search(range);
- if (summary == null)
- continue;
- accumulate = map.apply(summary, accumulate);
- }
- }
- break;
- default:
- throw new AssertionError("Unknown domain: " + keysOrRanges.domain());
- }
- return accumulate;
- }
-
- CommandsForRanges commandsForRanges()
- {
- return commandsForRanges;
- }
-
- CommandsForRanges.Updater updateRanges()
- {
- return commandsForRanges.update();
- }
-
public void abortCurrentOperation()
{
current = null;
@@ -576,13 +551,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
AccordKeyspace.updateRedundantBefore(this, newRedundantBefore);
}
- @Override
- public void markShardDurable(SafeCommandStore safeStore, TxnId globalSyncId, Ranges ranges)
- {
- super.markShardDurable(safeStore, globalSyncId, ranges);
- commandsForRanges.prune(globalSyncId, ranges);
- }
-
public NavigableMap bootstrapBeganAt() { return super.bootstrapBeganAt(); }
public NavigableMap safeToRead() { return super.safeToRead(); }
diff --git a/src/java/org/apache/cassandra/service/accord/AccordJournal.java b/src/java/org/apache/cassandra/service/accord/AccordJournal.java
index 26e09ada14..0562da1139 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordJournal.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordJournal.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.service.accord;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
@@ -27,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.zip.Checksum;
@@ -99,6 +101,7 @@ import org.apache.cassandra.service.accord.serializers.PreacceptSerializers;
import org.apache.cassandra.service.accord.serializers.RecoverySerializers;
import org.apache.cassandra.service.accord.serializers.SetDurableSerializers;
import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.ExecutorUtils;
import org.apache.cassandra.utils.concurrent.Semaphore;
import org.jctools.queues.SpscLinkedQueue;
@@ -143,7 +146,7 @@ import static org.apache.cassandra.utils.CollectionSerializers.serializedListSiz
import static org.apache.cassandra.utils.concurrent.Semaphore.newSemaphore;
import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize;
-public class AccordJournal implements Shutdownable
+public class AccordJournal implements IJournal, Shutdownable
{
private static final Logger logger = LoggerFactory.getLogger(AccordJournal.class);
@@ -262,8 +265,15 @@ public class AccordJournal implements Shutdownable
@Override
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
{
- // TODO (expected, other)
- return true;
+ try
+ {
+ ExecutorUtils.awaitTermination(timeout, units, Arrays.asList(journal, frameAggregator, frameApplicator));
+ return true;
+ }
+ catch (TimeoutException e)
+ {
+ return false;
+ }
}
/**
@@ -296,6 +306,7 @@ public class AccordJournal implements Shutdownable
}
@VisibleForTesting
+ @Override
public void appendMessageBlocking(Message message)
{
Type type = Type.fromMessageType(message.type());
@@ -1061,7 +1072,7 @@ public class AccordJournal implements Shutdownable
* Once written, the frame record is submitted to {@link FrameApplicator}, which will process all the framed
* requests once the frame has been flushed to disk.
*/
- private final class FrameAggregator implements Interruptible.Task
+ private final class FrameAggregator implements Interruptible.Task, Shutdownable
{
/* external MPSC pending request queue */
private final ManyToOneConcurrentLinkedQueue unframedRequests = new ManyToOneConcurrentLinkedQueue<>();
@@ -1091,9 +1102,26 @@ public class AccordJournal implements Shutdownable
executor = executorFactory().infiniteLoop("AccordJournal#FrameAggregator", this, SAFE, NON_DAEMON, SYNCHRONIZED);
}
- void shutdown()
+ @Override
+ public boolean isTerminated() {
+ return executor == null || executor.isTerminated();
+ }
+
+ @Override
+ public void shutdown()
{
- executor.shutdown();
+ if (executor != null)
+ executor.shutdown();
+ }
+
+ @Override
+ public Object shutdownNow() {
+ return executor == null ? null : executor.shutdownNow();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException {
+ return executor == null || executor.awaitTermination(timeout, units);
}
@Override
@@ -1168,7 +1196,7 @@ public class AccordJournal implements Shutdownable
* Gets the aggregated frames containing previously written requests/messages,
* and sorts and "applies" them once part of the journal that fully contains them is flushed.
*/
- private final class FrameApplicator implements Runnable
+ private final class FrameApplicator implements Runnable, Shutdownable
{
/** external SPSC written frame queue */
private final SpscLinkedQueue newFrames = new SpscLinkedQueue<>();
@@ -1199,9 +1227,26 @@ public class AccordJournal implements Shutdownable
executor = executorFactory().sequential("AccordJournal#FrameApplicator");
}
- void shutdown()
+ @Override
+ public boolean isTerminated() {
+ return executor == null || executor.isTerminated();
+ }
+
+ @Override
+ public void shutdown()
{
- executor.shutdown();
+ if (executor != null)
+ executor.shutdown();
+ }
+
+ @Override
+ public Object shutdownNow() {
+ return executor == null ? null : executor.shutdownNow();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException {
+ return executor == null || executor.awaitTermination(timeout, units);
}
@Override
@@ -1334,8 +1379,8 @@ public class AccordJournal implements Shutdownable
/*
* Message provider implementation
*/
-
- SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId)
+ @Override
+ public SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId)
{
return LOG_MESSAGE_PROVIDER ? new LoggingMessageProvider(txnId, new MessageProvider(txnId)) : new MessageProvider(txnId);
}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java
index da832ae2cb..7138eb9ab2 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java
@@ -27,7 +27,10 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -37,6 +40,7 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -118,17 +122,23 @@ import org.apache.cassandra.db.rows.Row.Deletion;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.transform.FilteredPartitions;
import org.apache.cassandra.dht.ByteOrderedPartitioner;
+import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.LocalPartitioner;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.index.accord.RouteIndex;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.LocalVersionedSerializer;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.Indexes;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.schema.SchemaProvider;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.Tables;
@@ -138,7 +148,9 @@ import org.apache.cassandra.schema.Views;
import org.apache.cassandra.serializers.UUIDSerializer;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.accord.AccordConfigurationService.SyncStatus;
+import org.apache.cassandra.service.accord.api.AccordRoutingKey;
import org.apache.cassandra.service.accord.api.PartitionKey;
+import org.apache.cassandra.service.accord.serializers.AccordRoutingKeyByteSource;
import org.apache.cassandra.service.accord.serializers.CommandSerializers;
import org.apache.cassandra.service.accord.serializers.CommandStoreSerializers;
import org.apache.cassandra.service.accord.serializers.CommandsForKeySerializer;
@@ -192,6 +204,15 @@ public class AccordKeyspace
private static final ClusteringIndexFilter FULL_PARTITION = new ClusteringIndexSliceFilter(Slices.ALL, false);
+ //TODO (now, performance): should this be partitioner rather than TableId? As of this patch distributed tables should only have 1 partitioner...
+ private static final ConcurrentMap TABLE_SERIALIZERS = new ConcurrentHashMap<>();
+
+ // Schema needs all system keyspace, and this is a system keyspace! So can not touch schema in init
+ private static class SchemaHolder
+ {
+ private static SchemaProvider schema = Objects.requireNonNull(Schema.instance);
+ }
+
private enum TokenType
{
Murmur3((byte) 1),
@@ -216,7 +237,7 @@ public class AccordKeyspace
}
// TODO: store timestamps as blobs (confirm there are no negative numbers, or offset)
- private static final TableMetadata Commands =
+ public static final TableMetadata Commands =
parse(COMMANDS,
"accord commands",
"CREATE TABLE %s ("
@@ -234,10 +255,13 @@ public class AccordKeyspace
+ "PRIMARY KEY((store_id, domain, txn_id))"
+ ')')
.partitioner(new LocalPartitioner(CompositeType.getInstance(Int32Type.instance, Int32Type.instance, TIMESTAMP_TYPE)))
+ .indexes(Indexes.builder()
+ .add(IndexMetadata.fromSchemaMetadata("route", IndexMetadata.Kind.CUSTOM, ImmutableMap.of("class_name", RouteIndex.class.getCanonicalName(), "target", "route")))
+ .build())
.build();
// TODO: naming is not very clearly distinct from the base serializers
- private static class LocalVersionedSerializers
+ public static class LocalVersionedSerializers
{
static final LocalVersionedSerializer> route = localSerializer(KeySerializers.route);
static final LocalVersionedSerializer listeners = localSerializer(ListenerSerializers.listener);
@@ -266,8 +290,8 @@ public class AccordKeyspace
{
static final ClusteringComparator keyComparator = Commands.partitionKeyAsClusteringComparator();
static final CompositeType partitionKeyType = (CompositeType) Commands.partitionKeyType;
- static final ColumnMetadata txn_id = getColumn(Commands, "txn_id");
- static final ColumnMetadata store_id = getColumn(Commands, "store_id");
+ public static final ColumnMetadata txn_id = getColumn(Commands, "txn_id");
+ public static final ColumnMetadata store_id = getColumn(Commands, "store_id");
public static final ColumnMetadata status = getColumn(Commands, "status");
public static final ColumnMetadata route = getColumn(Commands, "route");
public static final ColumnMetadata durability = getColumn(Commands, "durability");
@@ -298,6 +322,12 @@ public class AccordKeyspace
return Int32Type.instance.compose(partitionKeyComponents[store_id.position()]);
}
+ public static int getStoreId(DecoratedKey pk)
+ {
+ ByteBuffer[] array = splitPartitionKey(pk);
+ return getStoreId(array);
+ }
+
public static TxnId getTxnId(ByteBuffer[] partitionKeyComponents)
{
return deserializeTimestampOrNull(partitionKeyComponents[txn_id.position()], ByteBufferAccessor.instance, TxnId::fromBits);
@@ -409,6 +439,8 @@ public class AccordKeyspace
}
}
+ //TODO (now, performance): do we actually care about the sort ordering? We don't do range scans on this table
+ //TODO (now, performance): should we remove key_token? We don't need it so its just added space
private static final TableMetadata TimestampsForKeys =
parse(TIMESTAMPS_FOR_KEY,
"accord timestamps per key",
@@ -578,6 +610,11 @@ public class AccordKeyspace
return Int32Type.instance.compose(partitionKeyComponents[store_id.position()]);
}
+ public PartitionKey getKey(DecoratedKey key)
+ {
+ return getKey(splitPartitionKey(key));
+ }
+
public PartitionKey getKey(ByteBuffer[] partitionKeyComponents)
{
return deserializeKey(partitionKeyComponents[key.position()]);
@@ -860,12 +897,13 @@ public class AccordKeyspace
return value;
}
- private static ByteBuffer serializeKey(PartitionKey key)
+ @VisibleForTesting
+ public static ByteBuffer serializeKey(PartitionKey key)
{
return KEY_TYPE.pack(UUIDSerializer.instance.serialize(key.table().asUUID()), key.partitionKey().getKey());
}
- private static ByteBuffer serializeTimestamp(Timestamp timestamp)
+ public static ByteBuffer serializeTimestamp(Timestamp timestamp)
{
return TIMESTAMP_TYPE.pack(bytes(timestamp.msb), bytes(timestamp.lsb), bytes(timestamp.node.id));
}
@@ -1097,15 +1135,15 @@ public class AccordKeyspace
}
public static void findAllKeysBetween(int commandStore,
- Token start, boolean startInclusive,
- Token end, boolean endInclusive,
+ AccordRoutingKey start, boolean startInclusive,
+ AccordRoutingKey end, boolean endInclusive,
Observable callback)
{
//TODO (optimize) : CQL doesn't look smart enough to only walk Index.db, and ends up walking the Data.db file for each row in the partitions found (for frequent keys, this cost adds up)
// it would be possible to find all SSTables that "could" intersect this range, then have a merge iterator over the Index.db (filtered to the range; index stores partition liveness)...
KeysBetween work = new KeysBetween(commandStore,
- AccordKeyspace.serializeToken(start), startInclusive,
- AccordKeyspace.serializeToken(end), endInclusive,
+ AccordKeyspace.serializeRoutingKey(start), startInclusive,
+ AccordKeyspace.serializeRoutingKey(end), endInclusive,
ImmutableSet.of("key"),
Stage.READ.executor(), Observable.distinct(callback).map(AccordKeyspace::deserializeKey));
work.schedule();
@@ -1131,14 +1169,14 @@ public class AccordKeyspace
this.start = start;
this.end = end;
- String selection = selection(TimestampsForKeys, requiredColumns, COLUMNS_FOR_ITERATION);
+ String selection = selection(CommandsForKeys, requiredColumns, COLUMNS_FOR_ITERATION);
this.cqlFirst = format("SELECT DISTINCT %s\n" +
"FROM %s\n" +
"WHERE store_id = ?\n" +
(startInclusive ? " AND key_token >= ?\n" : " AND key_token > ?\n") +
(endInclusive ? " AND key_token <= ?\n" : " AND key_token < ?\n") +
"ALLOW FILTERING",
- selection, TimestampsForKeys);
+ selection, CommandsForKeys);
this.cqlContinue = format("SELECT DISTINCT %s\n" +
"FROM %s\n" +
"WHERE store_id = ?\n" +
@@ -1146,7 +1184,7 @@ public class AccordKeyspace
" AND key > ?\n" +
(endInclusive ? " AND key_token <= ?\n" : " AND key_token < ?\n") +
"ALLOW FILTERING",
- selection, TimestampsForKeys);
+ selection, CommandsForKeys);
}
@Override
@@ -1216,11 +1254,16 @@ public class AccordKeyspace
return Status.Durability.values()[row.getInt("durability", 0)];
}
- private static Route> deserializeRouteOrNull(ByteBuffer bytes) throws IOException
+ public static Route> deserializeRouteOrNull(ByteBuffer bytes) throws IOException
{
return bytes != null && !ByteBufferAccessor.instance.isEmpty(bytes) ? deserialize(bytes, LocalVersionedSerializers.route) : null;
}
+ public static ByteBuffer serializeRoute(Route> route) throws IOException
+ {
+ return serialize(route, LocalVersionedSerializers.route);
+ }
+
private static Route> deserializeRouteOrNull(UntypedResultSet.Row row) throws IOException
{
return deserializeRouteOrNull(row.getBlob("route"));
@@ -1303,10 +1346,10 @@ public class AccordKeyspace
TableId tableId = TableId.fromUUID(UUIDSerializer.instance.deserialize(split.get(0)));
ByteBuffer key = split.get(1);
- TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
- if (metadata == null)
+ IPartitioner partitioner = SchemaHolder.schema.getTablePartitioner(tableId);
+ if (partitioner == null)
throw new IllegalStateException("Table with id " + tableId + " could not be found; was it deleted?");
- return new PartitionKey(tableId, metadata.partitioner.decorateKey(key));
+ return new PartitionKey(tableId, partitioner.decorateKey(key));
}
public static PartitionKey deserializeKey(UntypedResultSet.Row row)
@@ -1390,16 +1433,36 @@ public class AccordKeyspace
private static DecoratedKey makeKey(CommandsForKeyAccessor accessor, int storeId, PartitionKey key)
{
- Token token = key.token();
ByteBuffer pk = accessor.keyComparator.make(storeId,
- serializeToken(token),
+ serializeRoutingKey(key.toUnseekable()),
serializeKey(key)).serializeAsPartitionKey();
return accessor.table.partitioner.decorateKey(pk);
}
+ @VisibleForTesting
+ public static ByteBuffer serializeRoutingKey(AccordRoutingKey routingKey)
+ {
+ AccordRoutingKeyByteSource.Serializer serializer = TABLE_SERIALIZERS.computeIfAbsent(routingKey.table(), ignore -> {
+ IPartitioner partitioner;
+ if (routingKey.kindOfRoutingKey() == AccordRoutingKey.RoutingKeyKind.TOKEN)
+ partitioner = routingKey.asTokenKey().token().getPartitioner();
+ else
+ partitioner = SchemaHolder.schema.getTablePartitioner(routingKey.table());
+ return AccordRoutingKeyByteSource.variableLength(partitioner);
+ });
+ byte[] bytes = serializer.serialize(routingKey);
+ return ByteBuffer.wrap(bytes);
+ }
+
private static PartitionUpdate getCommandsForKeyPartitionUpdate(int storeId, PartitionKey key, CommandsForKey commandsForKey, long timestampMicros)
{
ByteBuffer bytes = CommandsForKeySerializer.toBytesWithoutKey(commandsForKey);
+ return getCommandsForKeyPartitionUpdate(storeId, key, timestampMicros, bytes);
+ }
+
+ @VisibleForTesting
+ public static PartitionUpdate getCommandsForKeyPartitionUpdate(int storeId, PartitionKey key, long timestampMicros, ByteBuffer bytes)
+ {
return singleRowUpdate(CommandsForKeysAccessor.table,
makeKey(CommandsForKeysAccessor, storeId, key),
singleCellRow(Clustering.EMPTY, BufferCell.live(CommandsForKeysAccessor.data, timestampMicros, bytes)));
@@ -1832,4 +1895,19 @@ public class AccordKeyspace
consumer.accept(rejectBefore, durableBefore, redundantBefore, bootstrapBeganAt, safeToRead);
}
+ @VisibleForTesting
+ public static void unsafeSetSchema(SchemaProvider provider)
+ {
+ SchemaHolder.schema = provider;
+ }
+
+ @VisibleForTesting
+ public static void unsafeClear()
+ {
+ for (ColumnFamilyStore store : Keyspace.open(SchemaConstants.ACCORD_KEYSPACE_NAME).getColumnFamilyStores())
+ store.truncateBlockingWithoutSnapshot();
+ TABLE_SERIALIZERS.clear();
+ SchemaHolder.schema = Schema.instance;
+ }
+
}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java
index 73827a3771..d72644811a 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java
@@ -222,7 +222,7 @@ public class AccordMessageSink implements MessageSink
Preconditions.checkNotNull(verb, "Verb is null for type %s", request.type());
Message message = Message.out(verb, request);
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(to);
- logger.debug("Sending {} {} to {}", verb, message.payload, endpoint);
+ logger.trace("Sending {} {} to {}", verb, message.payload, endpoint);
messaging.send(message, endpoint);
}
@@ -233,7 +233,7 @@ public class AccordMessageSink implements MessageSink
Preconditions.checkNotNull(verb, "Verb is null for type %s", request.type());
Message message = Message.out(verb, request);
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(to);
- logger.debug("Sending {} {} to {}", verb, message.payload, endpoint);
+ logger.trace("Sending {} {} to {}", verb, message.payload, endpoint);
messaging.sendWithCallback(message, endpoint, new AccordCallback<>(executor, (Callback) callback, endpointMapper));
}
@@ -246,7 +246,7 @@ public class AccordMessageSink implements MessageSink
responseMsg = responseMsg.withFlag(MessageFlag.NOT_FINAL);
checkReplyType(reply, respondTo);
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(replyingToNode);
- logger.debug("Replying {} {} to {}", responseMsg.verb(), responseMsg.payload, endpoint);
+ logger.trace("Replying {} {} to {}", responseMsg.verb(), responseMsg.payload, endpoint);
messaging.send(responseMsg, endpoint);
}
@@ -256,7 +256,7 @@ public class AccordMessageSink implements MessageSink
ResponseContext respondTo = (ResponseContext) replyContext;
Message> responseMsg = Message.failureResponse(RequestFailureReason.UNKNOWN, failure, respondTo);
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(replyingToNode);
- logger.debug("Replying with failure {} {} to {}", responseMsg.verb(), responseMsg.payload, endpoint);
+ logger.trace("Replying with failure {} {} to {}", responseMsg.verb(), responseMsg.payload, endpoint);
messaging.send(responseMsg, endpoint);
}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java b/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java
index 1c497cc5c9..9f4776c2e9 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordSafeCommandStore.java
@@ -18,12 +18,9 @@
package org.apache.cassandra.service.accord;
-import java.util.Collections;
-import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.function.BiFunction;
-
import javax.annotation.Nullable;
import accord.api.Agent;
@@ -33,12 +30,13 @@ import accord.api.ProgressLog;
import accord.impl.AbstractSafeCommandStore;
import accord.local.CommandsForKey;
import accord.impl.CommandsSummary;
-import accord.local.Command;
import accord.local.CommandStores.RangesForEpoch;
import accord.local.NodeTimeService;
import accord.local.PreLoadContext;
import accord.primitives.AbstractKeys;
+import accord.primitives.AbstractRanges;
import accord.primitives.Deps;
+import accord.primitives.Range;
import accord.primitives.Ranges;
import accord.primitives.Routables;
import accord.primitives.Seekables;
@@ -46,27 +44,27 @@ import accord.primitives.Timestamp;
import accord.primitives.Txn;
import accord.primitives.TxnId;
-import static accord.primitives.Routable.Domain.Range;
-
public class AccordSafeCommandStore extends AbstractSafeCommandStore
{
private final Map commands;
private final NavigableMap commandsForKeys;
private final NavigableMap timestampsForKeys;
+ private final @Nullable AccordSafeCommandsForRanges commandsForRanges;
private final AccordCommandStore commandStore;
private final RangesForEpoch ranges;
- CommandsForRanges.Updater rangeUpdates = null;
public AccordSafeCommandStore(PreLoadContext context,
Map commands,
NavigableMap timestampsForKey,
NavigableMap commandsForKey,
+ @Nullable AccordSafeCommandsForRanges commandsForRanges,
AccordCommandStore commandStore)
{
super(context);
this.commands = commands;
this.timestampsForKeys = timestampsForKey;
this.commandsForKeys = commandsForKey;
+ this.commandsForRanges = commandsForRanges;
this.commandStore = commandStore;
this.ranges = commandStore.updateRangesForEpoch();
}
@@ -185,27 +183,57 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore {
- if (commandsForRanges.containsLocally(txnId))
- return;
+ for (int i = 0; i < deps.rangeDeps.rangeCount(); i++)
+ {
+ Range range = deps.rangeDeps.range(i);
+ if (!allRanges.intersects(range))
+ continue;
+ deps.rangeDeps.forEach(range, txnId -> {
+ // TODO (desired, efficiency): this can be made more efficient by batching by epoch
+ if (ranges.coordinates(txnId).intersects(range))
+ return; // already coordinates, no need to replicate
+ if (!ranges.allBefore(txnId.epoch()).intersects(range))
+ return;
- Ranges ranges = deps.rangeDeps.ranges(txnId);
- if (this.ranges.coordinates(txnId).intersects(ranges))
- return; // already coordinates, no need to replicate
- if (!this.ranges.allBefore(txnId.epoch()).intersects(ranges))
- return;
-
- updateRanges().mergeRemote(txnId, ranges.slice(allRanges), Ranges::with);
- });
+ commandStore.diskCommandsForRanges().mergeHistoricalTransaction(txnId, Ranges.single(range).slice(allRanges), Ranges::with);
+ });
+ }
}
private O mapReduce(Routables> keysOrRanges, Ranges slice, BiFunction map, O accumulate)
{
- accumulate = commandStore.mapReduceForRange(keysOrRanges, slice, map, accumulate);
+ accumulate = mapReduceForRange(keysOrRanges, slice, map, accumulate);
return mapReduceForKey(keysOrRanges, slice, map, accumulate);
}
+ private O mapReduceForRange(Routables> keysOrRanges, Ranges slice, BiFunction map, O accumulate)
+ {
+ if (commandsForRanges == null)
+ return accumulate;
+ switch (keysOrRanges.domain())
+ {
+ case Key:
+ {
+ AbstractKeys keys = (AbstractKeys) keysOrRanges.slice(slice, Routables.Slice.Minimal);
+ if (!commandsForRanges.ranges().intersects(keys))
+ return accumulate;
+ accumulate = map.apply(commandsForRanges.current(), accumulate);
+ }
+ break;
+ case Range:
+ {
+ AbstractRanges ranges = (AbstractRanges) keysOrRanges.slice(slice, Routables.Slice.Minimal);
+ if (!commandsForRanges.ranges().intersects(ranges))
+ return accumulate;
+ accumulate = map.apply(commandsForRanges.current(), accumulate);
+ }
+ break;
+ default:
+ throw new AssertionError("Unknown domain: " + keysOrRanges.domain());
+ }
+ return accumulate;
+ }
+
private O mapReduceForKey(Routables> keysOrRanges, Ranges slice, BiFunction map, O accumulate)
{
switch (keysOrRanges.domain())
@@ -260,33 +288,6 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore keysOrRanges = updated.keysOrRanges();
- if (keysOrRanges == null) keysOrRanges = prev.keysOrRanges();
- if (keysOrRanges == null)
- return;
-
- List waitingOn;
- // TODO (required): this is faulty: we cannot simply save the raw transaction ids, as they may be for other ranges
- if (updated.partialDeps() == null) waitingOn = Collections.emptyList();
- else waitingOn = updated.partialDeps().txnIds();
- updateRanges().put(updated.txnId(), (Ranges)keysOrRanges, updated.saveStatus(), updated.executeAt(), waitingOn);
- }
- }
-
- protected CommandsForRanges.Updater updateRanges()
- {
- if (rangeUpdates == null)
- rangeUpdates = commandStore.updateRanges();
- return rangeUpdates;
- }
-
@Override
protected void invalidateSafeState()
{
@@ -297,14 +298,14 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore commands,
Map timestampsForKey,
- Map commandsForKeys
- )
+ Map commandsForKeys,
+ @Nullable AccordSafeCommandsForRanges commandsForRanges)
{
postExecute();
commands.values().forEach(AccordSafeState::postExecute);
timestampsForKey.values().forEach(AccordSafeState::postExecute);
commandsForKeys.values().forEach(AccordSafeState::postExecute);
- if (rangeUpdates != null)
- rangeUpdates.apply();
+ if (commandsForRanges != null)
+ commandsForRanges.postExecute();
}
}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordSafeCommandsForRanges.java b/src/java/org/apache/cassandra/service/accord/AccordSafeCommandsForRanges.java
new file mode 100644
index 0000000000..42fb0f6ef1
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordSafeCommandsForRanges.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.service.accord;
+
+import java.util.NavigableMap;
+import java.util.Objects;
+
+import accord.primitives.Range;
+import accord.primitives.Ranges;
+import accord.primitives.TxnId;
+import accord.utils.async.AsyncChains;
+import accord.utils.async.AsyncResult;
+import org.apache.cassandra.utils.Pair;
+
+public class AccordSafeCommandsForRanges implements AccordSafeState
+{
+ private final AsyncResult>> chain;
+ private final Ranges ranges;
+ private boolean invalidated;
+ private CommandsForRanges original, current;
+
+ public AccordSafeCommandsForRanges(Ranges ranges, AsyncResult>> chain)
+ {
+ this.ranges = ranges;
+ this.chain = chain;
+ }
+
+ public Ranges ranges()
+ {
+ return ranges;
+ }
+
+ @Override
+ public CommandsForRanges current()
+ {
+ checkNotInvalidated();
+ return current;
+ }
+
+ @Override
+ public void invalidate()
+ {
+ invalidated = true;
+ }
+
+ @Override
+ public boolean invalidated()
+ {
+ return invalidated;
+ }
+
+ @Override
+ public void set(CommandsForRanges update)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CommandsForRanges original()
+ {
+ checkNotInvalidated();
+ return original;
+ }
+
+ @Override
+ public void preExecute()
+ {
+ checkNotInvalidated();
+ Pair> pair = AsyncChains.getUnchecked(chain);
+ pair.left.close();
+ pair.left.get().entrySet().forEach(e -> pair.right.put(e.getKey(), e.getValue()));
+ current = original = new CommandsForRanges(ranges, pair.right);
+ }
+
+ @Override
+ public void postExecute()
+ {
+ checkNotInvalidated();
+ }
+
+ @Override
+ public AccordCachingState global()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AccordSafeCommandsForRanges that = (AccordSafeCommandsForRanges) o;
+ return Objects.equals(original, that.original) && Objects.equals(current, that.current);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(original, current);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "AccordSafeCommandsForRange{" +
+ "chain=" + chain +
+ ", invalidated=" + invalidated +
+ ", original=" + original +
+ ", current=" + current +
+ '}';
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordService.java b/src/java/org/apache/cassandra/service/accord/AccordService.java
index 8378f65c91..04ef7e6355 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordService.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordService.java
@@ -146,6 +146,12 @@ public class AccordService implements IAccordService, Shutdownable
throw new UnsupportedOperationException("No accord barriers should be executed when accord.enabled = false in cassandra.yaml");
}
+ @Override
+ public long barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException
+ {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public @Nonnull TxnResult coordinate(@Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, @Nonnull Dispatcher.RequestTime requestTime)
{
@@ -213,6 +219,12 @@ public class AccordService implements IAccordService, Shutdownable
instance = null;
}
+ @VisibleForTesting
+ public static void unsafeSetNoop()
+ {
+ instance = NOOP_SERVICE;
+ }
+
public static boolean isSetup()
{
return instance != null;
diff --git a/src/java/org/apache/cassandra/service/accord/AccordStateCache.java b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java
index 1196089d62..b76d63b830 100644
--- a/src/java/org/apache/cassandra/service/accord/AccordStateCache.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java
@@ -17,8 +17,11 @@
*/
package org.apache.cassandra.service.accord;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -68,9 +71,9 @@ public class AccordStateCache extends IntrusiveLinkedList> instances = ImmutableList.of();
@@ -83,6 +86,7 @@ public class AccordStateCache extends IntrusiveLinkedList maxSizeInBytes)
{
AccordCachingState, ?> node = iter.next();
- checkState(node.references == 0);
+ maybeEvict(node);
+ }
+ }
- /*
- * TODO (expected, efficiency):
- * can this be reworked so we're not skipping unevictable nodes everytime we try to evict?
- */
- Status status = node.status(); // status() call completes (if completeable)
- switch (status)
- {
- default: throw new IllegalStateException("Unhandled status " + status);
- case LOADED:
- unlink(node);
- evict(node);
- break;
- case MODIFIED:
- // schedule a save to disk, keep linked and in the cache map
- Instance, ?, ?> instance = instanceForNode(node);
- node.save(saveExecutor, instance.saveFunction);
- maybeUpdateSize(node, instance.heapEstimator);
- break;
- case SAVING:
- // skip over until completes to LOADED or FAILED_TO_SAVE
- break;
- case FAILED_TO_SAVE:
- // TODO (consider): panic when a save fails
- // permanently unlink, but keep in the map
- unlink(node);
- }
+ @VisibleForTesting
+ public boolean maybeEvict(AccordCachingState, ?> node)
+ {
+ checkState(node.references == 0);
+
+ /*
+ * TODO (expected, efficiency):
+ * can this be reworked so we're not skipping unevictable nodes everytime we try to evict?
+ */
+ Status status = node.status(); // status() call completes (if completeable)
+ switch (status)
+ {
+ default: throw new IllegalStateException("Unhandled status " + status);
+ case LOADED:
+ unlink(node);
+ evict(node);
+ return true;
+ case MODIFIED:
+ // schedule a save to disk, keep linked and in the cache map
+ Instance, ?, ?> instance = instanceForNode(node);
+ node.save(saveExecutor, instance.saveFunction);
+ maybeUpdateSize(node, instance.heapEstimator);
+ return false;
+ case SAVING:
+ // skip over until completes to LOADED or FAILED_TO_SAVE
+ return false;
+ case FAILED_TO_SAVE:
+ // TODO (consider): panic when a save fails
+ // permanently unlink, but keep in the map
+ unlink(node);
+ return false;
}
}
@@ -189,12 +200,14 @@ public class AccordStateCache extends IntrusiveLinkedList self = instances.get(node.index).cache.remove(node.key());
checkState(self == node, "Leaked node detected; was attempting to remove %s but cache had %s", node, self);
+ if (instance.listeners != null)
+ instance.listeners.forEach(l -> l.onEvict((AccordCachingState) node));
}
else
{
@@ -240,7 +253,19 @@ public class AccordStateCache extends IntrusiveLinkedList> implements CacheSize
+ public Collection> instances()
+ {
+ return instances;
+ }
+
+ public interface Listener
+ {
+ default void onAdd(AccordCachingState state) {}
+ default void onRelease(AccordCachingState state) {}
+ default void onEvict(AccordCachingState state) {}
+ }
+
+ public class Instance> implements CacheSize, Iterable>
{
private final int index;
private final Class keyClass;
@@ -257,6 +282,7 @@ public class AccordStateCache extends IntrusiveLinkedList> cache = new HashMap<>();
private final AccordCachingState.Factory nodeFactory;
+ private List> listeners = null;
public Instance(
int index, Class keyClass,
@@ -278,13 +304,36 @@ public class AccordStateCache extends IntrusiveLinkedList l)
+ {
+ if (listeners == null)
+ listeners = new ArrayList<>();
+ listeners.add(l);
+ }
+
+ public void unregister(Listener l)
+ {
+ if (listeners == null)
+ throw new AssertionError("No listeners exist");
+ if (!listeners.remove(l))
+ throw new AssertionError("Listener was not registered");
+ if (listeners.isEmpty())
+ listeners = null;
+ }
+
public Stream> stream()
{
return cache.entrySet().stream()
- .filter(e -> keyClass.isAssignableFrom(e.getKey().getClass()))
+ .filter(e -> instanceForNode(e.getValue()) == this)
.map(e -> (AccordCachingState) e.getValue());
}
+ @Override
+ public Iterator> iterator()
+ {
+ return stream().iterator();
+ }
+
public S acquireOrInitialize(K key, Function valueFactory)
{
incrementCacheQueries();
@@ -295,6 +344,11 @@ public class AccordStateCache extends IntrusiveLinkedList finalNode = node;
+ listeners.forEach(l -> l.onAdd(finalNode));
+ }
}
AccordCachingState acquired = acquireExisting(node, true);
Invariants.checkState(acquired != null, "%s could not be acquired", node);
@@ -350,6 +404,8 @@ public class AccordStateCache extends IntrusiveLinkedList l.onAdd(node));
maybeUpdateSize(node, heapEstimator);
metrics.objectSize.update(node.lastQueriedEstimatedSizeOnHeap);
maybeEvictSomeNodes();
@@ -402,6 +458,9 @@ public class AccordStateCache extends IntrusiveLinkedList l.onRelease(node));
+
if (--node.references == 0)
{
Status status = node.status(); // status() completes
@@ -508,18 +567,34 @@ public class AccordStateCache extends IntrusiveLinkedList map;
+
+ public CommandsForRanges(Ranges ranges, NavigableMap map)
{
- UNKNOWN, LOCAL, REMOTE;
-
- private boolean isSafeToMix(TxnType other)
- {
- if (this == UNKNOWN || other == UNKNOWN) return true;
- return this == other;
- }
- }
-
- public static final class RangeCommandSummary implements Comparable
- {
- public final TxnId txnId;
- public final Ranges ranges;
- public final SaveStatus status;
- public final @Nullable Timestamp executeAt;
- public final List deps;
-
- RangeCommandSummary(TxnId txnId, Ranges ranges, SaveStatus status, @Nullable Timestamp executeAt, List deps)
- {
- this.txnId = txnId;
- this.ranges = ranges;
- this.status = status;
- this.executeAt = executeAt;
- this.deps = deps;
- }
-
- public boolean equalsDeep(RangeCommandSummary other)
- {
- return Objects.equals(txnId, other.txnId)
- && Objects.equals(ranges, other.ranges)
- && status == other.status
- && Objects.equals(executeAt, other.executeAt)
- && Objects.equals(deps, other.deps);
- }
-
- @Override
- public boolean equals(Object o)
- {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- RangeCommandSummary that = (RangeCommandSummary) o;
- return txnId.equals(that.txnId);
- }
-
- @Override
- public int hashCode()
- {
- return Objects.hash(txnId);
- }
-
- @Override
- public String toString()
- {
- return "RangeCommandSummary{" +
- "txnId=" + txnId +
- ", status=" + status +
- ", ranges=" + ranges +
- '}';
- }
-
- public RangeCommandSummary withRanges(Ranges ranges, BiFunction super Ranges, ? super Ranges, ? extends Ranges> remappingFunction)
- {
- return new RangeCommandSummary(txnId, remappingFunction.apply(this.ranges, ranges), status, executeAt, deps);
- }
-
- @Override
- public int compareTo(RangeCommandSummary other)
- {
- // Used in IntervalTree with the expecation that compareTo uniquely identifies an RangeCommandSummary
- return txnId.compareTo(other.txnId);
- }
- }
-
- public static abstract class AbstractBuilder>
- {
- protected final Set localTxns = new HashSet<>();
- protected final TreeMap txnToRange = new TreeMap<>();
- protected final IntervalTree.Builder> rangeToTxn = new IntervalTree.Builder<>();
-
- public TxnType type(TxnId txnId)
- {
- if (!txnToRange.containsKey(txnId)) return TxnType.UNKNOWN;
- return localTxns.contains(txnId) ? TxnType.LOCAL : TxnType.REMOTE;
- }
-
- public T put(TxnId txnId, Ranges ranges, SaveStatus status, Timestamp execteAt, List dependsOn)
- {
- remove(txnId);
- put(new RangeCommandSummary(txnId, ranges, status, execteAt, dependsOn));
- //noinspection unchecked
- return (T) this;
- }
-
- private void put(RangeCommandSummary summary)
- {
- TxnId txnId = summary.txnId;
- localTxns.add(txnId);
- txnToRange.put(txnId, summary);
- addRanges(summary);
- }
-
- private void addRanges(RangeCommandSummary summary)
- {
- for (Range range : summary.ranges)
- rangeToTxn.add(Interval.create(normalize(range.start(), range.startInclusive(), true),
- normalize(range.end(), range.endInclusive(), false),
- summary));
- }
-
- public T putAll(CommandsForRanges other)
- {
- for (TxnId id : other.localCommands)
- {
- TxnType thisType = type(id);
- TxnType otherType = other.type(id);
- Invariants.checkArgument(thisType.isSafeToMix(otherType), "Attempted to add %s; expected %s but was %s", id, thisType, otherType);
- }
- localTxns.addAll(other.localCommands);
- txnToRange.putAll(other.commandsToRanges);
- // If "put" was called before for a txn present in "other", to respect the "put" semantics that update must
- // be removed from "rangeToTxn" (as it got removed from "txnToRange").
- // The expected common case is that this method is called on an empty builder, so the removeIf is off an
- // empty list (aka no-op)
- rangeToTxn.removeIf(data -> other.commandsToRanges.containsKey(data.txnId));
- rangeToTxn.addAll(other.rangesToCommands);
- //noinspection unchecked
- return (T) this;
- }
-
- public T mergeRemote(TxnId txnId, Ranges ranges, BiFunction super Ranges, ? super Ranges, ? extends Ranges> remappingFunction)
- {
- // TODO (durability) : remote ranges are not made durable for now. If this command is stored in commands table,
- // then we have a NotWitnessed command with Ranges, which is not expected in accord.local.Command.NotWitnessed.
- // To properly handle this, the long term storage looks like it will need to store these as well.
- Invariants.checkArgument(!localTxns.contains(txnId), "Attempted to merge remote txn %s, but this is a local txn", txnId);
- // accord.impl.CommandTimeseries.mapReduce does the check on status and deps type, and NotWitnessed should match the semantics hard coded in InMemorySafeStore...
- // in that store, the remote history is only ever included when minStauts == null and deps == ANY... but mapReduce sees accord.local.Status.KnownDeps.hasProposedOrDecidedDeps == false
- // as a mis-match, so will be excluded... since NotWitnessed will return false it will only be included IFF deps = ANY.
- // When it comes to the minStatus check, the current usage is "null", "Committed", "Accepted"... so NotWitnessed will only be included in the null case;
- // the only subtle difference is if minStatus = NotWitnessed, this API will include these but InMemoryStore won't
- RangeCommandSummary oldValue = txnToRange.get(txnId);
- RangeCommandSummary newValue = oldValue == null ?
- new RangeCommandSummary(txnId, ranges, SaveStatus.NotDefined, null, Collections.emptyList())
- : oldValue.withRanges(ranges, remappingFunction);
- if (oldValue == null || !oldValue.equalsDeep(newValue))
- {
- // changes detected... have to update range index
- rangeToTxn.removeIf(data -> data.txnId.equals(txnId));
- addRanges(newValue);
- }
- //noinspection unchecked
- return (T) this;
- }
-
- public T remove(TxnId txnId)
- {
- if (txnToRange.containsKey(txnId))
- {
- localTxns.remove(txnId);
- txnToRange.remove(txnId);
- rangeToTxn.removeIf(data -> data.txnId.equals(txnId));
- }
- //noinspection unchecked
- return (T) this;
- }
-
- public T map(Function super RangeCommandSummary, ? extends RangeCommandSummary> mapper)
- {
- for (TxnId id : new TreeSet<>(txnToRange.keySet()))
- {
- RangeCommandSummary summary = txnToRange.get(id);
- RangeCommandSummary update = mapper.apply(summary);
- if (summary.equals(update))
- continue;
- remove(summary.txnId);
- if (update != null)
- put(update);
- }
- //noinspection unchecked
- return (T) this;
- }
- }
-
- public static class Builder extends AbstractBuilder
- {
- public CommandsForRanges build()
- {
- CommandsForRanges cfr = new CommandsForRanges();
- cfr.set(this);
- return cfr;
- }
- }
-
- public class Updater extends AbstractBuilder
- {
- private Updater()
- {
- putAll(CommandsForRanges.this);
- }
-
- public void apply()
- {
- CommandsForRanges.this.set(this);
- }
- }
-
- private ImmutableSet localCommands;
- private ImmutableSortedMap commandsToRanges;
- private IntervalTree> rangesToCommands;
- @Nullable
- private Timestamp maxRedundant;
-
- public CommandsForRanges()
- {
- localCommands = ImmutableSet.of();
- commandsToRanges = ImmutableSortedMap.of();
- rangesToCommands = IntervalTree.emptyTree();
- }
-
- private void set(AbstractBuilder> builder)
- {
- this.localCommands = ImmutableSet.copyOf(builder.localTxns);
- this.commandsToRanges = ImmutableSortedMap.copyOf(builder.txnToRange);
- this.rangesToCommands = builder.rangeToTxn.build();
- }
-
- public TxnType type(TxnId txnId)
- {
- if (!commandsToRanges.containsKey(txnId)) return TxnType.UNKNOWN;
- return localCommands.contains(txnId) ? TxnType.LOCAL : TxnType.REMOTE;
- }
-
- @VisibleForTesting
- Set knownIds()
- {
- return commandsToRanges.keySet();
- }
-
- @VisibleForTesting
- IntervalTree> tree()
- {
- return rangesToCommands;
- }
-
- public @Nullable Timestamp maxRedundant()
- {
- return maxRedundant;
- }
-
- public static boolean needsUpdate(Command prev, Command updated)
- {
- return CommandsForKey.needsUpdate(prev, updated);
- }
-
- public boolean containsLocally(TxnId txnId)
- {
- return localCommands.contains(txnId);
- }
-
- public Iterable search(AbstractKeys keys)
- {
- // group by the table, as ranges are based off TokenKey, which is scoped to a range
- Map> groupByTable = new TreeMap<>();
- for (Key key : keys)
- groupByTable.computeIfAbsent(((PartitionKey) key).table(), ignore -> new ArrayList<>()).add(key);
- return () -> new AbstractIterator()
- {
- Iterator tblIt = groupByTable.keySet().iterator();
- Iterator>> rangeIt;
-
- @Override
- protected CommandsSummary computeNext()
- {
- while (true)
- {
- if (rangeIt != null && rangeIt.hasNext())
- {
- Map.Entry> next = rangeIt.next();
- return result(next.getKey(), next.getValue());
- }
- rangeIt = null;
- if (!tblIt.hasNext())
- {
- tblIt = null;
- return endOfData();
- }
- TableId tbl = tblIt.next();
- List keys = groupByTable.get(tbl);
- Map> groupByRange = new TreeMap<>(Range::compare);
- for (Key key : keys)
- {
- List> matches = rangesToCommands.matches(key);
- if (matches.isEmpty())
- continue;
- for (Interval interval : matches)
- groupByRange.computeIfAbsent(toRange(interval), ignore -> new HashSet<>()).add(interval.data);
- }
- rangeIt = groupByRange.entrySet().iterator();
- }
- }
- };
- }
-
- private static Range toRange(Interval interval)
- {
- AccordRoutingKey start = (AccordRoutingKey) interval.min;
- if (!(start instanceof AccordRoutingKey.SentinelKey))
- start = new TokenKey(start.table(), start.token().decreaseSlightly());
- AccordRoutingKey end = (AccordRoutingKey) interval.max;
- // TODO (required, correctness) : accord doesn't support wrap around, so decreaseSlightly may fail in some cases
- // TODO (required, correctness) : this logic is mostly used for testing, so is it actually safe for all partitioners?
- return new TokenRange(start, end);
- }
-
- @Nullable
- public CommandsSummary search(Range range)
- {
- List matches = rangesToCommands.search(Interval.create(normalize(range.start(), range.startInclusive(), true),
- normalize(range.end(), range.endInclusive(), false)));
- return result(range, matches);
- }
-
- private CommandsSummary result(Seekable seekable, Collection matches)
- {
- if (matches.isEmpty())
- return null;
- return new Holder(seekable, matches);
- }
-
- public int size()
- {
- return rangesToCommands.intervalCount();
- }
-
- public Updater update()
- {
- return new Updater();
+ this.ranges = ranges;
+ this.map = (NavigableMap) (NavigableMap, ?>) map;
}
@Override
- public String toString()
+ public T mapReduceFull(TxnId testTxnId, Txn.Kind.Kinds testKind, TestStartedAt testStartedAt, TestDep testDep, TestStatus testStatus, CommandFunction map, P1 p1, T accumulate)
{
- return rangesToCommands.unbuild().toString();
+ return mapReduce(testTxnId, testTxnId, testKind, testStartedAt, testDep, testStatus, map, p1, accumulate);
}
- private static RoutingKey normalize(RoutingKey key, boolean inclusive, boolean upOrDown)
+ @Override
+ public T mapReduceActive(Timestamp startedBefore, Txn.Kind.Kinds testKind, CommandFunction map, P1 p1, T accumulate)
{
- while (true)
+ return mapReduce(startedBefore, null, testKind, STARTED_BEFORE, ANY_DEPS, ANY_STATUS, map, p1, accumulate);
+ }
+
+ private T mapReduce(@Nonnull Timestamp testTimestamp, @Nullable TxnId testTxnId, Txn.Kind.Kinds testKind, TestStartedAt testStartedAt, TestDep testDep, TestStatus testStatus, CommandFunction map, P1 p1, T accumulate)
+ {
+ // TODO (required): reconsider how we build this, to avoid having to provide range keys in order (or ensure our range search does this for us)
+ Map> collect = new TreeMap<>(Range::compare);
+ NavigableMap submap;
+ switch (testStartedAt)
{
- if (inclusive) return key;
- AccordRoutingKey ak = (AccordRoutingKey) key;
- switch (ak.kindOfRoutingKey())
+ case STARTED_AFTER:
+ submap = this.map.tailMap(testTimestamp, false);
+ break;
+ case STARTED_BEFORE:
+ submap = this.map.headMap(testTimestamp, false);
+ break;
+ case ANY:
+ submap = this.map;
+ break;
+ default:
+ throw new AssertionError("Unknown started at: " + testStartedAt);
+ }
+ submap.values().forEach((summary -> {
+ if (!testKind.test(summary.txnId.kind()))
+ return;
+
+ // range specific logic... ranges don't update CommandsForRange based off the life cycle and instead
+ // merge the cache with the disk state; so exclude states that should get removed from CommandsFor*
+ if (summary.saveStatus.compareTo(SaveStatus.Erased) >= 0)
+ return;
+
+ switch (testStatus)
{
- case SENTINEL:
- // TODO (required, correctness): this doesn't work
- key = ak.asSentinelKey().toTokenKeyBroken();
+ default: throw new AssertionError("Unhandled TestStatus: " + testStatus);
+ case ANY_STATUS:
+ //TODO (now, symitry): how do we map to TRANSITIVELY_KNOWN?
+ break;
+ case IS_PROPOSED:
+ switch (summary.saveStatus.status)
+ {
+ default: return;
+ case PreCommitted:
+ case Committed:
+ case Accepted:
+ case AcceptedInvalidate:
+ }
+ break;
+ case IS_STABLE:
+ if (!summary.saveStatus.hasBeen(Stable) || summary.saveStatus.hasBeen(Truncated))
+ return;
+ }
+
+ if (testDep != ANY_DEPS)
+ {
+ // ! status.hasInfo
+ //TODO (now, reuse): should this just check if known?
+ if (!(summary.saveStatus.compareTo(SaveStatus.Accepted) >= 0))
+ return;
+
+ Timestamp executeAt = summary.executeAt;
+ if (executeAt.compareTo(testTxnId) <= 0)
+ return;
+
+ // TODO (required): we must ensure these txnId are limited to those we intersect in this command store
+ // We are looking for transactions A that have (or have not) B as a dependency.
+ // If B covers ranges [1..3] and A covers [2..3], but the command store only covers ranges [1..2],
+ // we could have A adopt B as a dependency on [3..3] only, and have that A intersects B on this
+ // command store, but also that there is no dependency relation between them on the overlapping
+ // key range [2..2].
+
+ // This can lead to problems on recovery, where we believe a transaction is a dependency
+ // and so it is safe to execute, when in fact it is only a dependency on a different shard
+ // (and that other shard, perhaps, does not know that it is a dependency - and so it is not durably known)
+ // TODO (required): consider this some more
+ if ((testDep == WITH) == !summary.depsIds.contains(testTxnId))
+ return;
+ }
+
+ // TODO (required): ensure we are excluding any ranges that are now shard-redundant (not sure if this is enforced yet)
+ for (Range range : summary.ranges)
+ {
+ if (!this.ranges.intersects(range))
continue;
- case TOKEN:
- TokenKey tk = ak.asTokenKey();
- // TODO (required, correctness): this doesn't work for ordered partitioner
- return tk.withToken(upOrDown ? tk.token().increaseSlightly() : tk.token().decreaseSlightly());
- default:
- throw new IllegalArgumentException("Unknown kind: " + ak.kindOfRoutingKey());
+ collect.computeIfAbsent(range, ignore -> new ArrayList<>()).add(summary);
}
+ }));
+
+ for (Map.Entry> e : collect.entrySet())
+ {
+ for (CommandsForRangesLoader.Summary command : e.getValue())
+ accumulate = map.apply(p1, e.getKey(), command.txnId, command.executeAt, accumulate);
}
+
+ return accumulate;
}
-
- private static class Holder implements CommandsSummary
- {
- private final Seekable keyOrRange;
- private final Collection matches;
-
- private Holder(Seekable keyOrRange, Collection matches)
- {
- this.keyOrRange = keyOrRange;
- this.matches = matches;
- }
-
- @Override
- public T mapReduceFull(TxnId testTxnId, Txn.Kind.Kinds testKind, TestStartedAt testStartedAt, TestDep testDep, TestStatus testStatus, CommandFunction map, P1 p1, T accumulate)
- {
- return mapReduce(testTxnId, testTxnId, testKind, testStartedAt, testDep, testStatus, map, p1, accumulate);
- }
-
- @Override
- public T mapReduceActive(Timestamp startedBefore, Txn.Kind.Kinds testKind, CommandFunction map, P1 p1, T accumulate)
- {
- return mapReduce(startedBefore, null, testKind, STARTED_BEFORE, ANY_DEPS, ANY_STATUS, map, p1, accumulate);
- }
-
- private T mapReduce(@Nonnull Timestamp testTimestamp, @Nullable TxnId testTxnId, Txn.Kind.Kinds testKind, TestStartedAt testStartedAt, TestDep testDep, TestStatus testStatus, CommandFunction map, P1 p1, T accumulate)
- {
- // TODO (required): reconsider how we build this, to avoid having to provide range keys in order (or ensure our range search does this for us)
- Map> collect = new TreeMap<>(Range::compare);
- matches.forEach((summary -> {
- if (summary.status.compareTo(SaveStatus.Erased) >= 0)
- return;
-
- switch (testStartedAt)
- {
- default: throw new AssertionError();
- case STARTED_AFTER:
- if (summary.txnId.compareTo(testTimestamp) <= 0) return;
- else break;
- case STARTED_BEFORE:
- if (summary.txnId.compareTo(testTimestamp) >= 0) return;
- case ANY:
- if (testDep != ANY_DEPS && (summary.executeAt == null || summary.executeAt.compareTo(testTxnId) < 0))
- return;
- }
-
- switch (testStatus)
- {
- default: throw new AssertionError("Unhandled TestStatus: " + testStatus);
- case ANY_STATUS:
- break;
- case IS_PROPOSED:
- switch (summary.status)
- {
- default: return;
- case PreCommitted:
- case Committed:
- case Accepted:
- }
- break;
- case IS_STABLE:
- if (!summary.status.hasBeen(Stable) || summary.status.hasBeen(Truncated))
- return;
- }
-
- if (!testKind.test(summary.txnId.kind()))
- return;
-
- if (testDep != ANY_DEPS)
- {
- if (!summary.status.known.deps.hasProposedOrDecidedDeps())
- return;
-
- // TODO (required): we must ensure these txnId are limited to those we intersect in this command store
- // We are looking for transactions A that have (or have not) B as a dependency.
- // If B covers ranges [1..3] and A covers [2..3], but the command store only covers ranges [1..2],
- // we could have A adopt B as a dependency on [3..3] only, and have that A intersects B on this
- // command store, but also that there is no dependency relation between them on the overlapping
- // key range [2..2].
-
- // This can lead to problems on recovery, where we believe a transaction is a dependency
- // and so it is safe to execute, when in fact it is only a dependency on a different shard
- // (and that other shard, perhaps, does not know that it is a dependency - and so it is not durably known)
- // TODO (required): consider this some more
- if ((testDep == WITH) == !summary.deps.contains(testTxnId))
- return;
- }
-
- // TODO (required): ensure we are excluding any ranges that are now shard-redundant (not sure if this is enforced yet)
- for (Range range : summary.ranges)
- collect.computeIfAbsent(range, ignore -> new ArrayList<>()).add(summary);
- }));
-
- for (Map.Entry> e : collect.entrySet())
- {
- for (RangeCommandSummary command : e.getValue())
- {
- T initial = accumulate;
- accumulate = map.apply(p1, e.getKey(), command.txnId, command.executeAt, initial);
- }
- }
-
- return accumulate;
- }
-
- @Override
- public String toString()
- {
- return "Holder{" +
- "keyOrRange=" + keyOrRange +
- ", matches=" + matches +
- '}';
- }
- }
-
- public void prune(TxnId pruneBefore, Ranges pruneRanges)
- {
- class MaxErased { Timestamp v; }
- MaxErased maxErased = new MaxErased();
- Updater update = update();
- update.map(summary -> {
- if (summary.txnId.compareTo(pruneBefore) >= 0)
- return summary;
-
- Ranges newRanges = summary.ranges.subtract(pruneRanges);
- if (newRanges == summary.ranges || newRanges.equals(summary.ranges))
- return summary;
-
- maxErased.v = Timestamp.nonNullOrMax(maxErased.v, summary.executeAt);
- if (newRanges.isEmpty())
- return null;
- return new RangeCommandSummary(summary.txnId, newRanges, summary.status, summary.executeAt, summary.deps);
- }).apply();
- maxRedundant = Timestamp.nonNullOrMax(maxRedundant, maxErased.v);
- }
-
}
diff --git a/src/java/org/apache/cassandra/service/accord/CommandsForRangesLoader.java b/src/java/org/apache/cassandra/service/accord/CommandsForRangesLoader.java
new file mode 100644
index 0000000000..f90d57b32e
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/CommandsForRangesLoader.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.BiFunction;
+import javax.annotation.Nullable;
+
+import com.google.common.collect.ImmutableMap;
+
+import accord.local.Command;
+import accord.local.DurableBefore;
+import accord.local.SaveStatus;
+import accord.local.Status;
+import accord.primitives.PartialDeps;
+import accord.primitives.Range;
+import accord.primitives.Ranges;
+import accord.primitives.Routable;
+import accord.primitives.Seekables;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.utils.async.AsyncChains;
+import accord.utils.async.AsyncResult;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.index.accord.RoutesSearcher;
+import org.apache.cassandra.service.accord.api.AccordRoutingKey;
+import org.apache.cassandra.utils.Pair;
+
+public class CommandsForRangesLoader
+{
+ private final RoutesSearcher searcher = new RoutesSearcher();
+ //TODO (now, durability): find solution for this...
+ private final Map historicalTransaction = new HashMap<>();
+ private final AccordCommandStore store;
+
+ public CommandsForRangesLoader(AccordCommandStore store)
+ {
+ this.store = store;
+ }
+
+ public AsyncResult>> get(Ranges ranges)
+ {
+ Watcher watcher = fromCache(ranges);
+ ImmutableMap before = ImmutableMap.copyOf(watcher.get());
+ return AsyncChains.ofCallable(Stage.READ.executor(), () -> get(ranges, before))
+ .map(map -> Pair.create(watcher, map), store)
+ .beginAsResult();
+ }
+
+ private NavigableMap get(Ranges ranges, Map cacheHits)
+ {
+ Set matches = new HashSet<>();
+ for (Range range : ranges)
+ matches.addAll(intersects(range));
+ if (matches.isEmpty())
+ return new TreeMap<>();
+ return load(ranges, cacheHits, matches);
+ }
+
+ private Collection intersects(Range range)
+ {
+ assert range instanceof TokenRange : "Require TokenRange but given " + range.getClass();
+ Set intersects = searcher.intersects(store.id(), (TokenRange) range);
+ if (!historicalTransaction.isEmpty())
+ {
+ if (intersects.isEmpty())
+ intersects = new HashSet<>();
+ for (Map.Entry e : historicalTransaction.entrySet())
+ {
+ if (e.getValue().intersects(range))
+ intersects.add(e.getKey());
+ }
+ if (intersects.isEmpty())
+ intersects = Collections.emptySet();
+ }
+ return intersects;
+ }
+
+ public class Watcher implements AccordStateCache.Listener