mirror of https://github.com/apache/cassandra
Accord: PreLoadContext must properly and consistently support ranges
patch by David Capwell; reviewed by Benedict Elliott Smith for CASSANDRA-19355
This commit is contained in:
parent
333c748a91
commit
777cf84f64
|
|
@ -253,10 +253,6 @@
|
||||||
|
|
||||||
<delete file="${build.lib}/netty-tcnative-boringssl-static-2.0.70.Final-windows-x86_64.jar" failonerror="false"/>
|
<delete file="${build.lib}/netty-tcnative-boringssl-static-2.0.70.Final-windows-x86_64.jar" failonerror="false"/>
|
||||||
<delete file="${build.dir.lib}/jars/netty-tcnative-boringssl-static-2.0.70.Final-windows-x86_64.jar" failonerror="false"/>
|
<delete file="${build.dir.lib}/jars/netty-tcnative-boringssl-static-2.0.70.Final-windows-x86_64.jar" failonerror="false"/>
|
||||||
|
|
||||||
<copy todir="${test.lib}/jars" quiet="false">
|
|
||||||
<file file="${build.lib}/harry-core-0.0.2-CASSANDRA-18768.jar"/>
|
|
||||||
</copy>
|
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
<target name="_resolver-dist-lib_get_files">
|
<target name="_resolver-dist-lib_get_files">
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3562bb3c9ce4e9eecdf65e236e968ef3ee9e0a86
|
Subproject commit f78d1da27b09f89417dd29bde0529f12cd744e3d
|
||||||
|
|
@ -70,4 +70,5 @@ public class AccordSpec
|
||||||
*/
|
*/
|
||||||
public TransactionalMode default_transactional_mode = TransactionalMode.off;
|
public TransactionalMode default_transactional_mode = TransactionalMode.off;
|
||||||
public boolean ephemeralReadEnabled = false;
|
public boolean ephemeralReadEnabled = false;
|
||||||
|
public boolean state_cache_listener_jfr_enabled = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ public enum CassandraRelevantProperties
|
||||||
*/
|
*/
|
||||||
DRAIN_EXECUTOR_TIMEOUT_MS("cassandra.drain_executor_timeout_ms", convertToString(TimeUnit.MINUTES.toMillis(5))),
|
DRAIN_EXECUTOR_TIMEOUT_MS("cassandra.drain_executor_timeout_ms", convertToString(TimeUnit.MINUTES.toMillis(5))),
|
||||||
DROP_OVERSIZED_READ_REPAIR_MUTATIONS("cassandra.drop_oversized_readrepair_mutations"),
|
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"),
|
DTEST_API_LOG_TOPOLOGY("cassandra.dtest.api.log.topology"),
|
||||||
/** This property indicates if the code is running under the in-jvm dtest framework */
|
/** 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"),
|
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_UNSAFE_BOOT_WITH_CLUSTERMETADATA("cassandra.unsafe_boot_with_clustermetadata", null),
|
||||||
TCM_USE_ATOMIC_LONG_PROCESSOR("cassandra.test.use_atomic_long_processor", "false"),
|
TCM_USE_ATOMIC_LONG_PROCESSOR("cassandra.test.use_atomic_long_processor", "false"),
|
||||||
TCM_USE_NO_OP_REPLICATOR("cassandra.test.use_no_op_replicator", "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_BBFAILHELPER_ENABLED("test.bbfailhelper.enabled"),
|
||||||
TEST_BLOB_SHARED_SEED("cassandra.test.blob.shared.seed", "42"),
|
TEST_BLOB_SHARED_SEED("cassandra.test.blob.shared.seed", "42"),
|
||||||
TEST_BYTEMAN_TRANSFORMATIONS_DEBUG("cassandra.test.byteman.transformations.debug"),
|
TEST_BYTEMAN_TRANSFORMATIONS_DEBUG("cassandra.test.byteman.transformations.debug"),
|
||||||
|
|
|
||||||
|
|
@ -5370,6 +5370,11 @@ public class DatabaseDescriptor
|
||||||
conf.accord.shard_durability_cycle = new DurationSpec.IntSecondsBound(seconds);
|
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()
|
public static boolean getForceNewPreparedStatementBehaviour()
|
||||||
{
|
{
|
||||||
return conf.force_new_prepared_statement_behaviour;
|
return conf.force_new_prepared_statement_behaviour;
|
||||||
|
|
|
||||||
|
|
@ -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<Interval[], Interval, byte[]> 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<byte[]> 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<byte[], Interval> comparator, SortedArrays.Search op)
|
||||||
|
{
|
||||||
|
return SortedArrays.binarySearch(intervals, from, to, find, comparator, op);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
public static final Supplier<Checksum> 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<byte[], byte[]> 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<IndexComponent, Segment.ComponentMetadata> write(Interval[] sortedIntervals) throws IOException
|
||||||
|
{
|
||||||
|
EnumMap<IndexComponent, Segment.ComponentMetadata> 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<Interval> 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<ChecksumedRandomAccessReader, byte[], byte[]> 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<byte[]> 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<byte[], byte[]> comparator, SortedArrays.Search op)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return reader.binarySearch(indexInput, stats, recordBuffer, from, to, find, comparator, op);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
CheckpointIntervalArray<ChecksumedRandomAccessReader, byte[], byte[]> 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<Interval>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Group>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<IndexComponent, String> fileNameFormatter;
|
||||||
|
|
||||||
|
Version(String versionString, Function<IndexComponent, String> 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<Component> 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<IndexDescriptor.IndexComponent> getLiveComponents()
|
||||||
|
{
|
||||||
|
return Stream.of(IndexComponent.values())
|
||||||
|
.filter(c -> fileFor(c).exists())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<CassandraMetricsRegistry.MetricName> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ByteBuffer> search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
|
||||||
|
{
|
||||||
|
return memoryIndex.search(storeId, tableId, start, startInclusive, end, endInclusive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ByteBuffer> search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Group, RangeTree<byte[], Range, DecoratedKey>> map = new HashMap<>();
|
||||||
|
@GuardedBy("this")
|
||||||
|
private final Map<Group, Metadata> groupMetadata = new HashMap<>();
|
||||||
|
|
||||||
|
private static class Metadata
|
||||||
|
{
|
||||||
|
public byte[] minTerm, maxTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RangeTree<byte[], Range, DecoratedKey> 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<ByteBuffer> search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
|
||||||
|
{
|
||||||
|
RangeTree<byte[], Range, DecoratedKey> rangesToPks = map.get(new Group(storeId, tableId));
|
||||||
|
if (rangesToPks == null || rangesToPks.isEmpty())
|
||||||
|
return Collections.emptyNavigableSet();
|
||||||
|
TreeMap<Range, Set<DecoratedKey>> matches = search(rangesToPks, start, end);
|
||||||
|
if (matches.isEmpty())
|
||||||
|
return Collections.emptyNavigableSet();
|
||||||
|
TreeSet<ByteBuffer> pks = new TreeSet<>();
|
||||||
|
matches.values().forEach(s -> s.forEach(d -> pks.add(d.getKey())));
|
||||||
|
return pks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TreeMap<Range, Set<DecoratedKey>> search(RangeTree<byte[], Range, DecoratedKey> tokensToPks, byte[] start, byte[] end)
|
||||||
|
{
|
||||||
|
|
||||||
|
TreeMap<Range, Set<DecoratedKey>> 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<Group, Segment.Metadata> output = new HashMap<>();
|
||||||
|
|
||||||
|
List<Group> groups = new ArrayList<>(map.keySet());
|
||||||
|
groups.sort(Comparator.naturalOrder());
|
||||||
|
|
||||||
|
for (Group group : groups)
|
||||||
|
{
|
||||||
|
RangeTree<byte[], Range, DecoratedKey> 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<CheckpointIntervalArrayIndex.Interval> 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<IndexDescriptor.IndexComponent, Segment.ComponentMetadata> 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<Range>, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ColumnMetadata, IndexTarget.Type> 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<ColumnFamilyStore> getBackingTable()
|
||||||
|
{
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Component> 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<SSTableReader> 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<List<SSTableReader>> groups = StorageAttachedIndex.groupBySize(nonIndexed, DatabaseDescriptor.getConcurrentIndexBuilders());
|
||||||
|
List<Future<?>> futures = new ArrayList<>();
|
||||||
|
|
||||||
|
for (List<SSTableReader> group : groups)
|
||||||
|
{
|
||||||
|
futures.add(CompactionManager.instance.submitIndexBuild(new RouteSecondaryIndexBuilder(this, sstableManager, group, false, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FutureCombiner.allOf(futures).get();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SSTableReader> findNonIndexedSSTables(ColumnFamilyStore baseCfs, SSTableManager manager)
|
||||||
|
{
|
||||||
|
Set<SSTableReader> 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<SSTableReader> 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<RowFilter.Expression> 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<ByteBuffer> partitions = search(finalStoreId, finalStart, finalStartInclusive, finalEnd, finalEndInclusive);
|
||||||
|
// do SinglePartitionReadCommand per partition
|
||||||
|
return new SearchIterator(executionController, command, partitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableSet<ByteBuffer> 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<ByteBuffer> 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<UnfilteredRowIterator> implements UnfilteredPartitionIterator
|
||||||
|
{
|
||||||
|
private final ReadExecutionController executionController;
|
||||||
|
private final ReadCommand command;
|
||||||
|
private final TableMetadata metadata;
|
||||||
|
private final Iterator<ByteBuffer> partitions;
|
||||||
|
|
||||||
|
private SearchIterator(ReadExecutionController executionController, ReadCommand command, NavigableSet<ByteBuffer> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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> 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<Segment> readSegements(Map<IndexComponent, FileHandle> index) throws IOException
|
||||||
|
{
|
||||||
|
List<Segment> 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<Group, Segment.Metadata> 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<IndexComponent, Segment.ComponentMetadata> 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<Group> 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<IndexComponent, Segment.ComponentMetadata> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Memtable, MemtableIndex> 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<ByteBuffer> search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive)
|
||||||
|
{
|
||||||
|
TreeSet<ByteBuffer> matches = new TreeSet<>();
|
||||||
|
liveMemtableIndexMap.values().forEach(m -> matches.addAll(m.search(storeId, tableId, start, startInclusive, end, endInclusive)));
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SSTableReader, SSTableIndex> sstables = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onSSTableChanged(Collection<SSTableReader> removed, Iterable<SSTableReader> 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<SSTableReader> 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<ByteBuffer> search(int storeId, TableId tableId,
|
||||||
|
byte[] start, boolean startInclusive,
|
||||||
|
byte[] end, boolean endInclusive)
|
||||||
|
{
|
||||||
|
Group group = new Group(storeId, tableId);
|
||||||
|
TreeSet<ByteBuffer> matches = new TreeSet<>();
|
||||||
|
for (SSTableIndex index : sstables.values())
|
||||||
|
matches.addAll(index.search(group, start, startInclusive, end, endInclusive));
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SSTableReader> sstables;
|
||||||
|
private final boolean isFullRebuild;
|
||||||
|
private final boolean isInitialBuild;
|
||||||
|
private final long totalSizeInBytes;
|
||||||
|
private long bytesProcessed = 0;
|
||||||
|
|
||||||
|
public RouteSecondaryIndexBuilder(RouteIndex index,
|
||||||
|
SSTableManager sstableManager,
|
||||||
|
List<SSTableReader> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Entry> 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<Entry>()
|
||||||
|
{
|
||||||
|
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<TxnId> intersects(int store, TokenRange range)
|
||||||
|
{
|
||||||
|
return intersects(store, (AccordRoutingKey) range.start(), (AccordRoutingKey) range.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<TxnId> intersects(int store, AccordRoutingKey start, AccordRoutingKey end)
|
||||||
|
{
|
||||||
|
HashSet<TxnId> set = new HashSet<TxnId>();
|
||||||
|
try (CloseableIterator<Entry> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<IndexComponent, FileHandle> files;
|
||||||
|
private final List<Segment> segments;
|
||||||
|
|
||||||
|
private SSTableIndex(IndexDescriptor id,
|
||||||
|
Map<IndexComponent, FileHandle> files,
|
||||||
|
List<Segment> 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<IndexComponent, FileHandle> files = new EnumMap<>(IndexComponent.class);
|
||||||
|
for (IndexComponent c : id.getLiveComponents())
|
||||||
|
files.put(c, new FileHandle.Builder(id.fileFor(c)).mmapped(true).complete());
|
||||||
|
List<Segment> 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<Segment> 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<ByteBuffer> 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<ByteBuffer> 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<IndexComponent, FileHandle> files;
|
||||||
|
|
||||||
|
private Cleanup(Map<IndexComponent, FileHandle> 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SSTableReader> removed, Iterable<SSTableReader> added);
|
||||||
|
boolean isIndexComplete(SSTableReader reader);
|
||||||
|
|
||||||
|
NavigableSet<ByteBuffer> search(int storeId, TableId tableId, byte[] start, boolean startInclusive, byte[] end, boolean endInclusive);
|
||||||
|
}
|
||||||
|
|
@ -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<Group, Metadata> groups;
|
||||||
|
|
||||||
|
public Segment(Map<Group, Metadata> 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<IndexDescriptor.IndexComponent, ComponentMetadata> metas;
|
||||||
|
public final byte[] minTerm, maxTerm;
|
||||||
|
|
||||||
|
public Metadata(EnumMap<IndexDescriptor.IndexComponent, ComponentMetadata> metas, byte[] minTerm, byte[] maxTerm)
|
||||||
|
{
|
||||||
|
this.metas = metas;
|
||||||
|
this.minTerm = minTerm;
|
||||||
|
this.maxTerm = maxTerm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Checksum> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Checksum> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Checksum> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Checksum> fn)
|
||||||
|
{
|
||||||
|
super(delegate, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomAccessReader delegate()
|
||||||
|
{
|
||||||
|
return (RandomAccessReader) super.delegate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long length()
|
||||||
|
{
|
||||||
|
return delegate().length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Checksum> fn)
|
||||||
|
{
|
||||||
|
super(delegate, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChecksumedSequentialWriter open(File file, boolean append, Supplier<Checksum> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
package org.apache.cassandra.io.util;
|
package org.apache.cassandra.io.util;
|
||||||
|
|
||||||
import java.io.DataInput;
|
import java.io.DataInput;
|
||||||
|
import java.io.DataInputStream;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -98,6 +99,81 @@ public interface DataInputPlus extends DataInput
|
||||||
throw new EOFException("EOF after " + skipped + " bytes out of " + n);
|
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
|
* Wrapper around an InputStream that provides no buffering but can decode varints
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -193,4 +193,65 @@ public interface DataOutputPlus extends DataOutput
|
||||||
{
|
{
|
||||||
throw new UnsupportedOperationException();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +195,10 @@ public class Journal<K, V> implements Shutdownable
|
||||||
@Override
|
@Override
|
||||||
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.db.Keyspace;
|
import org.apache.cassandra.db.Keyspace;
|
||||||
import org.apache.cassandra.db.PartitionPosition;
|
import org.apache.cassandra.db.PartitionPosition;
|
||||||
import org.apache.cassandra.dht.AbstractBounds;
|
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.dht.Token;
|
||||||
import org.apache.cassandra.schema.KeyspaceMetadata;
|
import org.apache.cassandra.schema.KeyspaceMetadata;
|
||||||
import org.apache.cassandra.schema.TableId;
|
import org.apache.cassandra.schema.TableId;
|
||||||
|
|
@ -405,6 +407,10 @@ public abstract class ReplicaLayout<E extends Endpoints<E>>
|
||||||
|
|
||||||
static EndpointsForToken forLocalStrategyToken(ClusterMetadata metadata, AbstractReplicationStrategy replicationStrategy, Token t)
|
static EndpointsForToken forLocalStrategyToken(ClusterMetadata metadata, AbstractReplicationStrategy replicationStrategy, Token t)
|
||||||
{
|
{
|
||||||
|
if (!(t instanceof LocalPartitioner.LocalToken))
|
||||||
return replicationStrategy.calculateNaturalReplicas(t, metadata).forToken(t);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import org.apache.cassandra.db.ColumnFamilyStore;
|
||||||
import org.apache.cassandra.db.Keyspace;
|
import org.apache.cassandra.db.Keyspace;
|
||||||
import org.apache.cassandra.db.KeyspaceNotDefinedException;
|
import org.apache.cassandra.db.KeyspaceNotDefinedException;
|
||||||
import org.apache.cassandra.db.marshal.AbstractType;
|
import org.apache.cassandra.db.marshal.AbstractType;
|
||||||
|
import org.apache.cassandra.dht.IPartitioner;
|
||||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||||
import org.apache.cassandra.exceptions.UnknownTableException;
|
import org.apache.cassandra.exceptions.UnknownTableException;
|
||||||
import org.apache.cassandra.io.sstable.Descriptor;
|
import org.apache.cassandra.io.sstable.Descriptor;
|
||||||
|
|
@ -135,6 +136,13 @@ public interface SchemaProvider
|
||||||
@Nullable
|
@Nullable
|
||||||
TableMetadata getTableMetadata(TableId id);
|
TableMetadata getTableMetadata(TableId id);
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
default IPartitioner getTablePartitioner(TableId id)
|
||||||
|
{
|
||||||
|
TableMetadata metadata = getTableMetadata(id);
|
||||||
|
return metadata == null ? null : metadata.partitioner;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
default TableMetadataRef getTableMetadataRef(TableId id)
|
default TableMetadataRef getTableMetadataRef(TableId id)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
|
||||||
import org.apache.cassandra.tcm.serialization.MetadataSerializer;
|
import org.apache.cassandra.tcm.serialization.MetadataSerializer;
|
||||||
import org.apache.cassandra.tcm.serialization.Version;
|
import org.apache.cassandra.tcm.serialization.Version;
|
||||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||||
|
import org.apache.cassandra.utils.ObjectSizes;
|
||||||
import org.apache.cassandra.utils.Pair;
|
import org.apache.cassandra.utils.Pair;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
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<TableId>
|
public class TableId implements Comparable<TableId>
|
||||||
{
|
{
|
||||||
public static final long MAGIC = 1956074401491665062L;
|
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 final UUID id;
|
||||||
|
|
||||||
private TableId(UUID id)
|
private TableId(UUID id)
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,11 @@ public class AccordCachingState<K, V> extends IntrusiveLinkedListNode
|
||||||
return status().isComplete();
|
return status().isComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int lastQueriedEstimatedSizeOnHeap()
|
||||||
|
{
|
||||||
|
return lastQueriedEstimatedSizeOnHeap;
|
||||||
|
}
|
||||||
|
|
||||||
int estimatedSizeOnHeap(ToLongFunction<V> estimator)
|
int estimatedSizeOnHeap(ToLongFunction<V> estimator)
|
||||||
{
|
{
|
||||||
shouldUpdateSize = false; // TODO (expected): probably not the safest place to clear need to compute size
|
shouldUpdateSize = false; // TODO (expected): probably not the safest place to clear need to compute size
|
||||||
|
|
@ -600,6 +605,12 @@ public class AccordCachingState<K, V> extends IntrusiveLinkedListNode
|
||||||
return isDone();
|
return isDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V get()
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public State<K, V> complete()
|
public State<K, V> complete()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@
|
||||||
|
|
||||||
package org.apache.cassandra.service.accord;
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NavigableMap;
|
import java.util.NavigableMap;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -27,23 +26,19 @@ import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import accord.api.Key;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import accord.api.Agent;
|
import accord.api.Agent;
|
||||||
import accord.api.DataStore;
|
import accord.api.DataStore;
|
||||||
import accord.api.Key;
|
|
||||||
import accord.api.ProgressLog;
|
import accord.api.ProgressLog;
|
||||||
import accord.local.CommandsForKey;
|
import accord.local.CommandsForKey;
|
||||||
import accord.impl.CommandsSummary;
|
|
||||||
import accord.impl.TimestampsForKey;
|
import accord.impl.TimestampsForKey;
|
||||||
import accord.local.Command;
|
import accord.local.Command;
|
||||||
import accord.local.CommandStore;
|
import accord.local.CommandStore;
|
||||||
|
|
@ -52,37 +47,28 @@ import accord.local.NodeTimeService;
|
||||||
import accord.local.PreLoadContext;
|
import accord.local.PreLoadContext;
|
||||||
import accord.local.RedundantBefore;
|
import accord.local.RedundantBefore;
|
||||||
import accord.local.SafeCommandStore;
|
import accord.local.SafeCommandStore;
|
||||||
import accord.local.SaveStatus;
|
|
||||||
import accord.local.SerializerSupport;
|
|
||||||
import accord.local.SerializerSupport.MessageProvider;
|
import accord.local.SerializerSupport.MessageProvider;
|
||||||
import accord.messages.Message;
|
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.Ranges;
|
||||||
import accord.primitives.Routable;
|
|
||||||
import accord.primitives.RoutableKey;
|
import accord.primitives.RoutableKey;
|
||||||
import accord.primitives.Routables;
|
|
||||||
import accord.primitives.Seekables;
|
|
||||||
import accord.primitives.Timestamp;
|
import accord.primitives.Timestamp;
|
||||||
import accord.primitives.TxnId;
|
import accord.primitives.TxnId;
|
||||||
import accord.utils.Invariants;
|
import accord.utils.Invariants;
|
||||||
import accord.utils.ReducingRangeMap;
|
import accord.utils.ReducingRangeMap;
|
||||||
import accord.utils.async.AsyncChain;
|
import accord.utils.async.AsyncChain;
|
||||||
import accord.utils.async.AsyncChains;
|
import accord.utils.async.AsyncChains;
|
||||||
import accord.utils.async.Observable;
|
|
||||||
import org.apache.cassandra.cache.CacheSize;
|
import org.apache.cassandra.cache.CacheSize;
|
||||||
import org.apache.cassandra.concurrent.ExecutorPlus;
|
import org.apache.cassandra.concurrent.ExecutorPlus;
|
||||||
import org.apache.cassandra.concurrent.Stage;
|
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.db.Mutation;
|
||||||
import org.apache.cassandra.metrics.AccordStateCacheMetrics;
|
import org.apache.cassandra.metrics.AccordStateCacheMetrics;
|
||||||
import org.apache.cassandra.service.accord.api.PartitionKey;
|
import org.apache.cassandra.service.accord.api.PartitionKey;
|
||||||
import org.apache.cassandra.service.accord.async.AsyncOperation;
|
import org.apache.cassandra.service.accord.async.AsyncOperation;
|
||||||
import org.apache.cassandra.service.accord.async.ExecutionOrder;
|
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.Clock;
|
||||||
import org.apache.cassandra.utils.concurrent.AsyncPromise;
|
|
||||||
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
|
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
|
||||||
|
|
||||||
import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
|
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 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)
|
private static long getThreadId(ExecutorService executor)
|
||||||
{
|
{
|
||||||
|
if (!CHECK_THREADS)
|
||||||
|
return 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return executor.submit(() -> Thread.currentThread().getId()).get();
|
return executor.submit(() -> Thread.currentThread().getId()).get();
|
||||||
|
|
@ -109,7 +99,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
|
|
||||||
private final long threadId;
|
private final long threadId;
|
||||||
public final String loggingId;
|
public final String loggingId;
|
||||||
private final AccordJournal journal;
|
private final IJournal journal;
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final ExecutionOrder executionOrder;
|
private final ExecutionOrder executionOrder;
|
||||||
private final AccordStateCache stateCache;
|
private final AccordStateCache stateCache;
|
||||||
|
|
@ -119,7 +109,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
private AsyncOperation<?> currentOperation = null;
|
private AsyncOperation<?> currentOperation = null;
|
||||||
private AccordSafeCommandStore current = null;
|
private AccordSafeCommandStore current = null;
|
||||||
private long lastSystemTimestampMicros = Long.MIN_VALUE;
|
private long lastSystemTimestampMicros = Long.MIN_VALUE;
|
||||||
private CommandsForRanges commandsForRanges = new CommandsForRanges();
|
private final CommandsForRangesLoader commandsForRangesLoader;
|
||||||
|
|
||||||
public AccordCommandStore(int id,
|
public AccordCommandStore(int id,
|
||||||
NodeTimeService time,
|
NodeTimeService time,
|
||||||
|
|
@ -127,12 +117,79 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
DataStore dataStore,
|
DataStore dataStore,
|
||||||
ProgressLog.Factory progressLogFactory,
|
ProgressLog.Factory progressLogFactory,
|
||||||
EpochUpdateHolder epochUpdateHolder,
|
EpochUpdateHolder epochUpdateHolder,
|
||||||
AccordJournal journal,
|
IJournal journal,
|
||||||
AccordStateCacheMetrics cacheMetrics)
|
AccordStateCacheMetrics cacheMetrics)
|
||||||
{
|
{
|
||||||
this(id, time, agent, dataStore, progressLogFactory, epochUpdateHolder, journal, Stage.READ.executor(), Stage.MUTATION.executor(), cacheMetrics);
|
this(id, time, agent, dataStore, progressLogFactory, epochUpdateHolder, journal, Stage.READ.executor(), Stage.MUTATION.executor(), cacheMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static <K, V> void registerJfrListener(int id, AccordStateCache.Instance<K, V, ?> instance, String name)
|
||||||
|
{
|
||||||
|
if (!DatabaseDescriptor.getAccordStateCacheListenerJFREnabled())
|
||||||
|
return;
|
||||||
|
instance.register(new AccordStateCache.Listener<K, V>() {
|
||||||
|
private final IdentityHashMap<AccordCachingState<?, ?>, CacheEvents.Evict> pendingEvicts = new IdentityHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdd(AccordCachingState<K, V> 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<K, V> state)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvict(AccordCachingState<K, V> 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
|
@VisibleForTesting
|
||||||
public AccordCommandStore(int id,
|
public AccordCommandStore(int id,
|
||||||
NodeTimeService time,
|
NodeTimeService time,
|
||||||
|
|
@ -140,7 +197,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
DataStore dataStore,
|
DataStore dataStore,
|
||||||
ProgressLog.Factory progressLogFactory,
|
ProgressLog.Factory progressLogFactory,
|
||||||
EpochUpdateHolder epochUpdateHolder,
|
EpochUpdateHolder epochUpdateHolder,
|
||||||
AccordJournal journal,
|
IJournal journal,
|
||||||
ExecutorPlus loadExecutor,
|
ExecutorPlus loadExecutor,
|
||||||
ExecutorPlus saveExecutor,
|
ExecutorPlus saveExecutor,
|
||||||
AccordStateCacheMetrics cacheMetrics)
|
AccordStateCacheMetrics cacheMetrics)
|
||||||
|
|
@ -160,6 +217,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
this::saveCommand,
|
this::saveCommand,
|
||||||
this::validateCommand,
|
this::validateCommand,
|
||||||
AccordObjectSizes::command);
|
AccordObjectSizes::command);
|
||||||
|
registerJfrListener(id, commandCache, "Command");
|
||||||
timestampsForKeyCache =
|
timestampsForKeyCache =
|
||||||
stateCache.instance(Key.class,
|
stateCache.instance(Key.class,
|
||||||
AccordSafeTimestampsForKey.class,
|
AccordSafeTimestampsForKey.class,
|
||||||
|
|
@ -168,6 +226,7 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
this::saveTimestampsForKey,
|
this::saveTimestampsForKey,
|
||||||
this::validateTimestampsForKey,
|
this::validateTimestampsForKey,
|
||||||
AccordObjectSizes::timestampsForKey);
|
AccordObjectSizes::timestampsForKey);
|
||||||
|
registerJfrListener(id, timestampsForKeyCache, "TimestampsForKey");
|
||||||
commandsForKeyCache =
|
commandsForKeyCache =
|
||||||
stateCache.instance(Key.class,
|
stateCache.instance(Key.class,
|
||||||
AccordSafeCommandsForKey.class,
|
AccordSafeCommandsForKey.class,
|
||||||
|
|
@ -177,6 +236,9 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
this::validateCommandsForKey,
|
this::validateCommandsForKey,
|
||||||
AccordObjectSizes::commandsForKey,
|
AccordObjectSizes::commandsForKey,
|
||||||
AccordCachingState::new);
|
AccordCachingState::new);
|
||||||
|
registerJfrListener(id, commandsForKeyCache, "CommandsForKey");
|
||||||
|
|
||||||
|
this.commandsForRangesLoader = new CommandsForRangesLoader(this);
|
||||||
|
|
||||||
AccordKeyspace.loadCommandStoreMetadata(id, ((rejectBefore, durableBefore, redundantBefore, bootstrapBeganAt, safeToRead) -> {
|
AccordKeyspace.loadCommandStoreMetadata(id, ((rejectBefore, durableBefore, redundantBefore, bootstrapBeganAt, safeToRead) -> {
|
||||||
executor.submit(() -> {
|
executor.submit(() -> {
|
||||||
|
|
@ -194,7 +256,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
}));
|
}));
|
||||||
|
|
||||||
executor.execute(() -> CommandStore.register(this));
|
executor.execute(() -> CommandStore.register(this));
|
||||||
executor.execute(this::loadRangesToCommands);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Factory factory(AccordJournal journal, AccordStateCacheMetrics cacheMetrics)
|
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);
|
new AccordCommandStore(id, time, agent, dataStore, progressLogFactory, rangesForEpoch, journal, cacheMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadRangesToCommands()
|
public CommandsForRangesLoader diskCommandsForRanges()
|
||||||
{
|
{
|
||||||
AsyncPromise<CommandsForRanges> future = new AsyncPromise<>();
|
return commandsForRangesLoader;
|
||||||
AccordKeyspace.findAllCommandsByDomain(id, Routable.Domain.Range, ImmutableSet.of("txn_id", "status", "accepted_ballot", "execute_at"), new Observable<UntypedResultSet.Row>()
|
|
||||||
{
|
|
||||||
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<TxnId> 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean inStore()
|
public boolean inStore()
|
||||||
{
|
{
|
||||||
|
if (!CHECK_THREADS)
|
||||||
|
return true;
|
||||||
return Thread.currentThread().getId() == threadId;
|
return Thread.currentThread().getId() == threadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,6 +309,8 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
|
|
||||||
public void checkNotInStoreThread()
|
public void checkNotInStoreThread()
|
||||||
{
|
{
|
||||||
|
if (!CHECK_THREADS)
|
||||||
|
return;
|
||||||
Invariants.checkState(!inStore());
|
Invariants.checkState(!inStore());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,7 +333,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
{
|
{
|
||||||
return commandsForKeyCache;
|
return commandsForKeyCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
Command loadCommand(TxnId txnId)
|
Command loadCommand(TxnId txnId)
|
||||||
{
|
{
|
||||||
return AccordKeyspace.loadCommand(this, txnId);
|
return AccordKeyspace.loadCommand(this, txnId);
|
||||||
|
|
@ -467,13 +479,16 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
public AccordSafeCommandStore beginOperation(PreLoadContext preLoadContext,
|
public AccordSafeCommandStore beginOperation(PreLoadContext preLoadContext,
|
||||||
Map<TxnId, AccordSafeCommand> commands,
|
Map<TxnId, AccordSafeCommand> commands,
|
||||||
NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKeys,
|
NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKeys,
|
||||||
NavigableMap<Key, AccordSafeCommandsForKey> commandsForKeys)
|
NavigableMap<Key, AccordSafeCommandsForKey> commandsForKeys,
|
||||||
|
@Nullable AccordSafeCommandsForRanges commandsForRanges)
|
||||||
{
|
{
|
||||||
Invariants.checkState(current == null);
|
Invariants.checkState(current == null);
|
||||||
commands.values().forEach(AccordSafeState::preExecute);
|
commands.values().forEach(AccordSafeState::preExecute);
|
||||||
commandsForKeys.values().forEach(AccordSafeState::preExecute);
|
commandsForKeys.values().forEach(AccordSafeState::preExecute);
|
||||||
timestampsForKeys.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;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,46 +504,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
current = null;
|
current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
<O> O mapReduceForRange(Routables<?> keysOrRanges, Ranges slice, BiFunction<CommandsSummary, O, O> map, O accumulate)
|
|
||||||
{
|
|
||||||
keysOrRanges = keysOrRanges.slice(slice, Routables.Slice.Minimal);
|
|
||||||
switch (keysOrRanges.domain())
|
|
||||||
{
|
|
||||||
case Key:
|
|
||||||
{
|
|
||||||
AbstractKeys<Key> keys = (AbstractKeys<Key>) 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()
|
public void abortCurrentOperation()
|
||||||
{
|
{
|
||||||
current = null;
|
current = null;
|
||||||
|
|
@ -576,13 +551,6 @@ public class AccordCommandStore extends CommandStore implements CacheSize
|
||||||
AccordKeyspace.updateRedundantBefore(this, newRedundantBefore);
|
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<TxnId, Ranges> bootstrapBeganAt() { return super.bootstrapBeganAt(); }
|
public NavigableMap<TxnId, Ranges> bootstrapBeganAt() { return super.bootstrapBeganAt(); }
|
||||||
public NavigableMap<Timestamp, Ranges> safeToRead() { return super.safeToRead(); }
|
public NavigableMap<Timestamp, Ranges> safeToRead() { return super.safeToRead(); }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ package org.apache.cassandra.service.accord;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -27,6 +28,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.zip.Checksum;
|
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.RecoverySerializers;
|
||||||
import org.apache.cassandra.service.accord.serializers.SetDurableSerializers;
|
import org.apache.cassandra.service.accord.serializers.SetDurableSerializers;
|
||||||
import org.apache.cassandra.utils.ByteArrayUtil;
|
import org.apache.cassandra.utils.ByteArrayUtil;
|
||||||
|
import org.apache.cassandra.utils.ExecutorUtils;
|
||||||
import org.apache.cassandra.utils.concurrent.Semaphore;
|
import org.apache.cassandra.utils.concurrent.Semaphore;
|
||||||
import org.jctools.queues.SpscLinkedQueue;
|
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.concurrent.Semaphore.newSemaphore;
|
||||||
import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize;
|
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);
|
private static final Logger logger = LoggerFactory.getLogger(AccordJournal.class);
|
||||||
|
|
||||||
|
|
@ -262,9 +265,16 @@ public class AccordJournal implements Shutdownable
|
||||||
@Override
|
@Override
|
||||||
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
|
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
|
||||||
{
|
{
|
||||||
// TODO (expected, other)
|
try
|
||||||
|
{
|
||||||
|
ExecutorUtils.awaitTermination(timeout, units, Arrays.asList(journal, frameAggregator, frameApplicator));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
catch (TimeoutException e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auxiliary records are journal entries that aren't Accord protocol requests - such as {@link FrameRecord}.
|
* Auxiliary records are journal entries that aren't Accord protocol requests - such as {@link FrameRecord}.
|
||||||
|
|
@ -296,6 +306,7 @@ public class AccordJournal implements Shutdownable
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@Override
|
||||||
public void appendMessageBlocking(Message message)
|
public void appendMessageBlocking(Message message)
|
||||||
{
|
{
|
||||||
Type type = Type.fromMessageType(message.type());
|
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
|
* 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.
|
* 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 */
|
/* external MPSC pending request queue */
|
||||||
private final ManyToOneConcurrentLinkedQueue<RequestContext> unframedRequests = new ManyToOneConcurrentLinkedQueue<>();
|
private final ManyToOneConcurrentLinkedQueue<RequestContext> unframedRequests = new ManyToOneConcurrentLinkedQueue<>();
|
||||||
|
|
@ -1091,11 +1102,28 @@ public class AccordJournal implements Shutdownable
|
||||||
executor = executorFactory().infiniteLoop("AccordJournal#FrameAggregator", this, SAFE, NON_DAEMON, SYNCHRONIZED);
|
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()
|
||||||
{
|
{
|
||||||
|
if (executor != null)
|
||||||
executor.shutdown();
|
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
|
@Override
|
||||||
public void run(Interruptible.State state) throws InterruptedException
|
public void run(Interruptible.State state) throws InterruptedException
|
||||||
{
|
{
|
||||||
|
|
@ -1168,7 +1196,7 @@ public class AccordJournal implements Shutdownable
|
||||||
* Gets the aggregated frames containing previously written requests/messages,
|
* 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.
|
* 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 */
|
/** external SPSC written frame queue */
|
||||||
private final SpscLinkedQueue<PendingFrame> newFrames = new SpscLinkedQueue<>();
|
private final SpscLinkedQueue<PendingFrame> newFrames = new SpscLinkedQueue<>();
|
||||||
|
|
@ -1199,11 +1227,28 @@ public class AccordJournal implements Shutdownable
|
||||||
executor = executorFactory().sequential("AccordJournal#FrameApplicator");
|
executor = executorFactory().sequential("AccordJournal#FrameApplicator");
|
||||||
}
|
}
|
||||||
|
|
||||||
void shutdown()
|
@Override
|
||||||
|
public boolean isTerminated() {
|
||||||
|
return executor == null || executor.isTerminated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown()
|
||||||
{
|
{
|
||||||
|
if (executor != null)
|
||||||
executor.shutdown();
|
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
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
|
|
@ -1334,8 +1379,8 @@ public class AccordJournal implements Shutdownable
|
||||||
/*
|
/*
|
||||||
* Message provider implementation
|
* Message provider implementation
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId)
|
public SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId)
|
||||||
{
|
{
|
||||||
return LOG_MESSAGE_PROVIDER ? new LoggingMessageProvider(txnId, new MessageProvider(txnId)) : new MessageProvider(txnId);
|
return LOG_MESSAGE_PROVIDER ? new LoggingMessageProvider(txnId, new MessageProvider(txnId)) : new MessageProvider(txnId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NavigableMap;
|
import java.util.NavigableMap;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
@ -37,6 +40,7 @@ import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
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.rows.RowIterator;
|
||||||
import org.apache.cassandra.db.transform.FilteredPartitions;
|
import org.apache.cassandra.db.transform.FilteredPartitions;
|
||||||
import org.apache.cassandra.dht.ByteOrderedPartitioner;
|
import org.apache.cassandra.dht.ByteOrderedPartitioner;
|
||||||
|
import org.apache.cassandra.dht.IPartitioner;
|
||||||
import org.apache.cassandra.dht.LocalPartitioner;
|
import org.apache.cassandra.dht.LocalPartitioner;
|
||||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||||
import org.apache.cassandra.dht.Token;
|
import org.apache.cassandra.dht.Token;
|
||||||
|
import org.apache.cassandra.index.accord.RouteIndex;
|
||||||
import org.apache.cassandra.io.IVersionedSerializer;
|
import org.apache.cassandra.io.IVersionedSerializer;
|
||||||
import org.apache.cassandra.io.LocalVersionedSerializer;
|
import org.apache.cassandra.io.LocalVersionedSerializer;
|
||||||
import org.apache.cassandra.io.util.DataInputBuffer;
|
import org.apache.cassandra.io.util.DataInputBuffer;
|
||||||
import org.apache.cassandra.io.util.DataOutputBuffer;
|
import org.apache.cassandra.io.util.DataOutputBuffer;
|
||||||
import org.apache.cassandra.schema.ColumnMetadata;
|
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.KeyspaceMetadata;
|
||||||
import org.apache.cassandra.schema.KeyspaceParams;
|
import org.apache.cassandra.schema.KeyspaceParams;
|
||||||
import org.apache.cassandra.schema.Schema;
|
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.TableId;
|
||||||
import org.apache.cassandra.schema.TableMetadata;
|
import org.apache.cassandra.schema.TableMetadata;
|
||||||
import org.apache.cassandra.schema.Tables;
|
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.serializers.UUIDSerializer;
|
||||||
import org.apache.cassandra.service.ClientState;
|
import org.apache.cassandra.service.ClientState;
|
||||||
import org.apache.cassandra.service.accord.AccordConfigurationService.SyncStatus;
|
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.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.CommandSerializers;
|
||||||
import org.apache.cassandra.service.accord.serializers.CommandStoreSerializers;
|
import org.apache.cassandra.service.accord.serializers.CommandStoreSerializers;
|
||||||
import org.apache.cassandra.service.accord.serializers.CommandsForKeySerializer;
|
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);
|
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<TableId, AccordRoutingKeyByteSource.Serializer> 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
|
private enum TokenType
|
||||||
{
|
{
|
||||||
Murmur3((byte) 1),
|
Murmur3((byte) 1),
|
||||||
|
|
@ -216,7 +237,7 @@ public class AccordKeyspace
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: store timestamps as blobs (confirm there are no negative numbers, or offset)
|
// 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,
|
parse(COMMANDS,
|
||||||
"accord commands",
|
"accord commands",
|
||||||
"CREATE TABLE %s ("
|
"CREATE TABLE %s ("
|
||||||
|
|
@ -234,10 +255,13 @@ public class AccordKeyspace
|
||||||
+ "PRIMARY KEY((store_id, domain, txn_id))"
|
+ "PRIMARY KEY((store_id, domain, txn_id))"
|
||||||
+ ')')
|
+ ')')
|
||||||
.partitioner(new LocalPartitioner(CompositeType.getInstance(Int32Type.instance, Int32Type.instance, TIMESTAMP_TYPE)))
|
.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();
|
.build();
|
||||||
|
|
||||||
// TODO: naming is not very clearly distinct from the base serializers
|
// TODO: naming is not very clearly distinct from the base serializers
|
||||||
private static class LocalVersionedSerializers
|
public static class LocalVersionedSerializers
|
||||||
{
|
{
|
||||||
static final LocalVersionedSerializer<Route<?>> route = localSerializer(KeySerializers.route);
|
static final LocalVersionedSerializer<Route<?>> route = localSerializer(KeySerializers.route);
|
||||||
static final LocalVersionedSerializer<Command.DurableAndIdempotentListener> listeners = localSerializer(ListenerSerializers.listener);
|
static final LocalVersionedSerializer<Command.DurableAndIdempotentListener> listeners = localSerializer(ListenerSerializers.listener);
|
||||||
|
|
@ -266,8 +290,8 @@ public class AccordKeyspace
|
||||||
{
|
{
|
||||||
static final ClusteringComparator keyComparator = Commands.partitionKeyAsClusteringComparator();
|
static final ClusteringComparator keyComparator = Commands.partitionKeyAsClusteringComparator();
|
||||||
static final CompositeType partitionKeyType = (CompositeType) Commands.partitionKeyType;
|
static final CompositeType partitionKeyType = (CompositeType) Commands.partitionKeyType;
|
||||||
static final ColumnMetadata txn_id = getColumn(Commands, "txn_id");
|
public static final ColumnMetadata txn_id = getColumn(Commands, "txn_id");
|
||||||
static final ColumnMetadata store_id = getColumn(Commands, "store_id");
|
public static final ColumnMetadata store_id = getColumn(Commands, "store_id");
|
||||||
public static final ColumnMetadata status = getColumn(Commands, "status");
|
public static final ColumnMetadata status = getColumn(Commands, "status");
|
||||||
public static final ColumnMetadata route = getColumn(Commands, "route");
|
public static final ColumnMetadata route = getColumn(Commands, "route");
|
||||||
public static final ColumnMetadata durability = getColumn(Commands, "durability");
|
public static final ColumnMetadata durability = getColumn(Commands, "durability");
|
||||||
|
|
@ -298,6 +322,12 @@ public class AccordKeyspace
|
||||||
return Int32Type.instance.compose(partitionKeyComponents[store_id.position()]);
|
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)
|
public static TxnId getTxnId(ByteBuffer[] partitionKeyComponents)
|
||||||
{
|
{
|
||||||
return deserializeTimestampOrNull(partitionKeyComponents[txn_id.position()], ByteBufferAccessor.instance, TxnId::fromBits);
|
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 =
|
private static final TableMetadata TimestampsForKeys =
|
||||||
parse(TIMESTAMPS_FOR_KEY,
|
parse(TIMESTAMPS_FOR_KEY,
|
||||||
"accord timestamps per key",
|
"accord timestamps per key",
|
||||||
|
|
@ -578,6 +610,11 @@ public class AccordKeyspace
|
||||||
return Int32Type.instance.compose(partitionKeyComponents[store_id.position()]);
|
return Int32Type.instance.compose(partitionKeyComponents[store_id.position()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PartitionKey getKey(DecoratedKey key)
|
||||||
|
{
|
||||||
|
return getKey(splitPartitionKey(key));
|
||||||
|
}
|
||||||
|
|
||||||
public PartitionKey getKey(ByteBuffer[] partitionKeyComponents)
|
public PartitionKey getKey(ByteBuffer[] partitionKeyComponents)
|
||||||
{
|
{
|
||||||
return deserializeKey(partitionKeyComponents[key.position()]);
|
return deserializeKey(partitionKeyComponents[key.position()]);
|
||||||
|
|
@ -860,12 +897,13 @@ public class AccordKeyspace
|
||||||
return value;
|
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());
|
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));
|
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,
|
public static void findAllKeysBetween(int commandStore,
|
||||||
Token start, boolean startInclusive,
|
AccordRoutingKey start, boolean startInclusive,
|
||||||
Token end, boolean endInclusive,
|
AccordRoutingKey end, boolean endInclusive,
|
||||||
Observable<PartitionKey> callback)
|
Observable<PartitionKey> 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)
|
//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)...
|
// 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,
|
KeysBetween work = new KeysBetween(commandStore,
|
||||||
AccordKeyspace.serializeToken(start), startInclusive,
|
AccordKeyspace.serializeRoutingKey(start), startInclusive,
|
||||||
AccordKeyspace.serializeToken(end), endInclusive,
|
AccordKeyspace.serializeRoutingKey(end), endInclusive,
|
||||||
ImmutableSet.of("key"),
|
ImmutableSet.of("key"),
|
||||||
Stage.READ.executor(), Observable.distinct(callback).map(AccordKeyspace::deserializeKey));
|
Stage.READ.executor(), Observable.distinct(callback).map(AccordKeyspace::deserializeKey));
|
||||||
work.schedule();
|
work.schedule();
|
||||||
|
|
@ -1131,14 +1169,14 @@ public class AccordKeyspace
|
||||||
this.start = start;
|
this.start = start;
|
||||||
this.end = end;
|
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" +
|
this.cqlFirst = format("SELECT DISTINCT %s\n" +
|
||||||
"FROM %s\n" +
|
"FROM %s\n" +
|
||||||
"WHERE store_id = ?\n" +
|
"WHERE store_id = ?\n" +
|
||||||
(startInclusive ? " AND key_token >= ?\n" : " AND key_token > ?\n") +
|
(startInclusive ? " AND key_token >= ?\n" : " AND key_token > ?\n") +
|
||||||
(endInclusive ? " AND key_token <= ?\n" : " AND key_token < ?\n") +
|
(endInclusive ? " AND key_token <= ?\n" : " AND key_token < ?\n") +
|
||||||
"ALLOW FILTERING",
|
"ALLOW FILTERING",
|
||||||
selection, TimestampsForKeys);
|
selection, CommandsForKeys);
|
||||||
this.cqlContinue = format("SELECT DISTINCT %s\n" +
|
this.cqlContinue = format("SELECT DISTINCT %s\n" +
|
||||||
"FROM %s\n" +
|
"FROM %s\n" +
|
||||||
"WHERE store_id = ?\n" +
|
"WHERE store_id = ?\n" +
|
||||||
|
|
@ -1146,7 +1184,7 @@ public class AccordKeyspace
|
||||||
" AND key > ?\n" +
|
" AND key > ?\n" +
|
||||||
(endInclusive ? " AND key_token <= ?\n" : " AND key_token < ?\n") +
|
(endInclusive ? " AND key_token <= ?\n" : " AND key_token < ?\n") +
|
||||||
"ALLOW FILTERING",
|
"ALLOW FILTERING",
|
||||||
selection, TimestampsForKeys);
|
selection, CommandsForKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1216,11 +1254,16 @@ public class AccordKeyspace
|
||||||
return Status.Durability.values()[row.getInt("durability", 0)];
|
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;
|
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
|
private static Route<?> deserializeRouteOrNull(UntypedResultSet.Row row) throws IOException
|
||||||
{
|
{
|
||||||
return deserializeRouteOrNull(row.getBlob("route"));
|
return deserializeRouteOrNull(row.getBlob("route"));
|
||||||
|
|
@ -1303,10 +1346,10 @@ public class AccordKeyspace
|
||||||
TableId tableId = TableId.fromUUID(UUIDSerializer.instance.deserialize(split.get(0)));
|
TableId tableId = TableId.fromUUID(UUIDSerializer.instance.deserialize(split.get(0)));
|
||||||
ByteBuffer key = split.get(1);
|
ByteBuffer key = split.get(1);
|
||||||
|
|
||||||
TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
|
IPartitioner partitioner = SchemaHolder.schema.getTablePartitioner(tableId);
|
||||||
if (metadata == null)
|
if (partitioner == null)
|
||||||
throw new IllegalStateException("Table with id " + tableId + " could not be found; was it deleted?");
|
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)
|
public static PartitionKey deserializeKey(UntypedResultSet.Row row)
|
||||||
|
|
@ -1390,16 +1433,36 @@ public class AccordKeyspace
|
||||||
|
|
||||||
private static DecoratedKey makeKey(CommandsForKeyAccessor accessor, int storeId, PartitionKey key)
|
private static DecoratedKey makeKey(CommandsForKeyAccessor accessor, int storeId, PartitionKey key)
|
||||||
{
|
{
|
||||||
Token token = key.token();
|
|
||||||
ByteBuffer pk = accessor.keyComparator.make(storeId,
|
ByteBuffer pk = accessor.keyComparator.make(storeId,
|
||||||
serializeToken(token),
|
serializeRoutingKey(key.toUnseekable()),
|
||||||
serializeKey(key)).serializeAsPartitionKey();
|
serializeKey(key)).serializeAsPartitionKey();
|
||||||
return accessor.table.partitioner.decorateKey(pk);
|
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)
|
private static PartitionUpdate getCommandsForKeyPartitionUpdate(int storeId, PartitionKey key, CommandsForKey commandsForKey, long timestampMicros)
|
||||||
{
|
{
|
||||||
ByteBuffer bytes = CommandsForKeySerializer.toBytesWithoutKey(commandsForKey);
|
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,
|
return singleRowUpdate(CommandsForKeysAccessor.table,
|
||||||
makeKey(CommandsForKeysAccessor, storeId, key),
|
makeKey(CommandsForKeysAccessor, storeId, key),
|
||||||
singleCellRow(Clustering.EMPTY, BufferCell.live(CommandsForKeysAccessor.data, timestampMicros, bytes)));
|
singleCellRow(Clustering.EMPTY, BufferCell.live(CommandsForKeysAccessor.data, timestampMicros, bytes)));
|
||||||
|
|
@ -1832,4 +1895,19 @@ public class AccordKeyspace
|
||||||
consumer.accept(rejectBefore, durableBefore, redundantBefore, bootstrapBeganAt, safeToRead);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ public class AccordMessageSink implements MessageSink
|
||||||
Preconditions.checkNotNull(verb, "Verb is null for type %s", request.type());
|
Preconditions.checkNotNull(verb, "Verb is null for type %s", request.type());
|
||||||
Message<Request> message = Message.out(verb, request);
|
Message<Request> message = Message.out(verb, request);
|
||||||
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(to);
|
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);
|
messaging.send(message, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +233,7 @@ public class AccordMessageSink implements MessageSink
|
||||||
Preconditions.checkNotNull(verb, "Verb is null for type %s", request.type());
|
Preconditions.checkNotNull(verb, "Verb is null for type %s", request.type());
|
||||||
Message<Request> message = Message.out(verb, request);
|
Message<Request> message = Message.out(verb, request);
|
||||||
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(to);
|
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<Reply>) callback, endpointMapper));
|
messaging.sendWithCallback(message, endpoint, new AccordCallback<>(executor, (Callback<Reply>) callback, endpointMapper));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,7 +246,7 @@ public class AccordMessageSink implements MessageSink
|
||||||
responseMsg = responseMsg.withFlag(MessageFlag.NOT_FINAL);
|
responseMsg = responseMsg.withFlag(MessageFlag.NOT_FINAL);
|
||||||
checkReplyType(reply, respondTo);
|
checkReplyType(reply, respondTo);
|
||||||
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(replyingToNode);
|
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);
|
messaging.send(responseMsg, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +256,7 @@ public class AccordMessageSink implements MessageSink
|
||||||
ResponseContext respondTo = (ResponseContext) replyContext;
|
ResponseContext respondTo = (ResponseContext) replyContext;
|
||||||
Message<?> responseMsg = Message.failureResponse(RequestFailureReason.UNKNOWN, failure, respondTo);
|
Message<?> responseMsg = Message.failureResponse(RequestFailureReason.UNKNOWN, failure, respondTo);
|
||||||
InetAddressAndPort endpoint = endpointMapper.mappedEndpoint(replyingToNode);
|
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);
|
messaging.send(responseMsg, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,9 @@
|
||||||
|
|
||||||
package org.apache.cassandra.service.accord;
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NavigableMap;
|
import java.util.NavigableMap;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import accord.api.Agent;
|
import accord.api.Agent;
|
||||||
|
|
@ -33,12 +30,13 @@ import accord.api.ProgressLog;
|
||||||
import accord.impl.AbstractSafeCommandStore;
|
import accord.impl.AbstractSafeCommandStore;
|
||||||
import accord.local.CommandsForKey;
|
import accord.local.CommandsForKey;
|
||||||
import accord.impl.CommandsSummary;
|
import accord.impl.CommandsSummary;
|
||||||
import accord.local.Command;
|
|
||||||
import accord.local.CommandStores.RangesForEpoch;
|
import accord.local.CommandStores.RangesForEpoch;
|
||||||
import accord.local.NodeTimeService;
|
import accord.local.NodeTimeService;
|
||||||
import accord.local.PreLoadContext;
|
import accord.local.PreLoadContext;
|
||||||
import accord.primitives.AbstractKeys;
|
import accord.primitives.AbstractKeys;
|
||||||
|
import accord.primitives.AbstractRanges;
|
||||||
import accord.primitives.Deps;
|
import accord.primitives.Deps;
|
||||||
|
import accord.primitives.Range;
|
||||||
import accord.primitives.Ranges;
|
import accord.primitives.Ranges;
|
||||||
import accord.primitives.Routables;
|
import accord.primitives.Routables;
|
||||||
import accord.primitives.Seekables;
|
import accord.primitives.Seekables;
|
||||||
|
|
@ -46,27 +44,27 @@ import accord.primitives.Timestamp;
|
||||||
import accord.primitives.Txn;
|
import accord.primitives.Txn;
|
||||||
import accord.primitives.TxnId;
|
import accord.primitives.TxnId;
|
||||||
|
|
||||||
import static accord.primitives.Routable.Domain.Range;
|
|
||||||
|
|
||||||
public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeCommand, AccordSafeTimestampsForKey, AccordSafeCommandsForKey>
|
public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeCommand, AccordSafeTimestampsForKey, AccordSafeCommandsForKey>
|
||||||
{
|
{
|
||||||
private final Map<TxnId, AccordSafeCommand> commands;
|
private final Map<TxnId, AccordSafeCommand> commands;
|
||||||
private final NavigableMap<Key, AccordSafeCommandsForKey> commandsForKeys;
|
private final NavigableMap<Key, AccordSafeCommandsForKey> commandsForKeys;
|
||||||
private final NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKeys;
|
private final NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKeys;
|
||||||
|
private final @Nullable AccordSafeCommandsForRanges commandsForRanges;
|
||||||
private final AccordCommandStore commandStore;
|
private final AccordCommandStore commandStore;
|
||||||
private final RangesForEpoch ranges;
|
private final RangesForEpoch ranges;
|
||||||
CommandsForRanges.Updater rangeUpdates = null;
|
|
||||||
|
|
||||||
public AccordSafeCommandStore(PreLoadContext context,
|
public AccordSafeCommandStore(PreLoadContext context,
|
||||||
Map<TxnId, AccordSafeCommand> commands,
|
Map<TxnId, AccordSafeCommand> commands,
|
||||||
NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKey,
|
NavigableMap<Key, AccordSafeTimestampsForKey> timestampsForKey,
|
||||||
NavigableMap<Key, AccordSafeCommandsForKey> commandsForKey,
|
NavigableMap<Key, AccordSafeCommandsForKey> commandsForKey,
|
||||||
|
@Nullable AccordSafeCommandsForRanges commandsForRanges,
|
||||||
AccordCommandStore commandStore)
|
AccordCommandStore commandStore)
|
||||||
{
|
{
|
||||||
super(context);
|
super(context);
|
||||||
this.commands = commands;
|
this.commands = commands;
|
||||||
this.timestampsForKeys = timestampsForKey;
|
this.timestampsForKeys = timestampsForKey;
|
||||||
this.commandsForKeys = commandsForKey;
|
this.commandsForKeys = commandsForKey;
|
||||||
|
this.commandsForRanges = commandsForRanges;
|
||||||
this.commandStore = commandStore;
|
this.commandStore = commandStore;
|
||||||
this.ranges = commandStore.updateRangesForEpoch();
|
this.ranges = commandStore.updateRangesForEpoch();
|
||||||
}
|
}
|
||||||
|
|
@ -185,27 +183,57 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeC
|
||||||
get(key).registerHistorical(this, txnId);
|
get(key).registerHistorical(this, txnId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
CommandsForRanges commandsForRanges = commandStore.commandsForRanges();
|
for (int i = 0; i < deps.rangeDeps.rangeCount(); i++)
|
||||||
deps.rangeDeps.forEachUniqueTxnId(allRanges, txnId -> {
|
{
|
||||||
if (commandsForRanges.containsLocally(txnId))
|
Range range = deps.rangeDeps.range(i);
|
||||||
return;
|
if (!allRanges.intersects(range))
|
||||||
|
continue;
|
||||||
Ranges ranges = deps.rangeDeps.ranges(txnId);
|
deps.rangeDeps.forEach(range, txnId -> {
|
||||||
if (this.ranges.coordinates(txnId).intersects(ranges))
|
// 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
|
return; // already coordinates, no need to replicate
|
||||||
if (!this.ranges.allBefore(txnId.epoch()).intersects(ranges))
|
if (!ranges.allBefore(txnId.epoch()).intersects(range))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
updateRanges().mergeRemote(txnId, ranges.slice(allRanges), Ranges::with);
|
commandStore.diskCommandsForRanges().mergeHistoricalTransaction(txnId, Ranges.single(range).slice(allRanges), Ranges::with);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private <O> O mapReduce(Routables<?> keysOrRanges, Ranges slice, BiFunction<CommandsSummary, O, O> map, O accumulate)
|
private <O> O mapReduce(Routables<?> keysOrRanges, Ranges slice, BiFunction<CommandsSummary, O, O> map, O accumulate)
|
||||||
{
|
{
|
||||||
accumulate = commandStore.mapReduceForRange(keysOrRanges, slice, map, accumulate);
|
accumulate = mapReduceForRange(keysOrRanges, slice, map, accumulate);
|
||||||
return mapReduceForKey(keysOrRanges, slice, map, accumulate);
|
return mapReduceForKey(keysOrRanges, slice, map, accumulate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private <O> O mapReduceForRange(Routables<?> keysOrRanges, Ranges slice, BiFunction<CommandsSummary, O, O> map, O accumulate)
|
||||||
|
{
|
||||||
|
if (commandsForRanges == null)
|
||||||
|
return accumulate;
|
||||||
|
switch (keysOrRanges.domain())
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
{
|
||||||
|
AbstractKeys<Key> keys = (AbstractKeys<Key>) 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> O mapReduceForKey(Routables<?> keysOrRanges, Ranges slice, BiFunction<CommandsSummary, O, O> map, O accumulate)
|
private <O> O mapReduceForKey(Routables<?> keysOrRanges, Ranges slice, BiFunction<CommandsSummary, O, O> map, O accumulate)
|
||||||
{
|
{
|
||||||
switch (keysOrRanges.domain())
|
switch (keysOrRanges.domain())
|
||||||
|
|
@ -260,33 +288,6 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeC
|
||||||
}, accumulate);
|
}, accumulate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void update(Command prev, Command updated)
|
|
||||||
{
|
|
||||||
super.update(prev, updated);
|
|
||||||
|
|
||||||
if (updated.txnId().domain() == Range && CommandsForKey.needsUpdate(prev, updated))
|
|
||||||
{
|
|
||||||
Seekables<?, ?> keysOrRanges = updated.keysOrRanges();
|
|
||||||
if (keysOrRanges == null) keysOrRanges = prev.keysOrRanges();
|
|
||||||
if (keysOrRanges == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
List<TxnId> 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
|
@Override
|
||||||
protected void invalidateSafeState()
|
protected void invalidateSafeState()
|
||||||
{
|
{
|
||||||
|
|
@ -297,14 +298,14 @@ public class AccordSafeCommandStore extends AbstractSafeCommandStore<AccordSafeC
|
||||||
|
|
||||||
public void postExecute(Map<TxnId, AccordSafeCommand> commands,
|
public void postExecute(Map<TxnId, AccordSafeCommand> commands,
|
||||||
Map<Key, AccordSafeTimestampsForKey> timestampsForKey,
|
Map<Key, AccordSafeTimestampsForKey> timestampsForKey,
|
||||||
Map<Key, AccordSafeCommandsForKey> commandsForKeys
|
Map<Key, AccordSafeCommandsForKey> commandsForKeys,
|
||||||
)
|
@Nullable AccordSafeCommandsForRanges commandsForRanges)
|
||||||
{
|
{
|
||||||
postExecute();
|
postExecute();
|
||||||
commands.values().forEach(AccordSafeState::postExecute);
|
commands.values().forEach(AccordSafeState::postExecute);
|
||||||
timestampsForKey.values().forEach(AccordSafeState::postExecute);
|
timestampsForKey.values().forEach(AccordSafeState::postExecute);
|
||||||
commandsForKeys.values().forEach(AccordSafeState::postExecute);
|
commandsForKeys.values().forEach(AccordSafeState::postExecute);
|
||||||
if (rangeUpdates != null)
|
if (commandsForRanges != null)
|
||||||
rangeUpdates.apply();
|
commandsForRanges.postExecute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Range, CommandsForRanges>
|
||||||
|
{
|
||||||
|
private final AsyncResult<Pair<CommandsForRangesLoader.Watcher, NavigableMap<TxnId, CommandsForRangesLoader.Summary>>> chain;
|
||||||
|
private final Ranges ranges;
|
||||||
|
private boolean invalidated;
|
||||||
|
private CommandsForRanges original, current;
|
||||||
|
|
||||||
|
public AccordSafeCommandsForRanges(Ranges ranges, AsyncResult<Pair<CommandsForRangesLoader.Watcher, NavigableMap<TxnId, CommandsForRangesLoader.Summary>>> 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<CommandsForRangesLoader.Watcher, NavigableMap<TxnId, CommandsForRangesLoader.Summary>> 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<Range, CommandsForRanges> 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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
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
|
@Override
|
||||||
public @Nonnull TxnResult coordinate(@Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, @Nonnull Dispatcher.RequestTime requestTime)
|
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;
|
instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static void unsafeSetNoop()
|
||||||
|
{
|
||||||
|
instance = NOOP_SERVICE;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isSetup()
|
public static boolean isSetup()
|
||||||
{
|
{
|
||||||
return instance != null;
|
return instance != null;
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.cassandra.service.accord;
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
@ -68,9 +71,9 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
|
|
||||||
static class Stats
|
static class Stats
|
||||||
{
|
{
|
||||||
private long queries;
|
long queries;
|
||||||
private long hits;
|
long hits;
|
||||||
private long misses;
|
long misses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ImmutableList<Instance<?, ?, ?>> instances = ImmutableList.of();
|
private ImmutableList<Instance<?, ?, ?>> instances = ImmutableList.of();
|
||||||
|
|
@ -83,6 +86,7 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
final AccordStateCacheMetrics metrics;
|
final AccordStateCacheMetrics metrics;
|
||||||
|
final Stats stats = new Stats();
|
||||||
|
|
||||||
public AccordStateCache(ExecutorPlus loadExecutor, ExecutorPlus saveExecutor, long maxSizeInBytes, AccordStateCacheMetrics metrics)
|
public AccordStateCache(ExecutorPlus loadExecutor, ExecutorPlus saveExecutor, long maxSizeInBytes, AccordStateCacheMetrics metrics)
|
||||||
{
|
{
|
||||||
|
|
@ -141,6 +145,13 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
while (iter.hasNext() && bytesCached > maxSizeInBytes)
|
while (iter.hasNext() && bytesCached > maxSizeInBytes)
|
||||||
{
|
{
|
||||||
AccordCachingState<?, ?> node = iter.next();
|
AccordCachingState<?, ?> node = iter.next();
|
||||||
|
maybeEvict(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public boolean maybeEvict(AccordCachingState<?, ?> node)
|
||||||
|
{
|
||||||
checkState(node.references == 0);
|
checkState(node.references == 0);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -154,21 +165,21 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
case LOADED:
|
case LOADED:
|
||||||
unlink(node);
|
unlink(node);
|
||||||
evict(node);
|
evict(node);
|
||||||
break;
|
return true;
|
||||||
case MODIFIED:
|
case MODIFIED:
|
||||||
// schedule a save to disk, keep linked and in the cache map
|
// schedule a save to disk, keep linked and in the cache map
|
||||||
Instance<?, ?, ?> instance = instanceForNode(node);
|
Instance<?, ?, ?> instance = instanceForNode(node);
|
||||||
node.save(saveExecutor, instance.saveFunction);
|
node.save(saveExecutor, instance.saveFunction);
|
||||||
maybeUpdateSize(node, instance.heapEstimator);
|
maybeUpdateSize(node, instance.heapEstimator);
|
||||||
break;
|
return false;
|
||||||
case SAVING:
|
case SAVING:
|
||||||
// skip over until completes to LOADED or FAILED_TO_SAVE
|
// skip over until completes to LOADED or FAILED_TO_SAVE
|
||||||
break;
|
return false;
|
||||||
case FAILED_TO_SAVE:
|
case FAILED_TO_SAVE:
|
||||||
// TODO (consider): panic when a save fails
|
// TODO (consider): panic when a save fails
|
||||||
// permanently unlink, but keep in the map
|
// permanently unlink, but keep in the map
|
||||||
unlink(node);
|
unlink(node);
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,12 +200,14 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
instance.bytesCached -= node.lastQueriedEstimatedSizeOnHeap;
|
instance.bytesCached -= node.lastQueriedEstimatedSizeOnHeap;
|
||||||
|
|
||||||
if (node.status() == LOADED && VALIDATE_LOAD_ON_EVICT)
|
if (node.status() == LOADED && VALIDATE_LOAD_ON_EVICT)
|
||||||
instanceForNode(node).validateLoadEvicted(node);
|
instance.validateLoadEvicted(node);
|
||||||
|
|
||||||
if (!node.hasListeners())
|
if (!node.hasListeners())
|
||||||
{
|
{
|
||||||
AccordCachingState<?, ?> self = instances.get(node.index).cache.remove(node.key());
|
AccordCachingState<?, ?> 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);
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -240,7 +253,19 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
return instance(keyClass, valClass, safeRefFactory, loadFunction, saveFunction, validateFunction, heapEstimator, AccordCachingState.defaultFactory());
|
return instance(keyClass, valClass, safeRefFactory, loadFunction, saveFunction, validateFunction, heapEstimator, AccordCachingState.defaultFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Instance<K, V, S extends AccordSafeState<K, V>> implements CacheSize
|
public Collection<Instance<?, ? ,? >> instances()
|
||||||
|
{
|
||||||
|
return instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener<K, V>
|
||||||
|
{
|
||||||
|
default void onAdd(AccordCachingState<K, V> state) {}
|
||||||
|
default void onRelease(AccordCachingState<K, V> state) {}
|
||||||
|
default void onEvict(AccordCachingState<K, V> state) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Instance<K, V, S extends AccordSafeState<K, V>> implements CacheSize, Iterable<AccordCachingState<K, V>>
|
||||||
{
|
{
|
||||||
private final int index;
|
private final int index;
|
||||||
private final Class<K> keyClass;
|
private final Class<K> keyClass;
|
||||||
|
|
@ -257,6 +282,7 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
private final Stats stats = new Stats();
|
private final Stats stats = new Stats();
|
||||||
private final Map<Object, AccordCachingState<?, ?>> cache = new HashMap<>();
|
private final Map<Object, AccordCachingState<?, ?>> cache = new HashMap<>();
|
||||||
private final AccordCachingState.Factory<K, V> nodeFactory;
|
private final AccordCachingState.Factory<K, V> nodeFactory;
|
||||||
|
private List<Listener<K, V>> listeners = null;
|
||||||
|
|
||||||
public Instance(
|
public Instance(
|
||||||
int index, Class<K> keyClass,
|
int index, Class<K> keyClass,
|
||||||
|
|
@ -278,13 +304,36 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
this.nodeFactory = nodeFactory;
|
this.nodeFactory = nodeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void register(Listener<K, V> l)
|
||||||
|
{
|
||||||
|
if (listeners == null)
|
||||||
|
listeners = new ArrayList<>();
|
||||||
|
listeners.add(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister(Listener<K, V> 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<AccordCachingState<K, V>> stream()
|
public Stream<AccordCachingState<K, V>> stream()
|
||||||
{
|
{
|
||||||
return cache.entrySet().stream()
|
return cache.entrySet().stream()
|
||||||
.filter(e -> keyClass.isAssignableFrom(e.getKey().getClass()))
|
.filter(e -> instanceForNode(e.getValue()) == this)
|
||||||
.map(e -> (AccordCachingState<K, V>) e.getValue());
|
.map(e -> (AccordCachingState<K, V>) e.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<AccordCachingState<K, V>> iterator()
|
||||||
|
{
|
||||||
|
return stream().iterator();
|
||||||
|
}
|
||||||
|
|
||||||
public S acquireOrInitialize(K key, Function<K, V> valueFactory)
|
public S acquireOrInitialize(K key, Function<K, V> valueFactory)
|
||||||
{
|
{
|
||||||
incrementCacheQueries();
|
incrementCacheQueries();
|
||||||
|
|
@ -295,6 +344,11 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
node = nodeFactory.create(key, index);
|
node = nodeFactory.create(key, index);
|
||||||
node.initialize(valueFactory.apply(key));
|
node.initialize(valueFactory.apply(key));
|
||||||
cache.put(key, node);
|
cache.put(key, node);
|
||||||
|
if (listeners != null)
|
||||||
|
{
|
||||||
|
AccordCachingState<K, V> finalNode = node;
|
||||||
|
listeners.forEach(l -> l.onAdd(finalNode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AccordCachingState<K, V> acquired = acquireExisting(node, true);
|
AccordCachingState<K, V> acquired = acquireExisting(node, true);
|
||||||
Invariants.checkState(acquired != null, "%s could not be acquired", node);
|
Invariants.checkState(acquired != null, "%s could not be acquired", node);
|
||||||
|
|
@ -350,6 +404,8 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
node.references++;
|
node.references++;
|
||||||
|
|
||||||
cache.put(key, node);
|
cache.put(key, node);
|
||||||
|
if (listeners != null)
|
||||||
|
listeners.forEach(l -> l.onAdd(node));
|
||||||
maybeUpdateSize(node, heapEstimator);
|
maybeUpdateSize(node, heapEstimator);
|
||||||
metrics.objectSize.update(node.lastQueriedEstimatedSizeOnHeap);
|
metrics.objectSize.update(node.lastQueriedEstimatedSizeOnHeap);
|
||||||
maybeEvictSomeNodes();
|
maybeEvictSomeNodes();
|
||||||
|
|
@ -402,6 +458,9 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
|
|
||||||
maybeUpdateSize(node, heapEstimator);
|
maybeUpdateSize(node, heapEstimator);
|
||||||
|
|
||||||
|
if (listeners != null)
|
||||||
|
listeners.forEach(l -> l.onRelease(node));
|
||||||
|
|
||||||
if (--node.references == 0)
|
if (--node.references == 0)
|
||||||
{
|
{
|
||||||
Status status = node.status(); // status() completes
|
Status status = node.status(); // status() completes
|
||||||
|
|
@ -508,18 +567,34 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
{
|
{
|
||||||
instanceMetrics.requests.mark();
|
instanceMetrics.requests.mark();
|
||||||
metrics.requests.mark();
|
metrics.requests.mark();
|
||||||
|
stats.queries++;
|
||||||
|
AccordStateCache.this.stats.queries++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void incrementCacheHits()
|
private void incrementCacheHits()
|
||||||
{
|
{
|
||||||
instanceMetrics.hits.mark();
|
instanceMetrics.hits.mark();
|
||||||
metrics.hits.mark();
|
metrics.hits.mark();
|
||||||
|
stats.hits++;
|
||||||
|
AccordStateCache.this.stats.hits++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void incrementCacheMisses()
|
private void incrementCacheMisses()
|
||||||
{
|
{
|
||||||
instanceMetrics.misses.mark();
|
instanceMetrics.misses.mark();
|
||||||
metrics.misses.mark();
|
metrics.misses.mark();
|
||||||
|
stats.misses++;
|
||||||
|
AccordStateCache.this.stats.misses++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stats stats()
|
||||||
|
{
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stats globalStats()
|
||||||
|
{
|
||||||
|
return AccordStateCache.this.stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
@ -557,6 +632,30 @@ public class AccordStateCache extends IntrusiveLinkedList<AccordCachingState<?,?
|
||||||
{
|
{
|
||||||
return bytesCached;
|
return bytesCached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long globalAllocated()
|
||||||
|
{
|
||||||
|
return AccordStateCache.this.bytesCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int globalReferencedEntries()
|
||||||
|
{
|
||||||
|
return AccordStateCache.this.numReferencedEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int globalUnreferencedEntries()
|
||||||
|
{
|
||||||
|
return AccordStateCache.this.numUnreferencedEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "Instance{" +
|
||||||
|
"index=" + index +
|
||||||
|
", keyClass=" + keyClass +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public class AccordVerbHandler<T extends Request> implements IVerbHandler<T>
|
||||||
{
|
{
|
||||||
// TODO (desired): need a non-blocking way to inform CMS of an unknown epoch and add callback to it's receipt
|
// TODO (desired): need a non-blocking way to inform CMS of an unknown epoch and add callback to it's receipt
|
||||||
// ClusterMetadataService.instance().maybeCatchup(message.epoch());
|
// ClusterMetadataService.instance().maybeCatchup(message.epoch());
|
||||||
logger.debug("Receiving {} from {}", message.payload, message.from());
|
logger.trace("Receiving {} from {}", message.payload, message.from());
|
||||||
T request = message.payload;
|
T request = message.payload;
|
||||||
|
|
||||||
if (request.type().hasSideEffects())
|
if (request.type().hasSideEffects())
|
||||||
|
|
|
||||||
|
|
@ -19,49 +19,25 @@
|
||||||
package org.apache.cassandra.service.accord;
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.NavigableMap;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.common.collect.AbstractIterator;
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
|
||||||
|
|
||||||
import accord.api.Key;
|
|
||||||
import accord.api.RoutingKey;
|
|
||||||
import accord.local.CommandsForKey;
|
|
||||||
import accord.impl.CommandsSummary;
|
import accord.impl.CommandsSummary;
|
||||||
import accord.local.Command;
|
import accord.local.SafeCommandStore.CommandFunction;
|
||||||
|
import accord.local.SafeCommandStore.TestDep;
|
||||||
|
import accord.local.SafeCommandStore.TestStartedAt;
|
||||||
|
import accord.local.SafeCommandStore.TestStatus;
|
||||||
import accord.local.SaveStatus;
|
import accord.local.SaveStatus;
|
||||||
import accord.primitives.AbstractKeys;
|
|
||||||
import accord.primitives.Range;
|
import accord.primitives.Range;
|
||||||
import accord.primitives.Ranges;
|
import accord.primitives.Ranges;
|
||||||
import accord.primitives.RoutableKey;
|
|
||||||
import accord.primitives.Seekable;
|
|
||||||
import accord.primitives.Timestamp;
|
import accord.primitives.Timestamp;
|
||||||
import accord.primitives.Txn;
|
import accord.primitives.Txn;
|
||||||
import accord.primitives.TxnId;
|
import accord.primitives.TxnId;
|
||||||
import accord.utils.Invariants;
|
|
||||||
import org.apache.cassandra.schema.TableId;
|
|
||||||
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
|
||||||
import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey;
|
|
||||||
import org.apache.cassandra.service.accord.api.PartitionKey;
|
|
||||||
import org.apache.cassandra.utils.Interval;
|
|
||||||
import org.apache.cassandra.utils.IntervalTree;
|
|
||||||
|
|
||||||
import static accord.local.SafeCommandStore.*;
|
|
||||||
import static accord.local.SafeCommandStore.TestDep.ANY_DEPS;
|
import static accord.local.SafeCommandStore.TestDep.ANY_DEPS;
|
||||||
import static accord.local.SafeCommandStore.TestDep.WITH;
|
import static accord.local.SafeCommandStore.TestDep.WITH;
|
||||||
import static accord.local.SafeCommandStore.TestStartedAt.STARTED_BEFORE;
|
import static accord.local.SafeCommandStore.TestStartedAt.STARTED_BEFORE;
|
||||||
|
|
@ -69,386 +45,15 @@ import static accord.local.SafeCommandStore.TestStatus.ANY_STATUS;
|
||||||
import static accord.local.Status.Stable;
|
import static accord.local.Status.Stable;
|
||||||
import static accord.local.Status.Truncated;
|
import static accord.local.Status.Truncated;
|
||||||
|
|
||||||
public class CommandsForRanges
|
public class CommandsForRanges implements CommandsSummary
|
||||||
{
|
{
|
||||||
public enum TxnType
|
private final Ranges ranges;
|
||||||
{
|
private final NavigableMap<Timestamp, CommandsForRangesLoader.Summary> map;
|
||||||
UNKNOWN, LOCAL, REMOTE;
|
|
||||||
|
|
||||||
private boolean isSafeToMix(TxnType other)
|
public CommandsForRanges(Ranges ranges, NavigableMap<TxnId, CommandsForRangesLoader.Summary> map)
|
||||||
{
|
{
|
||||||
if (this == UNKNOWN || other == UNKNOWN) return true;
|
|
||||||
return this == other;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class RangeCommandSummary implements Comparable<RangeCommandSummary>
|
|
||||||
{
|
|
||||||
public final TxnId txnId;
|
|
||||||
public final Ranges ranges;
|
|
||||||
public final SaveStatus status;
|
|
||||||
public final @Nullable Timestamp executeAt;
|
|
||||||
public final List<TxnId> deps;
|
|
||||||
|
|
||||||
RangeCommandSummary(TxnId txnId, Ranges ranges, SaveStatus status, @Nullable Timestamp executeAt, List<TxnId> deps)
|
|
||||||
{
|
|
||||||
this.txnId = txnId;
|
|
||||||
this.ranges = ranges;
|
this.ranges = ranges;
|
||||||
this.status = status;
|
this.map = (NavigableMap<Timestamp, CommandsForRangesLoader.Summary>) (NavigableMap<?, ?>) map;
|
||||||
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<T extends AbstractBuilder<T>>
|
|
||||||
{
|
|
||||||
protected final Set<TxnId> localTxns = new HashSet<>();
|
|
||||||
protected final TreeMap<TxnId, RangeCommandSummary> txnToRange = new TreeMap<>();
|
|
||||||
protected final IntervalTree.Builder<RoutableKey, RangeCommandSummary, Interval<RoutableKey, RangeCommandSummary>> 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<TxnId> 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<Builder>
|
|
||||||
{
|
|
||||||
public CommandsForRanges build()
|
|
||||||
{
|
|
||||||
CommandsForRanges cfr = new CommandsForRanges();
|
|
||||||
cfr.set(this);
|
|
||||||
return cfr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Updater extends AbstractBuilder<Updater>
|
|
||||||
{
|
|
||||||
private Updater()
|
|
||||||
{
|
|
||||||
putAll(CommandsForRanges.this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void apply()
|
|
||||||
{
|
|
||||||
CommandsForRanges.this.set(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImmutableSet<TxnId> localCommands;
|
|
||||||
private ImmutableSortedMap<TxnId, RangeCommandSummary> commandsToRanges;
|
|
||||||
private IntervalTree<RoutableKey, RangeCommandSummary, Interval<RoutableKey, RangeCommandSummary>> 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<TxnId> knownIds()
|
|
||||||
{
|
|
||||||
return commandsToRanges.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
IntervalTree<RoutableKey, RangeCommandSummary, Interval<RoutableKey, RangeCommandSummary>> 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<CommandsSummary> search(AbstractKeys<Key> keys)
|
|
||||||
{
|
|
||||||
// group by the table, as ranges are based off TokenKey, which is scoped to a range
|
|
||||||
Map<TableId, List<Key>> groupByTable = new TreeMap<>();
|
|
||||||
for (Key key : keys)
|
|
||||||
groupByTable.computeIfAbsent(((PartitionKey) key).table(), ignore -> new ArrayList<>()).add(key);
|
|
||||||
return () -> new AbstractIterator<CommandsSummary>()
|
|
||||||
{
|
|
||||||
Iterator<TableId> tblIt = groupByTable.keySet().iterator();
|
|
||||||
Iterator<Map.Entry<Range, Set<RangeCommandSummary>>> rangeIt;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected CommandsSummary computeNext()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (rangeIt != null && rangeIt.hasNext())
|
|
||||||
{
|
|
||||||
Map.Entry<Range, Set<RangeCommandSummary>> next = rangeIt.next();
|
|
||||||
return result(next.getKey(), next.getValue());
|
|
||||||
}
|
|
||||||
rangeIt = null;
|
|
||||||
if (!tblIt.hasNext())
|
|
||||||
{
|
|
||||||
tblIt = null;
|
|
||||||
return endOfData();
|
|
||||||
}
|
|
||||||
TableId tbl = tblIt.next();
|
|
||||||
List<Key> keys = groupByTable.get(tbl);
|
|
||||||
Map<Range, Set<RangeCommandSummary>> groupByRange = new TreeMap<>(Range::compare);
|
|
||||||
for (Key key : keys)
|
|
||||||
{
|
|
||||||
List<Interval<RoutableKey, RangeCommandSummary>> matches = rangesToCommands.matches(key);
|
|
||||||
if (matches.isEmpty())
|
|
||||||
continue;
|
|
||||||
for (Interval<RoutableKey, RangeCommandSummary> interval : matches)
|
|
||||||
groupByRange.computeIfAbsent(toRange(interval), ignore -> new HashSet<>()).add(interval.data);
|
|
||||||
}
|
|
||||||
rangeIt = groupByRange.entrySet().iterator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Range toRange(Interval<RoutableKey, RangeCommandSummary> 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<RangeCommandSummary> 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<RangeCommandSummary> matches)
|
|
||||||
{
|
|
||||||
if (matches.isEmpty())
|
|
||||||
return null;
|
|
||||||
return new Holder(seekable, matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int size()
|
|
||||||
{
|
|
||||||
return rangesToCommands.intervalCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Updater update()
|
|
||||||
{
|
|
||||||
return new Updater();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return rangesToCommands.unbuild().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RoutingKey normalize(RoutingKey key, boolean inclusive, boolean upOrDown)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (inclusive) return key;
|
|
||||||
AccordRoutingKey ak = (AccordRoutingKey) key;
|
|
||||||
switch (ak.kindOfRoutingKey())
|
|
||||||
{
|
|
||||||
case SENTINEL:
|
|
||||||
// TODO (required, correctness): this doesn't work
|
|
||||||
key = ak.asSentinelKey().toTokenKeyBroken();
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Holder implements CommandsSummary
|
|
||||||
{
|
|
||||||
private final Seekable keyOrRange;
|
|
||||||
private final Collection<RangeCommandSummary> matches;
|
|
||||||
|
|
||||||
private Holder(Seekable keyOrRange, Collection<RangeCommandSummary> matches)
|
|
||||||
{
|
|
||||||
this.keyOrRange = keyOrRange;
|
|
||||||
this.matches = matches;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -466,49 +71,61 @@ public class CommandsForRanges
|
||||||
private <P1, T> T mapReduce(@Nonnull Timestamp testTimestamp, @Nullable TxnId testTxnId, Txn.Kind.Kinds testKind, TestStartedAt testStartedAt, TestDep testDep, TestStatus testStatus, CommandFunction<P1, T, T> map, P1 p1, T accumulate)
|
private <P1, T> T mapReduce(@Nonnull Timestamp testTimestamp, @Nullable TxnId testTxnId, Txn.Kind.Kinds testKind, TestStartedAt testStartedAt, TestDep testDep, TestStatus testStatus, CommandFunction<P1, T, T> 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)
|
// 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<Range, List<RangeCommandSummary>> collect = new TreeMap<>(Range::compare);
|
Map<Range, List<CommandsForRangesLoader.Summary>> collect = new TreeMap<>(Range::compare);
|
||||||
matches.forEach((summary -> {
|
NavigableMap<Timestamp, CommandsForRangesLoader.Summary> submap;
|
||||||
if (summary.status.compareTo(SaveStatus.Erased) >= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (testStartedAt)
|
switch (testStartedAt)
|
||||||
{
|
{
|
||||||
default: throw new AssertionError();
|
|
||||||
case STARTED_AFTER:
|
case STARTED_AFTER:
|
||||||
if (summary.txnId.compareTo(testTimestamp) <= 0) return;
|
submap = this.map.tailMap(testTimestamp, false);
|
||||||
else break;
|
break;
|
||||||
case STARTED_BEFORE:
|
case STARTED_BEFORE:
|
||||||
if (summary.txnId.compareTo(testTimestamp) >= 0) return;
|
submap = this.map.headMap(testTimestamp, false);
|
||||||
|
break;
|
||||||
case ANY:
|
case ANY:
|
||||||
if (testDep != ANY_DEPS && (summary.executeAt == null || summary.executeAt.compareTo(testTxnId) < 0))
|
submap = this.map;
|
||||||
return;
|
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)
|
switch (testStatus)
|
||||||
{
|
{
|
||||||
default: throw new AssertionError("Unhandled TestStatus: " + testStatus);
|
default: throw new AssertionError("Unhandled TestStatus: " + testStatus);
|
||||||
case ANY_STATUS:
|
case ANY_STATUS:
|
||||||
|
//TODO (now, symitry): how do we map to TRANSITIVELY_KNOWN?
|
||||||
break;
|
break;
|
||||||
case IS_PROPOSED:
|
case IS_PROPOSED:
|
||||||
switch (summary.status)
|
switch (summary.saveStatus.status)
|
||||||
{
|
{
|
||||||
default: return;
|
default: return;
|
||||||
case PreCommitted:
|
case PreCommitted:
|
||||||
case Committed:
|
case Committed:
|
||||||
case Accepted:
|
case Accepted:
|
||||||
|
case AcceptedInvalidate:
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case IS_STABLE:
|
case IS_STABLE:
|
||||||
if (!summary.status.hasBeen(Stable) || summary.status.hasBeen(Truncated))
|
if (!summary.saveStatus.hasBeen(Stable) || summary.saveStatus.hasBeen(Truncated))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!testKind.test(summary.txnId.kind()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (testDep != ANY_DEPS)
|
if (testDep != ANY_DEPS)
|
||||||
{
|
{
|
||||||
if (!summary.status.known.deps.hasProposedOrDecidedDeps())
|
// ! 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;
|
return;
|
||||||
|
|
||||||
// TODO (required): we must ensure these txnId are limited to those we intersect in this command store
|
// TODO (required): we must ensure these txnId are limited to those we intersect in this command store
|
||||||
|
|
@ -522,56 +139,25 @@ public class CommandsForRanges
|
||||||
// and so it is safe to execute, when in fact it is only a dependency on a different shard
|
// 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)
|
// (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
|
// TODO (required): consider this some more
|
||||||
if ((testDep == WITH) == !summary.deps.contains(testTxnId))
|
if ((testDep == WITH) == !summary.depsIds.contains(testTxnId))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (required): ensure we are excluding any ranges that are now shard-redundant (not sure if this is enforced yet)
|
// 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)
|
for (Range range : summary.ranges)
|
||||||
|
{
|
||||||
|
if (!this.ranges.intersects(range))
|
||||||
|
continue;
|
||||||
collect.computeIfAbsent(range, ignore -> new ArrayList<>()).add(summary);
|
collect.computeIfAbsent(range, ignore -> new ArrayList<>()).add(summary);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (Map.Entry<Range, List<RangeCommandSummary>> e : collect.entrySet())
|
for (Map.Entry<Range, List<CommandsForRangesLoader.Summary>> e : collect.entrySet())
|
||||||
{
|
{
|
||||||
for (RangeCommandSummary command : e.getValue())
|
for (CommandsForRangesLoader.Summary command : e.getValue())
|
||||||
{
|
accumulate = map.apply(p1, e.getKey(), command.txnId, command.executeAt, accumulate);
|
||||||
T initial = accumulate;
|
|
||||||
accumulate = map.apply(p1, e.getKey(), command.txnId, command.executeAt, initial);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return accumulate;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<TxnId, Ranges> historicalTransaction = new HashMap<>();
|
||||||
|
private final AccordCommandStore store;
|
||||||
|
|
||||||
|
public CommandsForRangesLoader(AccordCommandStore store)
|
||||||
|
{
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncResult<Pair<Watcher, NavigableMap<TxnId, Summary>>> get(Ranges ranges)
|
||||||
|
{
|
||||||
|
Watcher watcher = fromCache(ranges);
|
||||||
|
ImmutableMap<TxnId, Summary> before = ImmutableMap.copyOf(watcher.get());
|
||||||
|
return AsyncChains.ofCallable(Stage.READ.executor(), () -> get(ranges, before))
|
||||||
|
.map(map -> Pair.create(watcher, map), store)
|
||||||
|
.beginAsResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NavigableMap<TxnId, Summary> get(Ranges ranges, Map<TxnId, Summary> cacheHits)
|
||||||
|
{
|
||||||
|
Set<TxnId> matches = new HashSet<>();
|
||||||
|
for (Range range : ranges)
|
||||||
|
matches.addAll(intersects(range));
|
||||||
|
if (matches.isEmpty())
|
||||||
|
return new TreeMap<>();
|
||||||
|
return load(ranges, cacheHits, matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<TxnId> intersects(Range range)
|
||||||
|
{
|
||||||
|
assert range instanceof TokenRange : "Require TokenRange but given " + range.getClass();
|
||||||
|
Set<TxnId> intersects = searcher.intersects(store.id(), (TokenRange) range);
|
||||||
|
if (!historicalTransaction.isEmpty())
|
||||||
|
{
|
||||||
|
if (intersects.isEmpty())
|
||||||
|
intersects = new HashSet<>();
|
||||||
|
for (Map.Entry<TxnId, Ranges> 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<TxnId, Command>, AutoCloseable
|
||||||
|
{
|
||||||
|
private final Ranges ranges;
|
||||||
|
|
||||||
|
private NavigableMap<TxnId, Summary> summaries = null;
|
||||||
|
private List<AccordCachingState<TxnId, Command>> needToDoubleCheck = null;
|
||||||
|
|
||||||
|
public Watcher(Ranges ranges)
|
||||||
|
{
|
||||||
|
this.ranges = ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NavigableMap<TxnId, Summary> get()
|
||||||
|
{
|
||||||
|
return summaries == null ? Collections.emptyNavigableMap() : summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdd(AccordCachingState<TxnId, Command> n)
|
||||||
|
{
|
||||||
|
if (n.key().domain() != Routable.Domain.Range)
|
||||||
|
return;
|
||||||
|
AccordCachingState.State<TxnId, Command> state = n.state();
|
||||||
|
if (state instanceof AccordCachingState.Loading)
|
||||||
|
{
|
||||||
|
if (needToDoubleCheck == null)
|
||||||
|
needToDoubleCheck = new ArrayList<>();
|
||||||
|
needToDoubleCheck.add(n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//TODO (now): include FailedToSave? Most likely need to, but need to improve test coverage to have failed writes
|
||||||
|
if (!(state instanceof AccordCachingState.Loaded
|
||||||
|
|| state instanceof AccordCachingState.Modified
|
||||||
|
|| state instanceof AccordCachingState.Saving))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Command cmd = state.get();
|
||||||
|
if (cmd == null)
|
||||||
|
return;
|
||||||
|
Summary summary = create(cmd, ranges, null);
|
||||||
|
if (summary != null)
|
||||||
|
{
|
||||||
|
if (summaries == null)
|
||||||
|
summaries = new TreeMap<>();
|
||||||
|
summaries.put(summary.txnId, summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvict(AccordCachingState<TxnId, Command> state)
|
||||||
|
{
|
||||||
|
if (needToDoubleCheck == null)
|
||||||
|
return;
|
||||||
|
if (!needToDoubleCheck.remove(state))
|
||||||
|
return;
|
||||||
|
if (state.state() instanceof AccordCachingState.Loading)
|
||||||
|
return; // can't double check
|
||||||
|
onAdd(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close()
|
||||||
|
{
|
||||||
|
store.commandCache().unregister(this);
|
||||||
|
if (needToDoubleCheck != null)
|
||||||
|
{
|
||||||
|
List<AccordCachingState<TxnId, Command>> copy = needToDoubleCheck;
|
||||||
|
needToDoubleCheck = null;
|
||||||
|
copy.forEach(this::onAdd);
|
||||||
|
}
|
||||||
|
needToDoubleCheck = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Watcher fromCache(Ranges ranges)
|
||||||
|
{
|
||||||
|
Watcher watcher = new Watcher(ranges);
|
||||||
|
store.commandCache().stream().forEach(watcher::onAdd);
|
||||||
|
store.commandCache().register(watcher);
|
||||||
|
return watcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NavigableMap<TxnId, Summary> load(Ranges ranges, Map<TxnId, Summary> cacheHits, Collection<TxnId> possibleTxns)
|
||||||
|
{
|
||||||
|
//TODO (now): this logic is kinda duplicate of org.apache.cassandra.service.accord.CommandsForRange.mapReduce
|
||||||
|
// should figure out if this can be improved... also what is correct?
|
||||||
|
DurableBefore durableBefore = store.durableBefore();
|
||||||
|
NavigableMap<TxnId, Summary> map = new TreeMap<>();
|
||||||
|
for (TxnId txnId : possibleTxns)
|
||||||
|
{
|
||||||
|
if (cacheHits.containsKey(txnId))
|
||||||
|
continue;
|
||||||
|
Command cmd = store.loadCommand(txnId);
|
||||||
|
if (cmd == null)
|
||||||
|
continue; // unknown command
|
||||||
|
Summary summary = create(cmd, ranges, durableBefore);
|
||||||
|
if (summary == null)
|
||||||
|
continue;
|
||||||
|
map.put(txnId, summary);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Summary create(Command cmd, Ranges cacheRanges, @Nullable DurableBefore durableBefore)
|
||||||
|
{
|
||||||
|
//TODO (now, correctness): C* did Invalidated, accord-core did Erased... what is correct?
|
||||||
|
SaveStatus saveStatus = cmd.saveStatus();
|
||||||
|
if (saveStatus == SaveStatus.Invalidated
|
||||||
|
|| saveStatus == SaveStatus.Erased
|
||||||
|
|| !saveStatus.hasBeen(Status.PreAccepted))
|
||||||
|
return null;
|
||||||
|
if (cmd.partialTxn() == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Seekables<?, ? extends Seekables<?, ?>> keysOrRanges = cmd.partialTxn().keys();
|
||||||
|
if (keysOrRanges.domain() != Routable.Domain.Range)
|
||||||
|
throw new AssertionError(String.format("Txn keys are not range for %s", cmd.partialTxn()));
|
||||||
|
Ranges ranges = (Ranges) keysOrRanges;
|
||||||
|
|
||||||
|
if (!ranges.intersects(cacheRanges))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (durableBefore != null)
|
||||||
|
{
|
||||||
|
Ranges durableAlready = Ranges.of(durableBefore.foldlWithBounds(ranges, (e, accum, start, end) -> {
|
||||||
|
if (e.universalBefore.compareTo(cmd.txnId()) < 0)
|
||||||
|
return accum;
|
||||||
|
accum.add(new TokenRange((AccordRoutingKey) start, (AccordRoutingKey) end));
|
||||||
|
return accum;
|
||||||
|
}, new ArrayList<Range>(), ignore -> false).toArray(Range[]::new));
|
||||||
|
Ranges newRanges = ranges.subtract(durableAlready);
|
||||||
|
|
||||||
|
if (newRanges.isEmpty())
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PartialDeps partialDeps = cmd.partialDeps();
|
||||||
|
List<TxnId> deps = partialDeps == null ? Collections.emptyList() : partialDeps.txnIds();
|
||||||
|
return new Summary(cmd.txnId(), cmd.executeAt(), saveStatus, ranges, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mergeHistoricalTransaction(TxnId txnId, Ranges ranges, BiFunction<? super Ranges, ? super Ranges, ? extends Ranges> remappingFunction)
|
||||||
|
{
|
||||||
|
historicalTransaction.merge(txnId, ranges, remappingFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Summary
|
||||||
|
{
|
||||||
|
public final TxnId txnId;
|
||||||
|
@Nullable
|
||||||
|
public final Timestamp executeAt;
|
||||||
|
public final SaveStatus saveStatus;
|
||||||
|
public final Ranges ranges;
|
||||||
|
public final List<TxnId> depsIds;
|
||||||
|
|
||||||
|
private Summary(TxnId txnId, @Nullable Timestamp executeAt, SaveStatus saveStatus, Ranges ranges, List<TxnId> depsIds)
|
||||||
|
{
|
||||||
|
this.txnId = txnId;
|
||||||
|
this.executeAt = executeAt;
|
||||||
|
this.saveStatus = saveStatus;
|
||||||
|
this.ranges = ranges;
|
||||||
|
this.depsIds = depsIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "Summary{" +
|
||||||
|
"txnId=" + txnId +
|
||||||
|
", executeAt=" + executeAt +
|
||||||
|
", saveStatus=" + saveStatus +
|
||||||
|
", ranges=" + ranges +
|
||||||
|
", depsIds=" + depsIds +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,10 +57,7 @@ public interface IAccordService
|
||||||
|
|
||||||
IVerbHandler<? extends Request> verbHandler();
|
IVerbHandler<? extends Request> verbHandler();
|
||||||
|
|
||||||
default long barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException
|
long barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException;
|
||||||
{
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
long barrier(@Nonnull Seekables keysOrRanges, long minEpoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite);
|
long barrier(@Nonnull Seekables keysOrRanges, long minEpoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
|
import accord.local.SerializerSupport;
|
||||||
|
import accord.messages.Message;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
|
||||||
|
public interface IJournal
|
||||||
|
{
|
||||||
|
SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId);
|
||||||
|
void appendMessageBlocking(Message message);
|
||||||
|
}
|
||||||
|
|
@ -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.service.accord;
|
||||||
|
|
||||||
|
import accord.api.RoutingKey;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import org.apache.cassandra.utils.RangeTree;
|
||||||
|
|
||||||
|
public enum RangeTreeRangeAccessor implements RangeTree.Accessor<RoutingKey, Range>
|
||||||
|
{
|
||||||
|
instance;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RoutingKey start(Range range)
|
||||||
|
{
|
||||||
|
return range.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RoutingKey end(Range range)
|
||||||
|
{
|
||||||
|
return range.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(Range range, RoutingKey routingKey)
|
||||||
|
{
|
||||||
|
return range.contains(routingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(RoutingKey start, RoutingKey end, RoutingKey routingKey)
|
||||||
|
{
|
||||||
|
if (routingKey.compareTo(start) <= 0)
|
||||||
|
return false;
|
||||||
|
if (routingKey.compareTo(end) > 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean intersects(Range range, RoutingKey start, RoutingKey end)
|
||||||
|
{
|
||||||
|
if (range.start().compareTo(end) >= 0) return false;
|
||||||
|
if (range.end().compareTo(start) <= 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean intersects(Range left, Range right)
|
||||||
|
{
|
||||||
|
return left.compareIntersecting(right) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -90,9 +90,9 @@ public abstract class AccordRoutingKey extends AccordRoutableKey implements Rout
|
||||||
{
|
{
|
||||||
private static final long EMPTY_SIZE = ObjectSizes.measure(new SentinelKey(null, true));
|
private static final long EMPTY_SIZE = ObjectSizes.measure(new SentinelKey(null, true));
|
||||||
|
|
||||||
private final boolean isMin;
|
public final boolean isMin;
|
||||||
|
|
||||||
private SentinelKey(TableId table, boolean isMin)
|
public SentinelKey(TableId table, boolean isMin)
|
||||||
{
|
{
|
||||||
super(table);
|
super(table);
|
||||||
this.isMin = isMin;
|
this.isMin = isMin;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import java.nio.ByteBuffer;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
import accord.api.Key;
|
import accord.api.Key;
|
||||||
import accord.api.RoutingKey;
|
|
||||||
import accord.primitives.Routable;
|
import accord.primitives.Routable;
|
||||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.db.DecoratedKey;
|
import org.apache.cassandra.db.DecoratedKey;
|
||||||
|
|
@ -96,7 +95,7 @@ public final class PartitionKey extends AccordRoutableKey implements Key
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RoutingKey toUnseekable()
|
public TokenKey toUnseekable()
|
||||||
{
|
{
|
||||||
return new TokenKey(table, token());
|
return new TokenKey(table, token());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,43 +17,30 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.cassandra.service.accord.async;
|
package org.apache.cassandra.service.accord.async;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import accord.api.Key;
|
import accord.api.Key;
|
||||||
import accord.api.RoutingKey;
|
import accord.local.CommandsForKey;
|
||||||
import accord.local.KeyHistory;
|
import accord.local.KeyHistory;
|
||||||
import accord.local.PreLoadContext;
|
import accord.local.PreLoadContext;
|
||||||
import accord.primitives.Range;
|
import accord.primitives.*;
|
||||||
import accord.primitives.Ranges;
|
|
||||||
import accord.primitives.Seekables;
|
|
||||||
import accord.primitives.TxnId;
|
|
||||||
import accord.utils.Invariants;
|
import accord.utils.Invariants;
|
||||||
import accord.utils.async.AsyncChain;
|
import accord.utils.async.AsyncChain;
|
||||||
import accord.utils.async.AsyncChains;
|
import accord.utils.async.AsyncChains;
|
||||||
import accord.utils.async.AsyncResult;
|
import accord.utils.async.AsyncResult;
|
||||||
import accord.utils.async.Observable;
|
import accord.utils.async.Observable;
|
||||||
import org.apache.cassandra.service.accord.AccordCachingState;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import org.apache.cassandra.service.accord.AccordCommandStore;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import org.apache.cassandra.service.accord.AccordKeyspace;
|
import com.google.common.collect.Iterables;
|
||||||
import org.apache.cassandra.service.accord.AccordSafeState;
|
import org.apache.cassandra.service.accord.*;
|
||||||
import org.apache.cassandra.service.accord.AccordStateCache;
|
|
||||||
import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey;
|
|
||||||
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
||||||
import org.apache.cassandra.service.accord.api.PartitionKey;
|
import org.apache.cassandra.service.accord.api.PartitionKey;
|
||||||
|
import org.apache.cassandra.utils.Pair;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class AsyncLoader
|
public class AsyncLoader
|
||||||
{
|
{
|
||||||
|
|
@ -172,15 +159,41 @@ public class AsyncLoader
|
||||||
|
|
||||||
private AsyncChain<?> referenceAndDispatchReadsForRange(AsyncOperation.Context context)
|
private AsyncChain<?> referenceAndDispatchReadsForRange(AsyncOperation.Context context)
|
||||||
{
|
{
|
||||||
AsyncChain<Set<? extends Key>> overlappingKeys = findOverlappingKeys((Ranges) keysOrRanges);
|
Ranges ranges = (Ranges) keysOrRanges;
|
||||||
|
|
||||||
return overlappingKeys.flatMap(keys -> {
|
List<AsyncChain<?>> root = new ArrayList<>(ranges.size() + 1);
|
||||||
if (keys.isEmpty())
|
class Watcher implements AccordStateCache.Listener<Key, CommandsForKey>
|
||||||
|
{
|
||||||
|
private final Set<PartitionKey> cached = commandStore.commandsForKeyCache().stream()
|
||||||
|
.map(n -> (PartitionKey) n.key())
|
||||||
|
.filter(ranges::contains)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdd(AccordCachingState<Key, CommandsForKey> state)
|
||||||
|
{
|
||||||
|
PartitionKey pk = (PartitionKey) state.key();
|
||||||
|
if (ranges.contains(pk))
|
||||||
|
cached.add(pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Watcher watcher = new Watcher();
|
||||||
|
commandStore.commandsForKeyCache().register(watcher);
|
||||||
|
root.add(findOverlappingKeys(ranges).flatMap(keys -> {
|
||||||
|
commandStore.commandsForKeyCache().unregister(watcher);
|
||||||
|
if (keys.isEmpty() && watcher.cached.isEmpty())
|
||||||
return AsyncChains.success(null);
|
return AsyncChains.success(null);
|
||||||
|
Set<? extends Key> set = ImmutableSet.<Key>builder().addAll(watcher.cached).addAll(keys).build();
|
||||||
List<AsyncChain<?>> chains = new ArrayList<>();
|
List<AsyncChain<?>> chains = new ArrayList<>();
|
||||||
keys.forEach(key -> referenceAndAssembleReadsForKey(key, context, chains));
|
set.forEach(key -> referenceAndAssembleReadsForKey(key, context, chains));
|
||||||
return chains.isEmpty() ? AsyncChains.success(null) : AsyncChains.reduce(chains, (a, b) -> null);
|
return chains.isEmpty() ? AsyncChains.success(null) : AsyncChains.reduce(chains, (a, b) -> null);
|
||||||
}, commandStore);
|
}, commandStore));
|
||||||
|
|
||||||
|
AsyncResult<Pair<CommandsForRangesLoader.Watcher, NavigableMap<TxnId, CommandsForRangesLoader.Summary>>> chain = commandStore.diskCommandsForRanges().get(ranges);
|
||||||
|
root.add(chain);
|
||||||
|
context.commandsForRanges = new AccordSafeCommandsForRanges(ranges, chain);
|
||||||
|
|
||||||
|
return AsyncChains.all(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AsyncChain<Set<? extends Key>> findOverlappingKeys(Ranges ranges)
|
private AsyncChain<Set<? extends Key>> findOverlappingKeys(Ranges ranges)
|
||||||
|
|
@ -195,27 +208,14 @@ public class AsyncLoader
|
||||||
|
|
||||||
private AsyncChain<Set<PartitionKey>> findOverlappingKeys(Range range)
|
private AsyncChain<Set<PartitionKey>> findOverlappingKeys(Range range)
|
||||||
{
|
{
|
||||||
Set<PartitionKey> cached = commandStore.commandsForKeyCache().stream()
|
|
||||||
.map(n -> (PartitionKey) n.key())
|
|
||||||
.filter(range::contains)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
// save to a variable as java gets confused when `.map` is called on the result of asChain
|
// save to a variable as java gets confused when `.map` is called on the result of asChain
|
||||||
AsyncChain<Set<PartitionKey>> map = Observable.asChain(callback ->
|
AsyncChain<Set<PartitionKey>> map = Observable.asChain(callback ->
|
||||||
AccordKeyspace.findAllKeysBetween(commandStore.id(),
|
AccordKeyspace.findAllKeysBetween(commandStore.id(),
|
||||||
toTokenKey(range.start()).token(), range.startInclusive(),
|
(AccordRoutingKey) range.start(), range.startInclusive(),
|
||||||
toTokenKey(range.end()).token(), range.endInclusive(),
|
(AccordRoutingKey) range.end(), range.endInclusive(),
|
||||||
callback),
|
callback),
|
||||||
Collectors.toSet());
|
Collectors.toSet());
|
||||||
return map.map(s -> ImmutableSet.<PartitionKey>builder().addAll(s).addAll(cached).build());
|
return map.map(s -> ImmutableSet.<PartitionKey>builder().addAll(s).build());
|
||||||
}
|
|
||||||
|
|
||||||
private static TokenKey toTokenKey(RoutingKey start)
|
|
||||||
{
|
|
||||||
if (start instanceof TokenKey)
|
|
||||||
return (TokenKey) start;
|
|
||||||
if (start instanceof AccordRoutingKey.SentinelKey)
|
|
||||||
return ((AccordRoutingKey.SentinelKey) start).toTokenKeyBroken();
|
|
||||||
throw new IllegalArgumentException(String.format("Unable to convert RoutingKey %s (type %s) to TokenKey", start, start.getClass()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,11 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.cassandra.service.accord.async;
|
package org.apache.cassandra.service.accord.async;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -34,26 +32,26 @@ import accord.api.Key;
|
||||||
import accord.local.CommandStore;
|
import accord.local.CommandStore;
|
||||||
import accord.local.PreLoadContext;
|
import accord.local.PreLoadContext;
|
||||||
import accord.local.SafeCommandStore;
|
import accord.local.SafeCommandStore;
|
||||||
import accord.primitives.RoutableKey;
|
|
||||||
import accord.primitives.Seekables;
|
import accord.primitives.Seekables;
|
||||||
import accord.primitives.TxnId;
|
import accord.primitives.TxnId;
|
||||||
import accord.utils.Invariants;
|
import accord.utils.Invariants;
|
||||||
import accord.utils.async.AsyncChains;
|
import accord.utils.async.AsyncChains;
|
||||||
import org.apache.cassandra.service.accord.AccordCommandStore;
|
import org.apache.cassandra.service.accord.AccordCommandStore;
|
||||||
import org.apache.cassandra.service.accord.AccordSafeCommand;
|
import org.apache.cassandra.service.accord.AccordSafeCommand;
|
||||||
import org.apache.cassandra.service.accord.AccordSafeCommandsForKey;
|
|
||||||
import org.apache.cassandra.service.accord.AccordSafeCommandStore;
|
import org.apache.cassandra.service.accord.AccordSafeCommandStore;
|
||||||
|
import org.apache.cassandra.service.accord.AccordSafeCommandsForKey;
|
||||||
|
import org.apache.cassandra.service.accord.AccordSafeCommandsForRanges;
|
||||||
import org.apache.cassandra.service.accord.AccordSafeState;
|
import org.apache.cassandra.service.accord.AccordSafeState;
|
||||||
import org.apache.cassandra.service.accord.AccordSafeTimestampsForKey;
|
import org.apache.cassandra.service.accord.AccordSafeTimestampsForKey;
|
||||||
|
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncLoader.txnIds;
|
import static org.apache.cassandra.service.accord.async.AsyncLoader.txnIds;
|
||||||
|
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.COMPLETING;
|
||||||
|
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.FAILED;
|
||||||
|
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.FINISHED;
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.INITIALIZED;
|
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.INITIALIZED;
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.LOADING;
|
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.LOADING;
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.PREPARING;
|
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.PREPARING;
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.RUNNING;
|
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.RUNNING;
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.COMPLETING;
|
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.FINISHED;
|
|
||||||
import static org.apache.cassandra.service.accord.async.AsyncOperation.State.FAILED;
|
|
||||||
|
|
||||||
public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements Runnable, Function<SafeCommandStore, R>
|
public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements Runnable, Function<SafeCommandStore, R>
|
||||||
{
|
{
|
||||||
|
|
@ -70,6 +68,8 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
|
||||||
final HashMap<TxnId, AccordSafeCommand> commands = new HashMap<>();
|
final HashMap<TxnId, AccordSafeCommand> commands = new HashMap<>();
|
||||||
final TreeMap<Key, AccordSafeTimestampsForKey> timestampsForKey = new TreeMap<>();
|
final TreeMap<Key, AccordSafeTimestampsForKey> timestampsForKey = new TreeMap<>();
|
||||||
final TreeMap<Key, AccordSafeCommandsForKey> commandsForKey = new TreeMap<>();
|
final TreeMap<Key, AccordSafeCommandsForKey> commandsForKey = new TreeMap<>();
|
||||||
|
@Nullable
|
||||||
|
AccordSafeCommandsForRanges commandsForRanges = null;
|
||||||
|
|
||||||
void releaseResources(AccordCommandStore commandStore)
|
void releaseResources(AccordCommandStore commandStore)
|
||||||
{
|
{
|
||||||
|
|
@ -83,6 +83,8 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
|
||||||
commands.values().forEach(AccordSafeState::revert);
|
commands.values().forEach(AccordSafeState::revert);
|
||||||
timestampsForKey.values().forEach(AccordSafeState::revert);
|
timestampsForKey.values().forEach(AccordSafeState::revert);
|
||||||
commandsForKey.values().forEach(AccordSafeState::revert);
|
commandsForKey.values().forEach(AccordSafeState::revert);
|
||||||
|
if (commandsForRanges != null)
|
||||||
|
commandsForRanges.revert();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,19 +189,9 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Iterable<RoutableKey> keys()
|
Seekables<?, ?> keys()
|
||||||
{
|
{
|
||||||
Seekables<?, ?> keys = preLoadContext.keys();
|
return preLoadContext.keys();
|
||||||
switch (keys.domain())
|
|
||||||
{
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Unhandled domain " + keys.domain());
|
|
||||||
case Key:
|
|
||||||
return (Iterable<RoutableKey>) keys;
|
|
||||||
case Range:
|
|
||||||
// TODO (expected): handle ranges
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fail(Throwable throwable)
|
private void fail(Throwable throwable)
|
||||||
|
|
@ -255,11 +247,11 @@ public abstract class AsyncOperation<R> extends AsyncChains.Head<R> implements R
|
||||||
return;
|
return;
|
||||||
state(PREPARING);
|
state(PREPARING);
|
||||||
case PREPARING:
|
case PREPARING:
|
||||||
safeStore = commandStore.beginOperation(preLoadContext, context.commands, context.timestampsForKey, context.commandsForKey);
|
safeStore = commandStore.beginOperation(preLoadContext, context.commands, context.timestampsForKey, context.commandsForKey, context.commandsForRanges);
|
||||||
state(RUNNING);
|
state(RUNNING);
|
||||||
case RUNNING:
|
case RUNNING:
|
||||||
result = apply(safeStore);
|
result = apply(safeStore);
|
||||||
safeStore.postExecute(context.commands, context.timestampsForKey, context.commandsForKey);
|
safeStore.postExecute(context.commands, context.timestampsForKey, context.commandsForKey, context.commandsForRanges);
|
||||||
context.releaseResources(commandStore);
|
context.releaseResources(commandStore);
|
||||||
commandStore.completeOperation(safeStore);
|
commandStore.completeOperation(safeStore);
|
||||||
commandStore.executionOrder().unregister(this);
|
commandStore.executionOrder().unregister(this);
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,20 @@
|
||||||
package org.apache.cassandra.service.accord.async;
|
package org.apache.cassandra.service.accord.async;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import accord.primitives.RoutableKey;
|
import accord.api.Key;
|
||||||
|
import accord.api.RoutingKey;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.primitives.Seekable;
|
||||||
import accord.primitives.TxnId;
|
import accord.primitives.TxnId;
|
||||||
import accord.utils.Invariants;
|
import accord.utils.Invariants;
|
||||||
import org.agrona.collections.Object2ObjectHashMap;
|
import org.agrona.collections.Object2ObjectHashMap;
|
||||||
|
import org.apache.cassandra.service.accord.RangeTreeRangeAccessor;
|
||||||
|
import org.apache.cassandra.utils.RTree;
|
||||||
|
import org.apache.cassandra.utils.RangeTree;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assists with correct ordering of {@link AsyncOperation} execution wrt each other,
|
* Assists with correct ordering of {@link AsyncOperation} execution wrt each other,
|
||||||
|
|
@ -30,7 +39,85 @@ import org.agrona.collections.Object2ObjectHashMap;
|
||||||
*/
|
*/
|
||||||
public class ExecutionOrder
|
public class ExecutionOrder
|
||||||
{
|
{
|
||||||
|
private static class Conflicts
|
||||||
|
{
|
||||||
|
private final List<Key> keyConflicts;
|
||||||
|
private final List<Range> rangeConflicts;
|
||||||
|
|
||||||
|
private Conflicts(List<Key> keyConflicts, List<Range> rangeConflicts)
|
||||||
|
{
|
||||||
|
this.keyConflicts = keyConflicts;
|
||||||
|
this.rangeConflicts = rangeConflicts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private class RangeState
|
||||||
|
{
|
||||||
|
private final Range range;
|
||||||
|
private final IdentityHashMap<AsyncOperation<?>, Conflicts> operationToConflicts = new IdentityHashMap<>();
|
||||||
|
private Object operationOrQueue;
|
||||||
|
|
||||||
|
public RangeState(Range range, List<Key> keyConflicts, List<Range> rangeConflicts, AsyncOperation<?> operation)
|
||||||
|
{
|
||||||
|
this.range = range;
|
||||||
|
this.operationOrQueue = operation;
|
||||||
|
add(operation, keyConflicts, rangeConflicts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(AsyncOperation<?> operation, List<Key> keyConflicts, List<Range> rangeConflicts)
|
||||||
|
{
|
||||||
|
operationToConflicts.put(operation, new Conflicts(keyConflicts, rangeConflicts));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canRun(AsyncOperation<?> operation)
|
||||||
|
{
|
||||||
|
if (operationOrQueue instanceof AsyncOperation<?>)
|
||||||
|
{
|
||||||
|
Invariants.checkState(operationOrQueue == operation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ArrayDeque<AsyncOperation<?>> queue = (ArrayDeque<AsyncOperation<?>>) operationOrQueue;
|
||||||
|
return queue.peek() == operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Conflicts remove(AsyncOperation<?> operation)
|
||||||
|
{
|
||||||
|
if (operationOrQueue instanceof AsyncOperation<?>)
|
||||||
|
{
|
||||||
|
Invariants.checkState(operationOrQueue == operation);
|
||||||
|
rangeQueues.remove(range);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArrayDeque<AsyncOperation<?>> queue = (ArrayDeque<AsyncOperation<?>>) operationOrQueue;
|
||||||
|
AsyncOperation<?> head = queue.poll();
|
||||||
|
Invariants.checkState(head == operation);
|
||||||
|
|
||||||
|
if (queue.isEmpty())
|
||||||
|
{
|
||||||
|
rangeQueues.remove(range);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
head = queue.peek();
|
||||||
|
if (canRun(head))
|
||||||
|
head.onUnblocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return operationToConflicts.remove(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Conflicts conflicts(AsyncOperation<?> operation)
|
||||||
|
{
|
||||||
|
return operationToConflicts.get(operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final Object2ObjectHashMap<Object, Object> queues = new Object2ObjectHashMap<>();
|
private final Object2ObjectHashMap<Object, Object> queues = new Object2ObjectHashMap<>();
|
||||||
|
private final RangeTree<RoutingKey, Range, RangeState> rangeQueues = RTree.create(RangeTreeRangeAccessor.instance);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an operation as having a dependency on its keys and TxnIds
|
* Register an operation as having a dependency on its keys and TxnIds
|
||||||
|
|
@ -39,14 +126,88 @@ public class ExecutionOrder
|
||||||
boolean register(AsyncOperation<?> operation)
|
boolean register(AsyncOperation<?> operation)
|
||||||
{
|
{
|
||||||
boolean canRun = true;
|
boolean canRun = true;
|
||||||
for (RoutableKey key : operation.keys())
|
for (Seekable seekable : operation.keys())
|
||||||
canRun &= register(key, operation);
|
{
|
||||||
|
switch (seekable.domain())
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
canRun &= register(seekable.asKey(), operation);
|
||||||
|
break;
|
||||||
|
case Range:
|
||||||
|
canRun &= register(seekable.asRange(), operation);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unexpected domain: " + seekable.domain());
|
||||||
|
}
|
||||||
|
}
|
||||||
TxnId primaryTxnId = operation.primaryTxnId();
|
TxnId primaryTxnId = operation.primaryTxnId();
|
||||||
if (null != primaryTxnId)
|
if (null != primaryTxnId)
|
||||||
canRun &= register(primaryTxnId, operation);
|
canRun &= register(primaryTxnId, operation);
|
||||||
return canRun;
|
return canRun;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean register(Range range, AsyncOperation<?> operation)
|
||||||
|
{
|
||||||
|
// Ranges depend on Ranges and Keys
|
||||||
|
// Keys depend on Keys...
|
||||||
|
// This adds a complication to this logic as keys should be able to make progress regardless of ranges, but rangest must depend on keys
|
||||||
|
List<Key> keyConflicts = null;
|
||||||
|
for (Object o : queues.keySet())
|
||||||
|
{
|
||||||
|
if (!(o instanceof Key))
|
||||||
|
continue;
|
||||||
|
Key key = (Key) o;
|
||||||
|
if (!range.contains(key))
|
||||||
|
continue;
|
||||||
|
if (keyConflicts == null)
|
||||||
|
keyConflicts = new ArrayList<>();
|
||||||
|
keyConflicts.add(key);
|
||||||
|
}
|
||||||
|
if (keyConflicts != null)
|
||||||
|
keyConflicts.forEach(k -> register(k, operation));
|
||||||
|
|
||||||
|
class Result
|
||||||
|
{
|
||||||
|
RangeState sameRange = null;
|
||||||
|
List<Range> rangeConflicts = null;
|
||||||
|
}
|
||||||
|
Result result = new Result();
|
||||||
|
rangeQueues.search(range, e -> {
|
||||||
|
if (range.equals(e.getKey()))
|
||||||
|
result.sameRange = e.getValue();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (result.rangeConflicts == null)
|
||||||
|
result.rangeConflicts = new ArrayList<>();
|
||||||
|
result.rangeConflicts.add(e.getKey());
|
||||||
|
}
|
||||||
|
RangeState state = e.getValue();
|
||||||
|
Object operationOrQueue = state.operationOrQueue;
|
||||||
|
if (operationOrQueue instanceof AsyncOperation)
|
||||||
|
{
|
||||||
|
ArrayDeque<AsyncOperation<?>> queue = new ArrayDeque<>(4);
|
||||||
|
queue.add((AsyncOperation<?>) operationOrQueue);
|
||||||
|
queue.add(operation);
|
||||||
|
state.operationOrQueue = queue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArrayDeque<AsyncOperation<?>> queue = (ArrayDeque<AsyncOperation<?>>) operationOrQueue;
|
||||||
|
queue.add(operation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (result.sameRange != null)
|
||||||
|
{
|
||||||
|
result.sameRange.add(operation, keyConflicts, result.rangeConflicts);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rangeQueues.add(range, new RangeState(range, keyConflicts, result.rangeConflicts, operation));
|
||||||
|
}
|
||||||
|
return keyConflicts == null && result.rangeConflicts == null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an operation as having a dependency on a key or a TxnId
|
* Register an operation as having a dependency on a key or a TxnId
|
||||||
* @return true if no other operation depends on the key/TxnId, false otherwise
|
* @return true if no other operation depends on the key/TxnId, false otherwise
|
||||||
|
|
@ -81,13 +242,36 @@ public class ExecutionOrder
|
||||||
*/
|
*/
|
||||||
void unregister(AsyncOperation<?> operation)
|
void unregister(AsyncOperation<?> operation)
|
||||||
{
|
{
|
||||||
for (RoutableKey key : operation.keys())
|
for (Seekable seekable : operation.keys())
|
||||||
unregister(key, operation);
|
{
|
||||||
|
switch (seekable.domain())
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
unregister(seekable.asKey(), operation);
|
||||||
|
break;
|
||||||
|
case Range:
|
||||||
|
unregister(seekable.asRange(), operation);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unexpected domain: " + seekable.domain());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
TxnId primaryTxnId = operation.primaryTxnId();
|
TxnId primaryTxnId = operation.primaryTxnId();
|
||||||
if (null != primaryTxnId)
|
if (null != primaryTxnId)
|
||||||
unregister(primaryTxnId, operation);
|
unregister(primaryTxnId, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void unregister(Range range, AsyncOperation<?> operation)
|
||||||
|
{
|
||||||
|
RangeState state = state(range);
|
||||||
|
Conflicts conflicts = state.remove(operation);
|
||||||
|
if (conflicts.rangeConflicts != null)
|
||||||
|
conflicts.rangeConflicts.forEach(r -> state(r).remove(operation));
|
||||||
|
if (conflicts.keyConflicts != null)
|
||||||
|
conflicts.keyConflicts.forEach(k -> unregister(k, operation));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister the operation as being a dependency for key or TxnId
|
* Unregister the operation as being a dependency for key or TxnId
|
||||||
*/
|
*/
|
||||||
|
|
@ -123,14 +307,60 @@ public class ExecutionOrder
|
||||||
|
|
||||||
boolean canRun(AsyncOperation<?> operation)
|
boolean canRun(AsyncOperation<?> operation)
|
||||||
{
|
{
|
||||||
for (RoutableKey key : operation.keys())
|
for (Seekable seekable : operation.keys())
|
||||||
if (!canRun(key, operation))
|
{
|
||||||
|
switch (seekable.domain())
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
if (!canRun(seekable.asKey(), operation))
|
||||||
return false;
|
return false;
|
||||||
|
break;
|
||||||
|
case Range:
|
||||||
|
if (!canRun(seekable.asRange(), operation))
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unexpected domain: " + seekable.domain());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
TxnId primaryTxnId = operation.primaryTxnId();
|
TxnId primaryTxnId = operation.primaryTxnId();
|
||||||
return primaryTxnId == null || canRun(primaryTxnId, operation);
|
return primaryTxnId == null || canRun(primaryTxnId, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canRun(Range range, AsyncOperation<?> operation)
|
||||||
|
{
|
||||||
|
RangeState state = state(range);
|
||||||
|
if (!state.canRun(operation))
|
||||||
|
return false;
|
||||||
|
Conflicts conflicts = state.conflicts(operation);
|
||||||
|
if (conflicts.rangeConflicts != null)
|
||||||
|
{
|
||||||
|
for (Range r : conflicts.rangeConflicts)
|
||||||
|
{
|
||||||
|
if (!state(r).canRun(operation))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (conflicts.keyConflicts != null)
|
||||||
|
{
|
||||||
|
for (Key key : conflicts.keyConflicts)
|
||||||
|
{
|
||||||
|
if (!canRun(key, operation))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RangeState state(Range range)
|
||||||
|
{
|
||||||
|
List<RangeState> list = rangeQueues.get(range);
|
||||||
|
assert list.size() == 1 : String.format("Expected 1 element but saw list %s", list);
|
||||||
|
return list.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean canRun(Object keyOrTxnId, AsyncOperation<?> operation)
|
private boolean canRun(Object keyOrTxnId, AsyncOperation<?> operation)
|
||||||
{
|
{
|
||||||
Object operationOrQueue = queues.get(keyOrTxnId);
|
Object operationOrQueue = queues.get(keyOrTxnId);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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.events;
|
||||||
|
|
||||||
|
import jdk.jfr.Category;
|
||||||
|
import jdk.jfr.DataAmount;
|
||||||
|
import jdk.jfr.Event;
|
||||||
|
import jdk.jfr.Label;
|
||||||
|
import jdk.jfr.Name;
|
||||||
|
import jdk.jfr.Percentage;
|
||||||
|
import jdk.jfr.StackTrace;
|
||||||
|
|
||||||
|
@Category({"Accord", "Accord Cache"})
|
||||||
|
@StackTrace(false)
|
||||||
|
public abstract class CacheEvents extends Event
|
||||||
|
{
|
||||||
|
public int store;
|
||||||
|
public String instance;
|
||||||
|
public String key;
|
||||||
|
public String status;
|
||||||
|
@DataAmount(DataAmount.BYTES)
|
||||||
|
public int lastQueriedEstimatedSizeOnHeap;
|
||||||
|
|
||||||
|
// instance
|
||||||
|
@DataAmount(DataAmount.BYTES)
|
||||||
|
public long instanceAllocated;
|
||||||
|
public long instanceStatsQueries, instanceStatsHits, instanceStatsMisses;
|
||||||
|
|
||||||
|
@Percentage
|
||||||
|
public double instanceStatsHitRate;
|
||||||
|
|
||||||
|
// cache
|
||||||
|
@DataAmount(DataAmount.BYTES)
|
||||||
|
public long globalCapacity, globalAllocated;
|
||||||
|
public int globalSize, globalReferenced, globalUnreferenced;
|
||||||
|
|
||||||
|
public long globalStatsQueries, globalStatsHits, globalStatsMisses;
|
||||||
|
|
||||||
|
@Percentage
|
||||||
|
public double globalStatsHitRate;
|
||||||
|
|
||||||
|
@Percentage
|
||||||
|
public double globalFree;
|
||||||
|
public void update()
|
||||||
|
{
|
||||||
|
instanceStatsHitRate = 1D - (instanceStatsHits / (double) instanceStatsQueries);
|
||||||
|
globalStatsHitRate = 1D - (globalStatsHits / (double) globalStatsQueries);
|
||||||
|
globalFree = 1.0D - (globalAllocated / (double) globalCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Name("cassandra.accord.cache.Add")
|
||||||
|
@Label("Accord Cache Add")
|
||||||
|
public static class Add extends CacheEvents { }
|
||||||
|
|
||||||
|
@Name("cassandra.accord.cache.Release")
|
||||||
|
@Label("Accord Cache Release")
|
||||||
|
public static class Release extends CacheEvents { }
|
||||||
|
|
||||||
|
@Name("cassandra.accord.cache.Evict")
|
||||||
|
@Label("Accord Cache Evict")
|
||||||
|
public static class Evict extends CacheEvents { }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
/*
|
||||||
|
* 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.serializers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
|
||||||
|
import org.apache.cassandra.db.marshal.LongType;
|
||||||
|
import org.apache.cassandra.db.marshal.ValueAccessor;
|
||||||
|
import org.apache.cassandra.dht.IPartitioner;
|
||||||
|
import org.apache.cassandra.dht.Token;
|
||||||
|
import org.apache.cassandra.schema.TableId;
|
||||||
|
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
||||||
|
import org.apache.cassandra.utils.ByteArrayUtil;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteSource;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.service.accord.api.AccordRoutingKey.RoutingKeyKind.SENTINEL;
|
||||||
|
|
||||||
|
public class AccordRoutingKeyByteSource
|
||||||
|
{
|
||||||
|
private static final byte[] MIN_ORDER = { -1 };
|
||||||
|
private static final byte[] TOKEN_ORDER = { 0 };
|
||||||
|
private static final byte[] MAX_ORDER = { 1 };
|
||||||
|
|
||||||
|
private static ByteSource minPrefix()
|
||||||
|
{
|
||||||
|
return ByteSource.signedFixedLengthNumber(ByteArrayAccessor.instance, MIN_ORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteSource tokenPrefix()
|
||||||
|
{
|
||||||
|
return ByteSource.signedFixedLengthNumber(ByteArrayAccessor.instance, TOKEN_ORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteSource maxPrefix()
|
||||||
|
{
|
||||||
|
return ByteSource.signedFixedLengthNumber(ByteArrayAccessor.instance, MAX_ORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Serializer create(IPartitioner partitioner)
|
||||||
|
{
|
||||||
|
if (partitioner.isFixedLength())
|
||||||
|
return new FixedLength(partitioner, ByteComparable.Version.OSS50);
|
||||||
|
return new VariableLength(partitioner, ByteComparable.Version.OSS50);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FixedLength fixedLength(IPartitioner partitioner)
|
||||||
|
{
|
||||||
|
return new FixedLength(partitioner, ByteComparable.Version.OSS50);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VariableLength variableLength(IPartitioner partitioner)
|
||||||
|
{
|
||||||
|
return new VariableLength(partitioner, ByteComparable.Version.OSS50);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class Serializer
|
||||||
|
{
|
||||||
|
protected final IPartitioner partitioner;
|
||||||
|
protected final ByteComparable.Version version;
|
||||||
|
protected final byte[] empty;
|
||||||
|
|
||||||
|
protected Serializer(IPartitioner partitioner, ByteComparable.Version version, byte[] empty)
|
||||||
|
{
|
||||||
|
this.partitioner = partitioner;
|
||||||
|
this.version = version;
|
||||||
|
this.empty = empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteSource minAsComparableBytes()
|
||||||
|
{
|
||||||
|
return ByteSource.withTerminator(ByteSource.TERMINATOR, minPrefix(), ByteSource.fixedLength(empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteSource maxAsComparableBytes()
|
||||||
|
{
|
||||||
|
return ByteSource.withTerminator(ByteSource.TERMINATOR, maxPrefix(), ByteSource.fixedLength(empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteSource asComparableBytes(Token token)
|
||||||
|
{
|
||||||
|
if (token.getPartitioner() != partitioner)
|
||||||
|
throw new IllegalArgumentException("Attempted to use the wrong partitioner: given " + token.getPartitioner() + " but expected " + partitioner);
|
||||||
|
return ByteSource.withTerminator(ByteSource.TERMINATOR, tokenPrefix(), token.asComparableBytes(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
public <V> Token tokenFromComparableBytes(ValueAccessor<V> accessor, V data) throws IOException
|
||||||
|
{
|
||||||
|
return tokenFromComparableBytes(ByteSource.peekable(ByteSource.fixedLength(accessor, data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Token tokenFromComparableBytes(ByteSource.Peekable bs) throws IOException
|
||||||
|
{
|
||||||
|
if (bs.peek() == ByteSource.TERMINATOR)
|
||||||
|
throw new IOException("Unable to read prefix");
|
||||||
|
ByteSource.Peekable component = progress(bs);
|
||||||
|
|
||||||
|
byte[] prefix = ByteSourceInverse.getOptionalSignedFixedLength(ByteArrayAccessor.instance, component, 1);
|
||||||
|
if (prefix == null)
|
||||||
|
throw new IOException("Unable to read prefix; prefix was null");
|
||||||
|
if (!Arrays.equals(TOKEN_ORDER, prefix))
|
||||||
|
{
|
||||||
|
String match = Arrays.equals(MIN_ORDER, prefix) ? "min"
|
||||||
|
: Arrays.equals(MAX_ORDER, prefix) ? "max"
|
||||||
|
: "unknown";
|
||||||
|
throw new IOException("Attempt to read token from non-token value: was " + match);
|
||||||
|
}
|
||||||
|
component = ByteSourceInverse.nextComponentSource(bs);
|
||||||
|
if (component == null)
|
||||||
|
throw new IOException("Unable to read token; component was not found");
|
||||||
|
return partitioner.getTokenFactory().fromComparableBytes(component, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteSource asComparableBytes(AccordRoutingKey key)
|
||||||
|
{
|
||||||
|
UUID uuid = key.table().asUUID();
|
||||||
|
ByteSource[] srcs = { LongType.instance.asComparableBytes(LongType.instance.decompose(uuid.getMostSignificantBits()), ByteComparable.Version.OSS50),
|
||||||
|
LongType.instance.asComparableBytes(LongType.instance.decompose(uuid.getLeastSignificantBits()), ByteComparable.Version.OSS50),
|
||||||
|
asComparableBytesNoTable(key) };
|
||||||
|
return ByteSource.withTerminator(ByteSource.TERMINATOR, srcs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteSource asComparableBytesNoTable(AccordRoutingKey key)
|
||||||
|
{
|
||||||
|
return key.kindOfRoutingKey() == SENTINEL ? key.asSentinelKey().isMin ? minAsComparableBytes() : maxAsComparableBytes()
|
||||||
|
: asComparableBytes(key.token());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <V> AccordRoutingKey fromComparableBytes(ValueAccessor<V> accessor, V data) throws IOException
|
||||||
|
{
|
||||||
|
ByteSource.Peekable bs = ByteSource.peekable(ByteSource.fixedLength(accessor, data));
|
||||||
|
long[] uuidValues = new long[2];
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
if (bs.peek() == ByteSource.TERMINATOR)
|
||||||
|
throw new IllegalArgumentException("Unable to parse bytes");
|
||||||
|
ByteSource.Peekable component = ByteSourceInverse.nextComponentSource(bs);
|
||||||
|
long value = LongType.instance.compose(LongType.instance.fromComparableBytes(component, ByteComparable.Version.OSS50));
|
||||||
|
uuidValues[i] = value;
|
||||||
|
}
|
||||||
|
TableId tableId = TableId.fromUUID(new UUID(uuidValues[0], uuidValues[1]));
|
||||||
|
return fromComparableBytes(bs,
|
||||||
|
isMin -> isMin ? AccordRoutingKey.SentinelKey.min(tableId) : AccordRoutingKey.SentinelKey.max(tableId),
|
||||||
|
token -> new AccordRoutingKey.TokenKey(tableId, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccordRoutingKey fromComparableBytes(ByteSource.Peekable bs,
|
||||||
|
Function<Boolean, AccordRoutingKey> onSentinel,
|
||||||
|
Function<Token, AccordRoutingKey> onToken) throws IOException
|
||||||
|
{
|
||||||
|
if (bs.peek() == ByteSource.TERMINATOR)
|
||||||
|
throw new IOException("Unable to read prefix");
|
||||||
|
ByteSource.Peekable component = progress(bs);
|
||||||
|
|
||||||
|
byte[] prefix = ByteSourceInverse.getOptionalSignedFixedLength(ByteArrayAccessor.instance, component, 1);
|
||||||
|
if (prefix == null)
|
||||||
|
throw new IOException("Unable to read prefix; prefix was null");
|
||||||
|
if (Arrays.equals(TOKEN_ORDER, prefix))
|
||||||
|
{
|
||||||
|
component = ByteSourceInverse.nextComponentSource(bs);
|
||||||
|
if (component == null)
|
||||||
|
throw new IOException("Unable to read token; component was not found");
|
||||||
|
return onToken.apply(partitioner.getTokenFactory().fromComparableBytes(component, version));
|
||||||
|
}
|
||||||
|
if (Arrays.equals(MIN_ORDER, prefix))
|
||||||
|
return onSentinel.apply(true);
|
||||||
|
if (Arrays.equals(MAX_ORDER, prefix))
|
||||||
|
return onSentinel.apply(false);
|
||||||
|
throw new AssertionError("Unknown prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteSource.Peekable progress(ByteSource.Peekable bs) throws IOException
|
||||||
|
{
|
||||||
|
ByteSource.Peekable component = ByteSourceInverse.nextComponentSource(bs);
|
||||||
|
if (component == null)
|
||||||
|
throw new IOException("Unable to read prefix; component was not found");
|
||||||
|
if (component.peek() == ByteSource.NEXT_COMPONENT)
|
||||||
|
{
|
||||||
|
// this came from (table, token_or_sentinel)
|
||||||
|
component = ByteSourceInverse.nextComponentSource(bs);
|
||||||
|
if (component == null)
|
||||||
|
throw new IOException("Unable to read prefix; component was not found");
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serialize(Token token)
|
||||||
|
{
|
||||||
|
return ByteSourceInverse.readBytes(asComparableBytes(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serialize(AccordRoutingKey key)
|
||||||
|
{
|
||||||
|
return ByteSourceInverse.readBytes(asComparableBytes(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serializeNoTable(AccordRoutingKey key)
|
||||||
|
{
|
||||||
|
return ByteSourceInverse.readBytes(asComparableBytesNoTable(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class VariableLength extends Serializer
|
||||||
|
{
|
||||||
|
public VariableLength(IPartitioner partitioner, ByteComparable.Version version)
|
||||||
|
{
|
||||||
|
super(partitioner, version, ByteArrayUtil.EMPTY_BYTE_ARRAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FixedLength extends Serializer
|
||||||
|
{
|
||||||
|
public FixedLength(IPartitioner partitioner, ByteComparable.Version version)
|
||||||
|
{
|
||||||
|
super(partitioner, version, computeEmptyBytes(partitioner, version));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] computeEmptyBytes(IPartitioner partitioner, ByteComparable.Version version)
|
||||||
|
{
|
||||||
|
if (!partitioner.isFixedLength())
|
||||||
|
throw new IllegalArgumentException("Unable to use partitioner " + partitioner.getClass() + "; it is not fixed-length");
|
||||||
|
|
||||||
|
int tokenSize = ByteSourceInverse.readBytes(partitioner.getMinimumToken().asComparableBytes(version)).length;
|
||||||
|
return new byte[tokenSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int valueSize()
|
||||||
|
{
|
||||||
|
return 4 + empty.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -107,6 +107,14 @@ public interface Clock
|
||||||
{
|
{
|
||||||
return instance.currentTimeMillis();
|
return instance.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantically equivalent to {@link FBUtilities#nowInSeconds()}
|
||||||
|
*/
|
||||||
|
public static long nowInSeconds()
|
||||||
|
{
|
||||||
|
return instance.nowInSeconds();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Default implements Clock
|
public static class Default implements Clock
|
||||||
|
|
|
||||||
|
|
@ -66,5 +66,4 @@ public interface CloseableIterator<T> extends Iterator<T>, AutoCloseable
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import java.util.List;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
@ -187,6 +189,11 @@ public class IntervalTree<C extends Comparable<? super C>, D extends Comparable<
|
||||||
return search(Interval.<C, D>create(point, point, null));
|
return search(Interval.<C, D>create(point, point, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<D> search(C start, C end)
|
||||||
|
{
|
||||||
|
return search(Interval.<C, D>create(start, end, null));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The input arrays aren't defensively copied and will be sorted. The update method doesn't allow duplicates or elements to be removed
|
* The input arrays aren't defensively copied and will be sorted. The update method doesn't allow duplicates or elements to be removed
|
||||||
* to be missing and this differs from the constructor which does not duplicate checking at all.
|
* to be missing and this differs from the constructor which does not duplicate checking at all.
|
||||||
|
|
@ -312,6 +319,11 @@ public class IntervalTree<C extends Comparable<? super C>, D extends Comparable<
|
||||||
return new TreeIterator(head);
|
return new TreeIterator(head);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Stream<I> stream()
|
||||||
|
{
|
||||||
|
return StreamSupport.stream(spliterator(), false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
|
|
@ -555,11 +567,6 @@ public class IntervalTree<C extends Comparable<? super C>, D extends Comparable<
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface TriPredicate<A, B, C>
|
|
||||||
{
|
|
||||||
boolean test(A a, B b, C c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder<C, D, I> removeIf(TriPredicate<C, C, D> predicate)
|
public Builder<C, D, I> removeIf(TriPredicate<C, C, D> predicate)
|
||||||
{
|
{
|
||||||
intervals.removeIf(i -> predicate.test(i.min, i.max, i.data));
|
intervals.removeIf(i -> predicate.test(i.min, i.max, i.data));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class MutableEntry<K, V> implements Map.Entry<K, V>
|
||||||
|
{
|
||||||
|
private final K k;
|
||||||
|
private V v;
|
||||||
|
|
||||||
|
public MutableEntry(K k, V v)
|
||||||
|
{
|
||||||
|
this.k = k;
|
||||||
|
this.v = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public K getKey()
|
||||||
|
{
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V getValue()
|
||||||
|
{
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V setValue(V value)
|
||||||
|
{
|
||||||
|
V previous = v;
|
||||||
|
v = Objects.requireNonNull(value);
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || !(o instanceof Map.Entry)) return false;
|
||||||
|
Map.Entry<?, ?> that = (Map.Entry<?, ?>) o;
|
||||||
|
return Objects.equals(k, that.getKey()) && Objects.equals(v, that.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hash(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return k + "=" + v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,535 @@
|
||||||
|
/*
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
import javax.annotation.CheckForNull;
|
||||||
|
|
||||||
|
import com.google.common.collect.AbstractIterator;
|
||||||
|
|
||||||
|
public class RTree<Token, Range, Value> implements RangeTree<Token, Range, Value>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tuning size target can be tricky as it is based on expected access patterns and expected matche sizes. There is also
|
||||||
|
* a memory cost to account for as large tree sizes will have far more nodes with a small target than a large target.
|
||||||
|
*
|
||||||
|
* If matching most of the data then larger sizes leads to fewer hops
|
||||||
|
* If matching few elements then tree depth maters the most, if walking a long tree is more costly than walking the
|
||||||
|
* element list, then shrinking depth (by having larger size target) can improve performance.
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_SIZE_TARGET = 1 << 7;
|
||||||
|
private static final int DEFAULT_NUMBER_OF_CHILDREN = 6;
|
||||||
|
|
||||||
|
private final Comparator<Token> comparator;
|
||||||
|
private final Accessor<Token, Range> accessor;
|
||||||
|
private final int sizeTarget;
|
||||||
|
private final int numChildren;
|
||||||
|
private Node node = new Node();
|
||||||
|
|
||||||
|
public RTree(Comparator<Token> comparator, Accessor<Token, Range> accessor)
|
||||||
|
{
|
||||||
|
this(comparator, accessor, DEFAULT_SIZE_TARGET, DEFAULT_NUMBER_OF_CHILDREN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RTree(Comparator<Token> comparator, Accessor<Token, Range> accessor, int sizeTarget, int numChildren)
|
||||||
|
{
|
||||||
|
if (sizeTarget <= 1)
|
||||||
|
throw new IllegalArgumentException("size target must be 2 or more");
|
||||||
|
if (numChildren <= 1)
|
||||||
|
throw new IllegalArgumentException("Number of children must be 2 or more");
|
||||||
|
if (sizeTarget < numChildren)
|
||||||
|
throw new IllegalArgumentException("Size target (" + sizeTarget + ") was less than number of children (" + numChildren + ")");
|
||||||
|
this.comparator = comparator;
|
||||||
|
this.accessor = accessor;
|
||||||
|
this.sizeTarget = sizeTarget;
|
||||||
|
this.numChildren = numChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <Token extends Comparable<? super Token>, Range, Value> RTree<Token, Range, Value> create(Accessor<Token, Range> accessor)
|
||||||
|
{
|
||||||
|
return new RTree<>(Comparator.naturalOrder(), accessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Value> get(Range range)
|
||||||
|
{
|
||||||
|
List<Value> matches = new ArrayList<>();
|
||||||
|
get(range, e -> matches.add(e.getValue()));
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void get(Range range, Consumer<Map.Entry<Range, Value>> onMatch)
|
||||||
|
{
|
||||||
|
node.search(range, onMatch, e -> e.getKey().equals(range), Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Value>> search(Range range)
|
||||||
|
{
|
||||||
|
List<Map.Entry<Range, Value>> matches = new ArrayList<>();
|
||||||
|
search(range, matches::add);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void search(Range range, Consumer<Map.Entry<Range, Value>> onMatch)
|
||||||
|
{
|
||||||
|
node.search(range, onMatch, ignore -> true, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Value> find(Range range)
|
||||||
|
{
|
||||||
|
List<Value> matches = new ArrayList<>();
|
||||||
|
find(range, matches::add);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void find(Range range, Consumer<Value> onMatch)
|
||||||
|
{
|
||||||
|
node.search(range, onMatch, ignore -> true, Map.Entry::getValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Value>> searchToken(Token token)
|
||||||
|
{
|
||||||
|
List<Map.Entry<Range, Value>> matches = new ArrayList<>();
|
||||||
|
searchToken(token, matches::add);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void searchToken(Token token, Consumer<Map.Entry<Range, Value>> onMatch)
|
||||||
|
{
|
||||||
|
node.searchToken(token, onMatch, ignore -> true, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Value> findToken(Token token)
|
||||||
|
{
|
||||||
|
List<Value> matches = new ArrayList<>();
|
||||||
|
findToken(token, matches::add);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void findToken(Token token, Consumer<Value> onMatch)
|
||||||
|
{
|
||||||
|
node.searchToken(token, onMatch, ignore -> true, Map.Entry::getValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean add(Range key, Value value)
|
||||||
|
{
|
||||||
|
node.add(key, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int remove(Range key)
|
||||||
|
{
|
||||||
|
return node.removeIf(e -> e.getKey().equals(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int remove(Range key, Value value)
|
||||||
|
{
|
||||||
|
Map.Entry<Range, Value> match = Map.entry(key, value);
|
||||||
|
return node.removeIf(match::equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear()
|
||||||
|
{
|
||||||
|
node = new Node();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size()
|
||||||
|
{
|
||||||
|
return node.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty()
|
||||||
|
{
|
||||||
|
return node.size == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String displayTree()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
node.displayTree(0, sb);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Map.Entry<Range, Value>> iterator()
|
||||||
|
{
|
||||||
|
return node.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<Map.Entry<Range, Value>> stream()
|
||||||
|
{
|
||||||
|
return StreamSupport.stream(spliterator(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Node implements Iterable<Map.Entry<Range, Value>>
|
||||||
|
{
|
||||||
|
private List<Map.Entry<Range, Value>> values = new ArrayList<>();
|
||||||
|
private List<Node> children = null;
|
||||||
|
private int size = 0;
|
||||||
|
private Token minStart, maxStart, minEnd, maxEnd;
|
||||||
|
|
||||||
|
int removeIf(Predicate<Map.Entry<Range, Value>> condition)
|
||||||
|
{
|
||||||
|
if (minStart == null)
|
||||||
|
return 0;
|
||||||
|
if (children != null)
|
||||||
|
{
|
||||||
|
int sum = 0;
|
||||||
|
for (Node node : children)
|
||||||
|
sum += node.removeIf(condition);
|
||||||
|
size -= sum;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
class Counter {int value;}
|
||||||
|
Counter counter = new Counter();
|
||||||
|
values.removeIf(e -> {
|
||||||
|
if (condition.test(e))
|
||||||
|
{
|
||||||
|
counter.value++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
size -= counter.value;
|
||||||
|
if (values.isEmpty())
|
||||||
|
minStart = maxStart = minEnd = maxEnd = null;
|
||||||
|
return counter.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(Range range, Value value)
|
||||||
|
{
|
||||||
|
size++;
|
||||||
|
if (minStart == null)
|
||||||
|
{
|
||||||
|
minStart = maxStart = accessor.start(range);
|
||||||
|
minEnd = maxEnd = accessor.end(range);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Token start = accessor.start(range);
|
||||||
|
minStart = min(minStart, start);
|
||||||
|
maxStart = max(maxStart, start);
|
||||||
|
Token end = accessor.end(range);
|
||||||
|
minEnd = min(minEnd, end);
|
||||||
|
maxEnd = max(maxEnd, end);
|
||||||
|
}
|
||||||
|
if (children != null)
|
||||||
|
{
|
||||||
|
findBestMatch(range).add(range, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
values.add(new MutableEntry(range, value));
|
||||||
|
if (shouldSplit())
|
||||||
|
split();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node findBestMatch(Range range)
|
||||||
|
{
|
||||||
|
int topIdx = 0;
|
||||||
|
Node node = children.get(0);
|
||||||
|
int topScore = node.score(range);
|
||||||
|
int size = node.size;
|
||||||
|
for (int i = 1; i < children.size(); i++)
|
||||||
|
{
|
||||||
|
node = children.get(i);
|
||||||
|
int score = node.score(range);
|
||||||
|
if (score > topScore || (score == topScore && size > node.size))
|
||||||
|
{
|
||||||
|
topIdx = i;
|
||||||
|
size = node.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children.get(topIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int score(Range range)
|
||||||
|
{
|
||||||
|
if (minStart == null)
|
||||||
|
return 0;
|
||||||
|
if (!intersects(range))
|
||||||
|
return -10;
|
||||||
|
int score = 5; // overlapps
|
||||||
|
if (values != null) // is leaf
|
||||||
|
score += 5;
|
||||||
|
|
||||||
|
int startScore = 0;
|
||||||
|
if (comparator.compare(maxStart, accessor.start(range)) <= 0)
|
||||||
|
startScore += 10;
|
||||||
|
else if (comparator.compare(minStart, accessor.start(range)) <= 0)
|
||||||
|
startScore += 5;
|
||||||
|
|
||||||
|
int endScore = 0;
|
||||||
|
if (comparator.compare(minEnd, accessor.end(range)) >= 0)
|
||||||
|
endScore += 10;
|
||||||
|
else if (comparator.compare(maxEnd, accessor.end(range)) >= 0)
|
||||||
|
endScore += 5;
|
||||||
|
// if fully contained, then add the scores: 10 for largest bounds, 20 for smallest bounds
|
||||||
|
if (!(startScore == 0 || endScore == 0))
|
||||||
|
score += startScore + endScore;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldSplit()
|
||||||
|
{
|
||||||
|
return values.size() > sizeTarget
|
||||||
|
// if the same range is used over and over again, splitting doesn't do much
|
||||||
|
&& !(comparator.compare(minStart, maxStart) == 0
|
||||||
|
&& comparator.compare(minEnd, maxEnd) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<Map.Entry<Range, Value>>> partitionByEnd()
|
||||||
|
{
|
||||||
|
List<Token> allEndpoints = new ArrayList<>(values.size() * 2);
|
||||||
|
for (Map.Entry<Range, Value> a : values)
|
||||||
|
{
|
||||||
|
allEndpoints.add(accessor.start(a.getKey()));
|
||||||
|
allEndpoints.add(accessor.end(a.getKey()));
|
||||||
|
}
|
||||||
|
allEndpoints.sort(comparator);
|
||||||
|
List<Token> maxToken = new ArrayList<>(numChildren);
|
||||||
|
int tick = allEndpoints.size() / numChildren;
|
||||||
|
int offset = tick;
|
||||||
|
for (int i = 0; i < numChildren; i++)
|
||||||
|
{
|
||||||
|
maxToken.add(allEndpoints.get(offset));
|
||||||
|
offset += tick;
|
||||||
|
if (offset >= allEndpoints.size())
|
||||||
|
{
|
||||||
|
maxToken.add(allEndpoints.get(allEndpoints.size() - 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<Map.Entry<Range, Value>>> partitions = new ArrayList<>(numChildren);
|
||||||
|
for (int i = 0; i < numChildren; i++)
|
||||||
|
partitions.add(new ArrayList<>());
|
||||||
|
|
||||||
|
for (Map.Entry<Range, Value> a : values)
|
||||||
|
{
|
||||||
|
Token end = accessor.end(a.getKey());
|
||||||
|
List<Map.Entry<Range, Value>> selected = null;
|
||||||
|
for (int i = 0; i < numChildren; i++)
|
||||||
|
{
|
||||||
|
if (comparator.compare(end, maxToken.get(i)) < 0)
|
||||||
|
{
|
||||||
|
selected = partitions.get(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected == null)
|
||||||
|
selected = partitions.get(partitions.size() - 1);
|
||||||
|
selected.add(a);
|
||||||
|
}
|
||||||
|
int[] sizes = partitions.stream().mapToInt(List::size).toArray();
|
||||||
|
return goodEnough(sizes) ? partitions : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean goodEnough(int[] sizes)
|
||||||
|
{
|
||||||
|
double sum = 0.0;
|
||||||
|
for (int i : sizes)
|
||||||
|
sum += i;
|
||||||
|
double mean = sum / sizes.length;
|
||||||
|
double stddev = 0.0;
|
||||||
|
for (int i : sizes)
|
||||||
|
stddev += Math.pow(i - mean, 2);
|
||||||
|
stddev = Math.sqrt(stddev / sizes.length);
|
||||||
|
return stddev < 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
void split()
|
||||||
|
{
|
||||||
|
children = new ArrayList<>(numChildren);
|
||||||
|
for (int i = 0; i < numChildren; i++)
|
||||||
|
children.add(new Node());
|
||||||
|
|
||||||
|
List<List<Map.Entry<Range, Value>>> partitions = partitionByEnd();
|
||||||
|
if (partitions == null)
|
||||||
|
partitions = partitionEven();
|
||||||
|
for (int i = 0; i < children.size(); i++)
|
||||||
|
{
|
||||||
|
Node c = children.get(i);
|
||||||
|
List<Map.Entry<Range, Value>> entries = partitions.get(i);
|
||||||
|
entries.forEach(e -> c.add(e.getKey(), e.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
values.clear();
|
||||||
|
values = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<Map.Entry<Range, Value>>> partitionEven()
|
||||||
|
{
|
||||||
|
values.sort((a, b) -> {
|
||||||
|
Range left = a.getKey();
|
||||||
|
Range right = b.getKey();
|
||||||
|
int rc = comparator.compare(accessor.start(left), accessor.start(right));
|
||||||
|
if (rc == 0)
|
||||||
|
rc = comparator.compare(accessor.end(left), accessor.end(right));
|
||||||
|
return rc;
|
||||||
|
});
|
||||||
|
List<List<Map.Entry<Range, Value>>> partition = new ArrayList<>(numChildren);
|
||||||
|
int size = Math.max(1, values.size() / numChildren);
|
||||||
|
int offset = 0;
|
||||||
|
for (int i = 0; i < numChildren - 1; i++)
|
||||||
|
{
|
||||||
|
int total = size;
|
||||||
|
partition.add(new ArrayList<>(values.subList(offset, offset + total)));
|
||||||
|
offset += total;
|
||||||
|
}
|
||||||
|
partition.add(new ArrayList<>(values.subList(offset, values.size())));
|
||||||
|
return partition;
|
||||||
|
}
|
||||||
|
|
||||||
|
<T> void search(Range range, Consumer<T> matches, Predicate<Map.Entry<Range, Value>> predicate, Function<Map.Entry<Range, Value>, T> transformer)
|
||||||
|
{
|
||||||
|
if (minStart == null)
|
||||||
|
return;
|
||||||
|
if (!intersects(range))
|
||||||
|
return;
|
||||||
|
if (children != null)
|
||||||
|
{
|
||||||
|
children.forEach(n -> n.search(range, matches, predicate, transformer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
values.forEach(e -> {
|
||||||
|
if (accessor.intersects(e.getKey(), range) && predicate.test(e))
|
||||||
|
matches.accept(transformer.apply(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
<T> void searchToken(Token token, Consumer<T> matches, Predicate<Map.Entry<Range, Value>> predicate, Function<Map.Entry<Range, Value>, T> transformer)
|
||||||
|
{
|
||||||
|
if (minStart == null)
|
||||||
|
return;
|
||||||
|
if (!contains(minStart, maxEnd, token))
|
||||||
|
return;
|
||||||
|
if (children != null)
|
||||||
|
{
|
||||||
|
for (int i = 0, size = children.size(); i < size; i++)
|
||||||
|
{
|
||||||
|
Node node = children.get(i);
|
||||||
|
node.searchToken(token, matches, predicate, transformer);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
values.forEach(e -> {
|
||||||
|
if (accessor.contains(e.getKey(), token) && predicate.test(e))
|
||||||
|
matches.accept(transformer.apply(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean intersects(Range range)
|
||||||
|
{
|
||||||
|
return accessor.intersects(range, minStart, maxEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean contains(Token start, Token end, Token value)
|
||||||
|
{
|
||||||
|
return accessor.contains(start, end, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayTree(int level, StringBuilder sb)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < level; i++)
|
||||||
|
sb.append('\t');
|
||||||
|
sb.append("start:(").append(minStart).append(", ").append(maxStart).append("), end:(").append(minEnd).append(", ").append(maxEnd).append("):");
|
||||||
|
if (children != null)
|
||||||
|
{
|
||||||
|
sb.append('\n');
|
||||||
|
children.forEach(n -> n.displayTree(level + 1, sb));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append(' ').append(size).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "Node{" +
|
||||||
|
"minStart=" + minStart +
|
||||||
|
", maxStart=" + maxStart +
|
||||||
|
", minEnd=" + minEnd +
|
||||||
|
", maxEnd=" + maxEnd +
|
||||||
|
", values=" + values +
|
||||||
|
", children=" + children +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
private Token min(Token a, Token b)
|
||||||
|
{
|
||||||
|
return comparator.compare(a, b) < 0 ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Token max(Token a, Token b)
|
||||||
|
{
|
||||||
|
return comparator.compare(a, b) < 0 ? b : a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Map.Entry<Range, Value>> iterator()
|
||||||
|
{
|
||||||
|
if (values != null)
|
||||||
|
return values.iterator();
|
||||||
|
return new AbstractIterator<>()
|
||||||
|
{
|
||||||
|
private int index = 0;
|
||||||
|
private Iterator<Map.Entry<Range, Value>> it = null;
|
||||||
|
@CheckForNull
|
||||||
|
@Override
|
||||||
|
protected Map.Entry<Range, Value> computeNext()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (it == null)
|
||||||
|
{
|
||||||
|
if (index == children.size())
|
||||||
|
return endOfData();
|
||||||
|
it = children.get(index++).iterator();
|
||||||
|
}
|
||||||
|
if (it.hasNext())
|
||||||
|
return it.next();
|
||||||
|
it = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
public interface RangeTree<Token, Range, Value> extends Iterable<Map.Entry<Range, Value>>
|
||||||
|
{
|
||||||
|
void searchToken(Token token, Consumer<Map.Entry<Range, Value>> onMatch);
|
||||||
|
|
||||||
|
boolean add(Range key, Value value);
|
||||||
|
|
||||||
|
List<Value> get(Range range);
|
||||||
|
|
||||||
|
void get(Range range, Consumer<Map.Entry<Range, Value>> onMatch);
|
||||||
|
|
||||||
|
List<Map.Entry<Range, Value>> search(Range range);
|
||||||
|
|
||||||
|
void search(Range range, Consumer<Map.Entry<Range, Value>> onMatch);
|
||||||
|
|
||||||
|
List<Map.Entry<Range, Value>> searchToken(Token token);
|
||||||
|
|
||||||
|
int remove(Range key);
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
int size();
|
||||||
|
|
||||||
|
boolean isEmpty();
|
||||||
|
|
||||||
|
default Stream<Map.Entry<Range, Value>> stream()
|
||||||
|
{
|
||||||
|
return StreamSupport.stream(spliterator(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Accessor<Token, Range>
|
||||||
|
{
|
||||||
|
Token start(Range range);
|
||||||
|
Token end(Range range);
|
||||||
|
boolean contains(Token start, Token end, Token token);
|
||||||
|
default boolean contains(Range range, Token token)
|
||||||
|
{
|
||||||
|
return contains(start(range), end(range), token);
|
||||||
|
}
|
||||||
|
boolean intersects(Range range, Token start, Token end);
|
||||||
|
default boolean intersects(Range left, Range right)
|
||||||
|
{
|
||||||
|
return intersects(left, start(right), end(right));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
public interface TriPredicate<A, B, C>
|
||||||
|
{
|
||||||
|
boolean test(A a, B b, C c);
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,8 @@ import org.apache.cassandra.locator.InetAddressAndPort;
|
||||||
import org.apache.cassandra.locator.NetworkTopologyProximity;
|
import org.apache.cassandra.locator.NetworkTopologyProximity;
|
||||||
import org.apache.cassandra.locator.SimpleSeedProvider;
|
import org.apache.cassandra.locator.SimpleSeedProvider;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.config.CassandraRelevantProperties.DTEST_ACCORD_ENABLED;
|
||||||
|
|
||||||
public class InstanceConfig implements IInstanceConfig
|
public class InstanceConfig implements IInstanceConfig
|
||||||
{
|
{
|
||||||
public final int num;
|
public final int num;
|
||||||
|
|
@ -323,7 +325,7 @@ public class InstanceConfig implements IInstanceConfig
|
||||||
{
|
{
|
||||||
int seedNode = provisionStrategy.seedNodeNum();
|
int seedNode = provisionStrategy.seedNodeNum();
|
||||||
AccordSpec accordSpec = new AccordSpec();
|
AccordSpec accordSpec = new AccordSpec();
|
||||||
accordSpec.enabled = true;
|
accordSpec.enabled = DTEST_ACCORD_ENABLED.getBoolean();
|
||||||
accordSpec.journal_directory = String.format("%s/node%d/accord_journal", root, nodeNum);
|
accordSpec.journal_directory = String.format("%s/node%d/accord_journal", root, nodeNum);
|
||||||
accordSpec.shard_count = new OptionaldPositiveInt(4);
|
accordSpec.shard_count = new OptionaldPositiveInt(4);
|
||||||
return new InstanceConfig(nodeNum,
|
return new InstanceConfig(nodeNum,
|
||||||
|
|
|
||||||
|
|
@ -18,41 +18,14 @@
|
||||||
|
|
||||||
package org.apache.cassandra.distributed.test.accord;
|
package org.apache.cassandra.distributed.test.accord;
|
||||||
|
|
||||||
import java.io.IOException;
|
import accord.primitives.Unseekables;
|
||||||
import java.nio.ByteBuffer;
|
import accord.topology.Topologies;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.Parameterized;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import accord.primitives.Unseekables;
|
|
||||||
import accord.topology.Topologies;
|
|
||||||
import org.apache.cassandra.cql3.CQLTester;
|
import org.apache.cassandra.cql3.CQLTester;
|
||||||
import org.apache.cassandra.cql3.functions.types.utils.Bytes;
|
import org.apache.cassandra.cql3.functions.types.utils.Bytes;
|
||||||
import org.apache.cassandra.db.marshal.Int32Type;
|
import org.apache.cassandra.db.marshal.*;
|
||||||
import org.apache.cassandra.db.marshal.ListType;
|
|
||||||
import org.apache.cassandra.db.marshal.MapType;
|
|
||||||
import org.apache.cassandra.db.marshal.SetType;
|
|
||||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
|
||||||
import org.apache.cassandra.distributed.Cluster;
|
import org.apache.cassandra.distributed.Cluster;
|
||||||
import org.apache.cassandra.distributed.api.ConsistencyLevel;
|
import org.apache.cassandra.distributed.api.ConsistencyLevel;
|
||||||
import org.apache.cassandra.distributed.api.ICoordinator;
|
import org.apache.cassandra.distributed.api.ICoordinator;
|
||||||
|
|
@ -64,18 +37,34 @@ import org.apache.cassandra.service.accord.AccordTestUtils;
|
||||||
import org.apache.cassandra.service.consensus.TransactionalMode;
|
import org.apache.cassandra.service.consensus.TransactionalMode;
|
||||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.apache.cassandra.cql3.CQLTester.row;
|
import static org.apache.cassandra.cql3.CQLTester.row;
|
||||||
import static org.apache.cassandra.distributed.util.QueryResultUtil.assertThat;
|
import static org.apache.cassandra.distributed.util.QueryResultUtil.assertThat;
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.*;
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
|
|
||||||
@RunWith(Parameterized.class)
|
public abstract class AccordCQLTestBase extends AccordTestBase
|
||||||
public class AccordCQLTest extends AccordTestBase
|
|
||||||
{
|
{
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AccordCQLTest.class);
|
private static final Logger logger = LoggerFactory.getLogger(AccordCQLTestBase.class);
|
||||||
|
|
||||||
|
private final TransactionalMode transactionalMode;
|
||||||
|
|
||||||
|
protected AccordCQLTestBase(TransactionalMode transactionalMode) {
|
||||||
|
this.transactionalMode = transactionalMode;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Logger logger()
|
protected Logger logger()
|
||||||
|
|
@ -83,24 +72,6 @@ public class AccordCQLTest extends AccordTestBase
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parameterized.Parameter
|
|
||||||
public String transactionalModeName;
|
|
||||||
|
|
||||||
TransactionalMode transactionalMode;
|
|
||||||
|
|
||||||
@Parameterized.Parameters(name = "transactionalMode={0}")
|
|
||||||
public static Collection<Object[]> data()
|
|
||||||
{
|
|
||||||
return ImmutableList.of(new Object[] {TransactionalMode.full.toString()},
|
|
||||||
new Object[] {TransactionalMode.mixed_reads.toString()});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setNonSerialWriteStrategy()
|
|
||||||
{
|
|
||||||
transactionalMode = TransactionalMode.valueOf(transactionalModeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setupClass() throws IOException
|
public static void setupClass() throws IOException
|
||||||
{
|
{
|
||||||
|
|
@ -2302,7 +2273,7 @@ public class AccordCQLTest extends AccordTestBase
|
||||||
@Test
|
@Test
|
||||||
public void testMultiCellMapSelection() throws Exception
|
public void testMultiCellMapSelection() throws Exception
|
||||||
{
|
{
|
||||||
testMapSelection("CREATE TABLE " + qualifiedTableName + " (k int PRIMARY KEY, int_map map<text, int>)");
|
testMapSelection("CREATE TABLE " + qualifiedTableName + " (k int PRIMARY KEY, int_map map<text, int>) WITH transactional_mode='" + transactionalMode + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.cassandra.distributed.test.accord;
|
||||||
|
|
||||||
|
import org.apache.cassandra.service.consensus.TransactionalMode;
|
||||||
|
|
||||||
|
public class FullAccordCQLTest extends AccordCQLTestBase
|
||||||
|
{
|
||||||
|
public FullAccordCQLTest()
|
||||||
|
{
|
||||||
|
super(TransactionalMode.full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.cassandra.distributed.test.accord;
|
||||||
|
|
||||||
|
import org.apache.cassandra.service.consensus.TransactionalMode;
|
||||||
|
|
||||||
|
public class MixedReadAccordCQLTest extends AccordCQLTestBase
|
||||||
|
{
|
||||||
|
public MixedReadAccordCQLTest()
|
||||||
|
{
|
||||||
|
super(TransactionalMode.mixed_reads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,8 @@ import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.cql3.QueryProcessor;
|
import org.apache.cassandra.cql3.QueryProcessor;
|
||||||
import org.apache.cassandra.cql3.statements.schema.CreateKeyspaceStatement;
|
import org.apache.cassandra.cql3.statements.schema.CreateKeyspaceStatement;
|
||||||
import org.apache.cassandra.cql3.statements.schema.KeyspaceAttributes;
|
import org.apache.cassandra.cql3.statements.schema.KeyspaceAttributes;
|
||||||
|
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||||
|
import org.apache.cassandra.db.Keyspace;
|
||||||
import org.apache.cassandra.dht.ByteOrderedPartitioner;
|
import org.apache.cassandra.dht.ByteOrderedPartitioner;
|
||||||
import org.apache.cassandra.dht.IPartitioner;
|
import org.apache.cassandra.dht.IPartitioner;
|
||||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||||
|
|
@ -51,6 +53,9 @@ import org.apache.cassandra.schema.KeyspaceMetadata;
|
||||||
import org.apache.cassandra.schema.KeyspaceParams;
|
import org.apache.cassandra.schema.KeyspaceParams;
|
||||||
import org.apache.cassandra.schema.Keyspaces;
|
import org.apache.cassandra.schema.Keyspaces;
|
||||||
import org.apache.cassandra.schema.ReplicationParams;
|
import org.apache.cassandra.schema.ReplicationParams;
|
||||||
|
import org.apache.cassandra.schema.MemtableParams;
|
||||||
|
import org.apache.cassandra.schema.Schema;
|
||||||
|
import org.apache.cassandra.schema.SchemaConstants;
|
||||||
import org.apache.cassandra.schema.SchemaTransformation;
|
import org.apache.cassandra.schema.SchemaTransformation;
|
||||||
import org.apache.cassandra.service.ClientState;
|
import org.apache.cassandra.service.ClientState;
|
||||||
import org.apache.cassandra.tcm.AtomicLongBackedProcessor;
|
import org.apache.cassandra.tcm.AtomicLongBackedProcessor;
|
||||||
|
|
@ -230,6 +235,30 @@ public class ClusterMetadataTestHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setMemtable(String ks, String table, String memtable)
|
||||||
|
{
|
||||||
|
setMemtable(ks, table, MemtableParams.get(memtable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setMemtable(String ks, String table, MemtableParams memtable)
|
||||||
|
{
|
||||||
|
if (SchemaConstants.isLocalSystemKeyspace(ks))
|
||||||
|
{
|
||||||
|
ColumnFamilyStore store = Keyspace.open(ks).getColumnFamilyStore(table);
|
||||||
|
store.reload(store.metadata().unbuild().memtable(memtable).build());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Schema.instance.submit(cms -> {
|
||||||
|
var km = cms.schema.getKeyspaceMetadata(ks);
|
||||||
|
var update = km.withSwapped(km.tables.withSwapped(km.tables.getNullable(table).unbuild()
|
||||||
|
.memtable(memtable)
|
||||||
|
.build()));
|
||||||
|
return cms.schema.getKeyspaces().withAddedOrUpdated(update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Set<InetAddressAndPort> leaving(ClusterMetadata metadata)
|
private static Set<InetAddressAndPort> leaving(ClusterMetadata metadata)
|
||||||
{
|
{
|
||||||
return metadata.directory.states.entrySet().stream()
|
return metadata.directory.states.entrySet().stream()
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import org.apache.cassandra.distributed.shared.Versions;
|
||||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||||
import org.apache.cassandra.utils.SimpleGraph;
|
import org.apache.cassandra.utils.SimpleGraph;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.config.CassandraRelevantProperties.DTEST_ACCORD_ENABLED;
|
||||||
import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_GC_INSPECTOR;
|
import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_GC_INSPECTOR;
|
||||||
import static org.apache.cassandra.distributed.shared.Versions.Version;
|
import static org.apache.cassandra.distributed.shared.Versions.Version;
|
||||||
import static org.apache.cassandra.distributed.shared.Versions.find;
|
import static org.apache.cassandra.distributed.shared.Versions.find;
|
||||||
|
|
@ -74,6 +75,7 @@ public class UpgradeTestBase extends DistributedTestBase
|
||||||
{
|
{
|
||||||
ICluster.setup();
|
ICluster.setup();
|
||||||
SKIP_GC_INSPECTOR.setBoolean(true);
|
SKIP_GC_INSPECTOR.setBoolean(true);
|
||||||
|
DTEST_ACCORD_ENABLED.setBoolean(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ import java.util.stream.Stream;
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
import accord.utils.random.Picker;
|
import accord.utilsfork.random.Picker;
|
||||||
|
|
||||||
public class Gens {
|
public class Gens {
|
||||||
private Gens() {
|
private Gens() {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import java.util.function.Supplier;
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
import accord.utils.random.Picker;
|
import accord.utilsfork.random.Picker;
|
||||||
|
|
||||||
// TODO (expected): merge with C* RandomSource
|
// TODO (expected): merge with C* RandomSource
|
||||||
public interface RandomSource
|
public interface RandomSource
|
||||||
|
|
@ -42,6 +42,11 @@ public interface RandomSource
|
||||||
{
|
{
|
||||||
return new accord.utilsfork.WrappedRandomSource(random);
|
return new accord.utilsfork.WrappedRandomSource(random);
|
||||||
}
|
}
|
||||||
|
//TODO (maintaince): once the rebase is over remove this...
|
||||||
|
static RandomSource wrap(accord.utils.RandomSource rs)
|
||||||
|
{
|
||||||
|
return new WrappedRandomSource(rs.asJdkRandom());
|
||||||
|
}
|
||||||
|
|
||||||
void nextBytes(byte[] bytes);
|
void nextBytes(byte[] bytes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package accord.utils.random;
|
package accord.utilsfork.random;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
@ -33,7 +33,7 @@ import org.apache.cassandra.utils.WithResources;
|
||||||
import org.apache.cassandra.utils.concurrent.AsyncPromise;
|
import org.apache.cassandra.utils.concurrent.AsyncPromise;
|
||||||
import org.apache.cassandra.utils.concurrent.Future;
|
import org.apache.cassandra.utils.concurrent.Future;
|
||||||
|
|
||||||
public class ForwardingExecutorPlus implements ExecutorPlus
|
public class ForwardingExecutorPlus implements ExecutorPlus, SequentialExecutorPlus
|
||||||
{
|
{
|
||||||
private final ExecutorService delegate;
|
private final ExecutorService delegate;
|
||||||
|
|
||||||
|
|
@ -216,4 +216,10 @@ public class ForwardingExecutorPlus implements ExecutorPlus
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Unexpected future type: " + submit.getClass());
|
throw new IllegalStateException("Unexpected future type: " + submit.getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AtLeastOnceTrigger atLeastOnceTrigger(Runnable runnable)
|
||||||
|
{
|
||||||
|
return new SingleThreadExecutorPlus.AtLeastOnce(this, runnable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2986,6 +2986,26 @@ public abstract class CQLTester
|
||||||
throw new IllegalArgumentException("Unsupported value type (value is " + value + ")");
|
throw new IllegalArgumentException("Unsupported value type (value is " + value + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static String wrapInTxn(String... stmts)
|
||||||
|
{
|
||||||
|
return wrapInTxn(Arrays.asList(stmts));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String wrapInTxn(List<String> stmts)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("BEGIN TRANSACTION\n");
|
||||||
|
for (String stmt : stmts)
|
||||||
|
{
|
||||||
|
sb.append('\t').append(stmt);
|
||||||
|
if (!stmt.endsWith(";"))
|
||||||
|
sb.append(';');
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
sb.append("COMMIT TRANSACTION");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private static class TupleValue
|
private static class TupleValue
|
||||||
{
|
{
|
||||||
protected final Object[] values;
|
protected final Object[] values;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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.dht;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.utils.AbstractTypeGenerators;
|
||||||
|
import org.apache.cassandra.utils.AccordGenerators;
|
||||||
|
import org.apache.cassandra.utils.CassandraGenerators;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteSource;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
|
||||||
|
public class IPartitionerTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void byteCompareSerde()
|
||||||
|
{
|
||||||
|
qt().forAll(AccordGenerators.fromQT(CassandraGenerators.token())).check(token -> {
|
||||||
|
var p = token.getPartitioner();
|
||||||
|
var comparable = Objects.requireNonNull(ByteSource.peekable(p.getTokenFactory().asComparableBytes(token, ByteComparable.Version.OSS50)));
|
||||||
|
Token read = p.getTokenFactory().fromComparableBytes(comparable, ByteComparable.Version.OSS50);
|
||||||
|
Assertions.assertThat(read)
|
||||||
|
.describedAs("If LocalPartitioner, the type is %s", (token.getPartitioner() instanceof LocalPartitioner ? AbstractTypeGenerators.typeTree(((LocalPartitioner) token.getPartitioner()).comparator) : null))
|
||||||
|
.isEqualTo(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,517 @@
|
||||||
|
/*
|
||||||
|
* 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.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.LongStream;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import accord.api.RoutingKey;
|
||||||
|
import accord.local.Node;
|
||||||
|
import accord.local.SaveStatus;
|
||||||
|
import accord.local.Status;
|
||||||
|
import accord.primitives.FullKeyRoute;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.Routable;
|
||||||
|
import accord.primitives.Route;
|
||||||
|
import accord.primitives.Txn;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.utils.RandomSource;
|
||||||
|
import org.agrona.collections.Int2ObjectHashMap;
|
||||||
|
import org.agrona.collections.Long2ObjectHashMap;
|
||||||
|
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||||
|
import org.apache.cassandra.cql3.CQLTester;
|
||||||
|
import org.apache.cassandra.cql3.UntypedResultSet;
|
||||||
|
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||||
|
import org.apache.cassandra.db.Keyspace;
|
||||||
|
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||||
|
import org.apache.cassandra.exceptions.ReadSizeAbortException;
|
||||||
|
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.service.accord.api.AccordRoutingKey.TokenKey;
|
||||||
|
import org.apache.cassandra.utils.FBUtilities;
|
||||||
|
import org.apache.cassandra.utils.Interval;
|
||||||
|
import org.apache.cassandra.utils.IntervalTree;
|
||||||
|
import org.apache.cassandra.utils.ObjectSizes;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test validates the system_accord.commands.route index to make sure it returns the right values... the way the
|
||||||
|
* test is strucutred allows pluggability but runs with fixed configs while running in CI...
|
||||||
|
*
|
||||||
|
* If you are interested in testing different cases, the following tunables exist:
|
||||||
|
*
|
||||||
|
* <table>
|
||||||
|
* <li>size: how many read/writes should be done</li>
|
||||||
|
* <li>pattern: how ranges are layed out. By default we test NO_OVERLAP so there are no conflicts</li>
|
||||||
|
* </table>
|
||||||
|
*/
|
||||||
|
public class AccordIndexStressTest extends CQLTester
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AccordIndexStressTest.class);
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
// The plan is to migrate away from SAI, so rather than hacking around timeout issues; just disable for now
|
||||||
|
CassandraRelevantProperties.SAI_TEST_DISABLE_TIMEOUT.setBoolean(true);
|
||||||
|
}
|
||||||
|
private static final boolean VALIDATE = true;
|
||||||
|
private static final boolean INCLUDE_FLUSH = true;
|
||||||
|
private static final long SLOW_NS = TimeUnit.MILLISECONDS.toNanos(25);
|
||||||
|
private static final Node.Id NODE = new Node.Id(42);
|
||||||
|
private final Routable.Domain domain = Routable.Domain.Range;
|
||||||
|
private final Int2ObjectHashMap<Map<TableId, Long2ObjectHashMap<List<TxnId>>>> storeToTableToRoutingKeysToTxns = new Int2ObjectHashMap<>();
|
||||||
|
private final Int2ObjectHashMap<Map<TableId, Map<TokenRange, List<TxnId>>>> storeToTableToRangesToTxns = new Int2ObjectHashMap<>();
|
||||||
|
private final RoutesSearcher searcher = new RoutesSearcher();
|
||||||
|
|
||||||
|
enum Size
|
||||||
|
{tiny, small, medium, large, benchmark}
|
||||||
|
|
||||||
|
private final Size size = Size.small;
|
||||||
|
|
||||||
|
private enum Read
|
||||||
|
{INDEX, CQL}
|
||||||
|
|
||||||
|
private Read read = Read.INDEX;
|
||||||
|
|
||||||
|
private enum Pattern { RANDOM, NO_OVERLAP }
|
||||||
|
private final Pattern pattern = Pattern.NO_OVERLAP;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test()
|
||||||
|
{
|
||||||
|
var tables = IntStream.range(0, 10)
|
||||||
|
.mapToObj(i -> TableId.fromUUID(new UUID(0, i)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
int numWrites, numReads;
|
||||||
|
switch (size)
|
||||||
|
{
|
||||||
|
case tiny:
|
||||||
|
numWrites = 100;
|
||||||
|
numReads = 10;
|
||||||
|
break;
|
||||||
|
case small:
|
||||||
|
numWrites = 100_000;
|
||||||
|
numReads = 1_000;
|
||||||
|
break;
|
||||||
|
case medium:
|
||||||
|
numWrites = 1_000_000;
|
||||||
|
numReads = 1_000;
|
||||||
|
break;
|
||||||
|
case large:
|
||||||
|
numWrites = 10_000_000;
|
||||||
|
numReads = 1_000;
|
||||||
|
break;
|
||||||
|
case benchmark:
|
||||||
|
numWrites = 1_000_000;
|
||||||
|
numReads = numWrites * 10;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unknown size: " + size);
|
||||||
|
}
|
||||||
|
var minToken = 0;
|
||||||
|
var maxToken = (1 << 8) * numWrites;
|
||||||
|
int numStores = 10;
|
||||||
|
qt().withSeed(-1464527987857660885L).withExamples(1).check(rs -> {
|
||||||
|
timed("write(" + numWrites + ")", () -> writeRecords(rs, numStores, tables, minToken, maxToken, numWrites));
|
||||||
|
if (INCLUDE_FLUSH)
|
||||||
|
timed("flush(writes=" + numWrites + ")", () -> FBUtilities.waitOnFutures(Keyspace.open("system_accord").flush(ColumnFamilyStore.FlushReason.UNIT_TESTS)));
|
||||||
|
var warmupReads = Math.max(1, (int) (numReads * .2));
|
||||||
|
timed("warmup read(" + warmupReads + ")", () -> readRecords(rs, warmupReads));
|
||||||
|
timed("read(" + numReads + ")", () -> readRecords(rs, numReads));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void timed(String name, Runnable fn)
|
||||||
|
{
|
||||||
|
logger.warn("Task {} starting...", name);
|
||||||
|
long startNs = nanoTime();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fn.run();
|
||||||
|
}
|
||||||
|
catch (Throwable t)
|
||||||
|
{
|
||||||
|
logger.warn("Task {} failed after {}ms", name, TimeUnit.NANOSECONDS.toMillis(nanoTime() - startNs));
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
logger.warn("Task {} completed after {}ms", name, TimeUnit.NANOSECONDS.toMillis(nanoTime() - startNs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RangeWrapper
|
||||||
|
{
|
||||||
|
final TokenRange[] ranges;
|
||||||
|
final IntervalTree<RoutingKey, TxnId, Interval<RoutingKey, TxnId>> tree;
|
||||||
|
|
||||||
|
RangeWrapper(TokenRange[] ranges, IntervalTree<RoutingKey, TxnId, Interval<RoutingKey, TxnId>> tree)
|
||||||
|
{
|
||||||
|
this.ranges = ranges;
|
||||||
|
this.tree = tree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Int2ObjectHashMap<Map<TableId, long[]>> store2Table2Tokens;
|
||||||
|
private Int2ObjectHashMap<Map<TableId, RangeWrapper>> store2Table2Ranges;
|
||||||
|
|
||||||
|
private void readRecords(RandomSource rs, int numRecords)
|
||||||
|
{
|
||||||
|
logger.warn("The bookkeeping is {} bytes", ObjectSizes.measureDeep(storeToTableToRoutingKeysToTxns));
|
||||||
|
// sort the tokens
|
||||||
|
if (domain == Routable.Domain.Key && store2Table2Tokens == null)
|
||||||
|
{
|
||||||
|
store2Table2Tokens = new Int2ObjectHashMap<>();
|
||||||
|
timed("Model building: key", () -> storeToTableToRoutingKeysToTxns.forEachInt((storeId, actual) -> {
|
||||||
|
Map<TableId, long[]> map = new HashMap<>();
|
||||||
|
for (var e : actual.entrySet())
|
||||||
|
{
|
||||||
|
var keys = e.getValue().keySet();
|
||||||
|
long[] tokens = new long[keys.size()];
|
||||||
|
var it = keys.iterator();
|
||||||
|
for (int i = 0; it.hasNext(); i++)
|
||||||
|
tokens[i] = it.nextLong();
|
||||||
|
Arrays.sort(tokens);
|
||||||
|
map.put(e.getKey(), tokens);
|
||||||
|
}
|
||||||
|
store2Table2Tokens.put(storeId, map);
|
||||||
|
}));
|
||||||
|
store2Table2Ranges = null;
|
||||||
|
}
|
||||||
|
else if (domain == Routable.Domain.Range && store2Table2Ranges == null)
|
||||||
|
{
|
||||||
|
store2Table2Ranges = new Int2ObjectHashMap<>();
|
||||||
|
timed("Model building: range", () -> storeToTableToRangesToTxns.forEachInt((storeId, actual) -> {
|
||||||
|
Map<TableId, RangeWrapper> map = new HashMap<>();
|
||||||
|
for (var e : actual.entrySet())
|
||||||
|
{
|
||||||
|
TableId tableId = e.getKey();
|
||||||
|
Map<TokenRange, List<TxnId>> range2Txns = e.getValue();
|
||||||
|
var keys = range2Txns.keySet();
|
||||||
|
TokenRange[] ranges = new TokenRange[keys.size()];
|
||||||
|
var it = keys.iterator();
|
||||||
|
var builder = new IntervalTree.Builder<RoutingKey, TxnId, Interval<RoutingKey, TxnId>>();
|
||||||
|
for (int i = 0; it.hasNext(); i++)
|
||||||
|
{
|
||||||
|
TokenRange r = ranges[i] = it.next();
|
||||||
|
List<TxnId> txns = range2Txns.get(r);
|
||||||
|
txns.forEach(txnId -> builder.add(new Interval<>(r.start(), r.end(), txnId)));
|
||||||
|
}
|
||||||
|
Arrays.sort(ranges, Range::compare);
|
||||||
|
map.put(tableId, new RangeWrapper(ranges, builder.build()));
|
||||||
|
}
|
||||||
|
store2Table2Ranges.put(storeId, map);
|
||||||
|
}));
|
||||||
|
store2Table2Tokens = null;
|
||||||
|
}
|
||||||
|
long[] samples = new long[numRecords];
|
||||||
|
int[] counts = new int[numRecords];
|
||||||
|
int size = 0;
|
||||||
|
int numReadSizeAborts = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < numRecords; i++)
|
||||||
|
{
|
||||||
|
int store;
|
||||||
|
TableId table;
|
||||||
|
Set<TxnId> expected = new HashSet<>();
|
||||||
|
TokenKey start, end;
|
||||||
|
switch (domain)
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
{
|
||||||
|
store = rs.pick(storeToTableToRoutingKeysToTxns.keySet());
|
||||||
|
var actual = this.storeToTableToRoutingKeysToTxns.get(store);
|
||||||
|
var tableToTokens = store2Table2Tokens.get(store);
|
||||||
|
|
||||||
|
table = rs.pick(actual.keySet());
|
||||||
|
var tokens = tableToTokens.get(table);
|
||||||
|
|
||||||
|
var offset = rs.nextInt(0, tokens.length);
|
||||||
|
var endOffset = offset == tokens.length - 1 ? tokens.length - 1 : offset + rs.nextInt(1, Math.min(3, tokens.length - offset));
|
||||||
|
IntStream.range(offset + 1, endOffset + 1).mapToLong(o -> tokens[o]).forEach(token -> expected.addAll(actual.get(table).get(token)));
|
||||||
|
|
||||||
|
start = new TokenKey(table, new Murmur3Partitioner.LongToken(tokens[offset]));
|
||||||
|
end = new TokenKey(table, new Murmur3Partitioner.LongToken(tokens[endOffset]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Range:
|
||||||
|
{
|
||||||
|
store = rs.pick(storeToTableToRangesToTxns.keySet());
|
||||||
|
var tableToRangesToTxns = storeToTableToRangesToTxns.get(store);
|
||||||
|
var tableToRanges = store2Table2Ranges.get(store);
|
||||||
|
|
||||||
|
table = rs.pick(tableToRangesToTxns.keySet());
|
||||||
|
var wrapper = tableToRanges.get(table);
|
||||||
|
var ranges = wrapper.ranges;
|
||||||
|
var tree = wrapper.tree;
|
||||||
|
var range = rs.pick(ranges);
|
||||||
|
var a = tokenValue(range.start());
|
||||||
|
var b = tokenValue(range.end());
|
||||||
|
start = new TokenKey(table, new Murmur3Partitioner.LongToken(a + 1));
|
||||||
|
end = new TokenKey(table, new Murmur3Partitioner.LongToken(b - 1));
|
||||||
|
expected.addAll(tree.search(start, end));
|
||||||
|
assert !expected.isEmpty();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown domain: " + domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
var startNs = nanoTime();
|
||||||
|
Set<TxnId> actual = read(store, start, end);
|
||||||
|
var durationNs = nanoTime() - startNs;
|
||||||
|
samples[size] = durationNs;
|
||||||
|
counts[size++] = actual.size();
|
||||||
|
if (slow(durationNs))
|
||||||
|
logger.warn("Slow search: i={}, store={}, [{}, {}), results={}", i, store, start, end, actual.size());
|
||||||
|
if (VALIDATE)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assertions.assertThat(actual).describedAs("[%s, %s)", start, end).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
catch (Throwable t)
|
||||||
|
{
|
||||||
|
logSamples(samples, counts, size);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
logger.info("Number of aborts due to size: {}", numReadSizeAborts);
|
||||||
|
logSamples(samples, counts, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long tokenValue(RoutingKey start)
|
||||||
|
{
|
||||||
|
return ((AccordRoutingKey.TokenKey) start).token().getLongValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean slow(long durationNs)
|
||||||
|
{
|
||||||
|
return durationNs >= SLOW_NS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<TxnId> read(int store, AccordRoutingKey start, AccordRoutingKey end)
|
||||||
|
{
|
||||||
|
switch (read)
|
||||||
|
{
|
||||||
|
case INDEX:
|
||||||
|
return readIndex(store, start, end);
|
||||||
|
case CQL:
|
||||||
|
return readCQL(store, start, end);
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unknown read type: " + read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<TxnId> readIndex(int store, AccordRoutingKey start, AccordRoutingKey end)
|
||||||
|
{
|
||||||
|
return searcher.intersects(store, start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<TxnId> readCQL(int store, AccordRoutingKey start, AccordRoutingKey end)
|
||||||
|
{
|
||||||
|
Set<TxnId> actual = new HashSet<>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UntypedResultSet results = execute("SELECT txn_id FROM system_accord.commands WHERE store_id = ? AND route > ? AND route <= ?", store, OrderedRouteSerializer.serializeRoutingKey(start), OrderedRouteSerializer.serializeRoutingKey(end));
|
||||||
|
for (var row : results)
|
||||||
|
actual.add(AccordKeyspace.deserializeTxnId(row));
|
||||||
|
}
|
||||||
|
catch (ReadSizeAbortException e)
|
||||||
|
{
|
||||||
|
// don't count it...
|
||||||
|
logger.warn("Abort query to [{}, {}) do to size", start, end);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logSamples(long[] samples, int[] counts, int size)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
logger.warn("No logs sampled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Arrays.sort(samples, 0, size);
|
||||||
|
Arrays.sort(counts, 0, size);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Samples: ").append(size);
|
||||||
|
sb.append("\nLatenciy:");
|
||||||
|
sb.append("\n Min (micro): ").append(TimeUnit.NANOSECONDS.toMicros(samples[0]));
|
||||||
|
sb.append("\n Max (micro): ").append(TimeUnit.NANOSECONDS.toMicros(samples[size - 1]));
|
||||||
|
sb.append("\n Median (micro): ").append(TimeUnit.NANOSECONDS.toMicros(samples[size / 2]));
|
||||||
|
sb.append("\n Avg (micro): ").append(TimeUnit.NANOSECONDS.toMicros((long) LongStream.of(samples).limit(size).average().getAsDouble()));
|
||||||
|
sb.append("\nCounts:");
|
||||||
|
sb.append("\n Min: ").append(counts[0]);
|
||||||
|
sb.append("\n Max: ").append(counts[size - 1]);
|
||||||
|
sb.append("\n Median: ").append(counts[size / 2]);
|
||||||
|
sb.append("\n Avg: ").append((int) IntStream.of(counts).limit(size).average().getAsDouble());
|
||||||
|
logger.info(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeRecords(RandomSource rs,
|
||||||
|
int numStores,
|
||||||
|
List<TableId> tables,
|
||||||
|
int minToken, int maxToken,
|
||||||
|
int numRecords)
|
||||||
|
{
|
||||||
|
var cql = "INSERT INTO system_accord.commands (store_id, domain, txn_id, status, route, durability) VALUES (?, ?, ?, ?, ?, ?)";
|
||||||
|
for (int i = 0; i < numRecords; i++)
|
||||||
|
{
|
||||||
|
int store = rs.nextInt(0, numStores);
|
||||||
|
TxnId txnId = new TxnId(0, 1000 + i, Txn.Kind.Write, domain, NODE);
|
||||||
|
int domain = txnId.domain().ordinal();
|
||||||
|
int status = SaveStatus.PreCommitted.ordinal();
|
||||||
|
ByteBuffer routeBB;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Route<?> route = createRoute(rs, numRecords, i, rs.nextInt(1, 20), tables, minToken, maxToken);
|
||||||
|
for (var u : route)
|
||||||
|
{
|
||||||
|
switch (u.domain())
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
{
|
||||||
|
AccordRoutingKey key = (AccordRoutingKey) u;
|
||||||
|
var table = key.table();
|
||||||
|
var token = key.token().getLongValue();
|
||||||
|
storeToTableToRoutingKeysToTxns.computeIfAbsent(store, ignore -> new HashMap<>())
|
||||||
|
.computeIfAbsent(table, ignore -> new Long2ObjectHashMap<>())
|
||||||
|
.computeIfAbsent(token, ignore -> new ArrayList<>())
|
||||||
|
.add(txnId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Range:
|
||||||
|
{
|
||||||
|
TokenRange range = (TokenRange) u;
|
||||||
|
var table = range.table();
|
||||||
|
storeToTableToRangesToTxns.computeIfAbsent(store, ignore -> new HashMap<>())
|
||||||
|
.computeIfAbsent(table, ignore -> new HashMap<>())
|
||||||
|
.computeIfAbsent(range, ignore -> new ArrayList<>())
|
||||||
|
.add(txnId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unexpected domain: " + u.domain());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routeBB = AccordKeyspace.serializeRoute(route);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
int durability = Status.Durability.NotDurable.ordinal();
|
||||||
|
|
||||||
|
execute(cql, store, domain, AccordKeyspace.serializeTimestamp(txnId), status, routeBB, durability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Route<?> createRoute(RandomSource rs, int numRecords, int index, int numKeys, List<TableId> tables, int minToken, int maxToken)
|
||||||
|
{
|
||||||
|
switch (domain)
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
{
|
||||||
|
TreeSet<AccordRoutingKey> keys = new TreeSet<>();
|
||||||
|
while (keys.size() < numKeys)
|
||||||
|
{
|
||||||
|
var table = rs.pick(tables);
|
||||||
|
var token = new Murmur3Partitioner.LongToken(rs.nextInt(minToken, maxToken));
|
||||||
|
keys.add(new TokenKey(table, token));
|
||||||
|
}
|
||||||
|
return new FullKeyRoute(keys.first(), true, keys.toArray(RoutingKey[]::new));
|
||||||
|
}
|
||||||
|
case Range:
|
||||||
|
{
|
||||||
|
TreeSet<TokenRange> ranges = new TreeSet<>(Range::compareTo);
|
||||||
|
RoutingKey routingKey = null;
|
||||||
|
var domain = maxToken - minToken + 1;
|
||||||
|
var delta = domain / numRecords;
|
||||||
|
var sub_delta = delta / numKeys;
|
||||||
|
while (ranges.size() < numKeys)
|
||||||
|
{
|
||||||
|
var table = rs.pick(tables);
|
||||||
|
int a, b;
|
||||||
|
switch (pattern)
|
||||||
|
{
|
||||||
|
case RANDOM:
|
||||||
|
{
|
||||||
|
a = rs.nextInt(minToken, maxToken);
|
||||||
|
b = rs.nextInt(minToken, maxToken);
|
||||||
|
while (a == b)
|
||||||
|
b = rs.nextInt(minToken, maxToken);
|
||||||
|
if (a > b)
|
||||||
|
{
|
||||||
|
var tmp = a;
|
||||||
|
a = b;
|
||||||
|
b = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NO_OVERLAP:
|
||||||
|
{
|
||||||
|
a = delta * index + (sub_delta * ranges.size());
|
||||||
|
b = a + sub_delta;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown pattern: " + pattern);
|
||||||
|
}
|
||||||
|
ranges.add(new TokenRange(new TokenKey(table, new Murmur3Partitioner.LongToken(a)), new TokenKey(table, new Murmur3Partitioner.LongToken(b))));
|
||||||
|
if (routingKey == null)
|
||||||
|
{
|
||||||
|
routingKey = new TokenKey(table, new Murmur3Partitioner.LongToken(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ranges.ofSorted(ranges.toArray(Range[]::new)).toRoute(routingKey);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown domain: " + domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,441 @@
|
||||||
|
/*
|
||||||
|
* 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.nio.ByteBuffer;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import accord.utils.Gen;
|
||||||
|
import accord.utils.Gens;
|
||||||
|
import accord.utils.RandomSource;
|
||||||
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
|
import org.apache.cassandra.db.ClusteringComparator;
|
||||||
|
import org.apache.cassandra.db.marshal.ByteBufferAccessor;
|
||||||
|
import org.apache.cassandra.db.marshal.Int32Type;
|
||||||
|
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||||
|
import org.apache.cassandra.index.accord.CheckpointIntervalArrayIndex.Interval;
|
||||||
|
import org.apache.cassandra.index.accord.IndexDescriptor.IndexComponent;
|
||||||
|
import org.apache.cassandra.io.sstable.Descriptor;
|
||||||
|
import org.apache.cassandra.io.sstable.SequenceBasedSSTableId;
|
||||||
|
import org.apache.cassandra.io.util.File;
|
||||||
|
import org.apache.cassandra.io.util.FileHandle;
|
||||||
|
import org.apache.cassandra.schema.TableId;
|
||||||
|
import org.apache.cassandra.utils.ByteArrayUtil;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteSource;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
|
||||||
|
|
||||||
|
public class CheckpointIntervalArrayIndexTest
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CheckpointIntervalArrayIndexTest.class);
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
DatabaseDescriptor.clientInitialization();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte[] EMPTY = new byte[0];
|
||||||
|
private static final TreeSet<Interval> EMPTY_TREE_SET = new TreeSet<>();
|
||||||
|
|
||||||
|
private static final Gen.IntGen MAX_TOKEN_GEN = Gens.pickInt(1 << 14,
|
||||||
|
1 << 16,
|
||||||
|
1 << 20,
|
||||||
|
1 << 30);
|
||||||
|
|
||||||
|
private enum Pattern { RANDOM, NO_OVERLAP, PARTIAL_OVERLAP }
|
||||||
|
|
||||||
|
private static final Gen<Pattern> PATTERN_GEN = Gens.enums().all(Pattern.class);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder folder = new TemporaryFolder();
|
||||||
|
private int generation = 0;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simple() throws IOException
|
||||||
|
{
|
||||||
|
int bytesPerKey = Integer.BYTES;
|
||||||
|
int bytesPerValue = 0;
|
||||||
|
|
||||||
|
List<Interval> list = new ArrayList<>(10);
|
||||||
|
list.add(new Interval(bytes(Integer.MIN_VALUE).array(), bytes(Integer.MAX_VALUE).array(), EMPTY));
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
list.add(new Interval(bytes(i).array(), bytes(i + 1).array(), EMPTY));
|
||||||
|
|
||||||
|
try (var searcher = index(bytesPerKey, bytesPerValue, list))
|
||||||
|
{
|
||||||
|
Set<List<Integer>> expected = Set.of(List.of(-2147483648, 2147483647),
|
||||||
|
List.of(2, 3),
|
||||||
|
List.of(3, 4));
|
||||||
|
Set<List<Integer>> actual = new HashSet<>();
|
||||||
|
var stats = searcher.intersects(bytes(2).array(), bytes(4).array(), value -> actual.add(List.of(ByteBuffer.wrap(value.start).getInt(), ByteBuffer.wrap(value.end).getInt())));
|
||||||
|
logger.info("Stats: {}", stats);
|
||||||
|
Assertions.assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fuzzSmall()
|
||||||
|
{
|
||||||
|
var minToken = 0;
|
||||||
|
int numRecords = 10;
|
||||||
|
qt().withTimeout(Duration.ofSeconds(60)).check(rs -> fuzz(rs, minToken, MAX_TOKEN_GEN.nextInt(rs), PATTERN_GEN.next(rs), numRecords));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fuzzMedium()
|
||||||
|
{
|
||||||
|
var minToken = 0;
|
||||||
|
int numRecords = 1_000;
|
||||||
|
qt().withTimeout(Duration.ofSeconds(60)).check(rs -> fuzz(rs, minToken, MAX_TOKEN_GEN.nextInt(rs), PATTERN_GEN.next(rs), numRecords));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fuzz(RandomSource rs, int minToken, int maxToken, Pattern pattern, int numRecords) throws IOException
|
||||||
|
{
|
||||||
|
List<DetailedInterval> intervals = buildIntervals(rs, minToken, maxToken, pattern, numRecords);
|
||||||
|
List<? extends Interval> nonContainedRanges = findMissingRanges(intervals);
|
||||||
|
|
||||||
|
try (var searcher = index(Integer.BYTES, Integer.BYTES, intervals))
|
||||||
|
{
|
||||||
|
for (int i = 0, samples = rs.nextInt(Math.min(10, numRecords), Math.min(10, numRecords) * 10); i < samples; i++)
|
||||||
|
{
|
||||||
|
SearchContext ctx = rs.decide(.2) ? miss(rs, nonContainedRanges)
|
||||||
|
: hit(rs, intervals, pattern);
|
||||||
|
Set<Interval> actual = new TreeSet<>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stats = searcher.intersects(ctx.start, ctx.end, interval -> actual.add(new DetailedInterval(interval)));
|
||||||
|
logger.info("[Pattern={}, size={}, expectedMatches={}, query=[{}, {})] Stats: {}", pattern, intervals.size(), ctx.expected.size(), ctx.a, ctx.b, stats);
|
||||||
|
}
|
||||||
|
catch (Throwable t)
|
||||||
|
{
|
||||||
|
throw new AssertionError(String.format("Failure searching for [%d, %d) from %s", ctx.a, ctx.b, intervals), t);
|
||||||
|
}
|
||||||
|
Assertions.assertThat(actual).describedAs("search(%d, %d) from %s", ctx.a, ctx.b, intervals).isEqualTo(ctx.expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mutable/shared ctx to avoid allocating in a loop...
|
||||||
|
*/
|
||||||
|
private SearchContext searchContext = new SearchContext();
|
||||||
|
|
||||||
|
private SearchContext miss(RandomSource rs, List<? extends Interval> nonContainedRanges)
|
||||||
|
{
|
||||||
|
var range = rs.pick(nonContainedRanges);
|
||||||
|
var s = unbc(range.start);
|
||||||
|
var e = unbc(range.end);
|
||||||
|
int domain = e - s;
|
||||||
|
int a, b;
|
||||||
|
if (domain == 1)
|
||||||
|
{
|
||||||
|
// you can not find multiple values within this range!
|
||||||
|
a = s;
|
||||||
|
b = e;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
a = e == Integer.MAX_VALUE ? rs.nextInt(s, e) : rs.nextInt(s, e) + 1;
|
||||||
|
b = e == Integer.MAX_VALUE ? rs.nextInt(s, e) : rs.nextInt(s, e) + 1;
|
||||||
|
for (int i = 0; i < 42 && a == b; i++)
|
||||||
|
b = e == Integer.MAX_VALUE ? rs.nextInt(s, e) : rs.nextInt(s, e) + 1;
|
||||||
|
if (a == b)
|
||||||
|
throw new IllegalStateException("Unable to create missing range: " + range);
|
||||||
|
if (b < a)
|
||||||
|
{
|
||||||
|
var tmp = a;
|
||||||
|
a = b;
|
||||||
|
b = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchContext.a = a;
|
||||||
|
searchContext.b = b;
|
||||||
|
searchContext.start = bc(a);
|
||||||
|
searchContext.end = bc(b);
|
||||||
|
searchContext.expected = EMPTY_TREE_SET;
|
||||||
|
return searchContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchContext hit(RandomSource rs, List<DetailedInterval> intervals, Pattern pattern)
|
||||||
|
{
|
||||||
|
int numRecords = intervals.size();
|
||||||
|
DetailedInterval first, second;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var offset = rs.nextInt(0, numRecords);
|
||||||
|
int endOffset;
|
||||||
|
switch (pattern)
|
||||||
|
{
|
||||||
|
case PARTIAL_OVERLAP:
|
||||||
|
case RANDOM:
|
||||||
|
endOffset = offset;
|
||||||
|
break;
|
||||||
|
case NO_OVERLAP:
|
||||||
|
endOffset = offset == numRecords - 1 ? offset : offset + rs.nextInt(1, Math.min(3, numRecords - offset));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown pattern: " + pattern);
|
||||||
|
}
|
||||||
|
first = intervals.get(offset);
|
||||||
|
second = intervals.get(endOffset);
|
||||||
|
}
|
||||||
|
while (first.compareTo(second) == 0 && first.size() == 1);
|
||||||
|
int a, b;
|
||||||
|
a = rs.nextInt(unbc(first.start), unbc(first.end)) + 1;
|
||||||
|
b = rs.nextInt(unbc(second.start), unbc(second.end)) + 1;
|
||||||
|
while (a == b)
|
||||||
|
b = rs.nextInt(unbc(second.start), unbc(second.end)) + 1;
|
||||||
|
if (b < a)
|
||||||
|
{
|
||||||
|
var tmp = b;
|
||||||
|
b = a;
|
||||||
|
a = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchContext.start = bc(a);
|
||||||
|
searchContext.end = bc(b);
|
||||||
|
|
||||||
|
searchContext.expected = intervals.stream().filter(i -> i.intersects(searchContext.start, searchContext.end)).collect(Collectors.toCollection(TreeSet::new));
|
||||||
|
Assertions.assertThat(searchContext.expected).isNotEmpty();
|
||||||
|
return searchContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DetailedInterval> buildIntervals(RandomSource rs, int minToken, int maxToken, Pattern pattern, int numRecords)
|
||||||
|
{
|
||||||
|
List<DetailedInterval> intervals = new ArrayList<>(numRecords);
|
||||||
|
{
|
||||||
|
var domain = maxToken - minToken + 1;
|
||||||
|
var delta = domain / numRecords;
|
||||||
|
var sub_delta = delta / 2;
|
||||||
|
for (int i = 0; i < numRecords; i++)
|
||||||
|
{
|
||||||
|
switch (pattern)
|
||||||
|
{
|
||||||
|
case RANDOM:
|
||||||
|
{
|
||||||
|
var start = rs.nextInt(minToken, maxToken);
|
||||||
|
var remaining = maxToken - start;
|
||||||
|
var end = start + (remaining == 1 ? 1 : rs.nextInt(1, remaining));
|
||||||
|
intervals.add(new DetailedInterval(bc(start), bc(end), bytes(i).array()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NO_OVERLAP:
|
||||||
|
{
|
||||||
|
var start = delta * i;
|
||||||
|
var end = start + sub_delta;
|
||||||
|
intervals.add(new DetailedInterval(bc(start), bc(end), bytes(i).array()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PARTIAL_OVERLAP:
|
||||||
|
{
|
||||||
|
if (i > 1 && rs.decide(.2))
|
||||||
|
{
|
||||||
|
// overlap
|
||||||
|
DetailedInterval start, end;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int numOverlaps = rs.nextInt(1, Math.min(3, intervals.size()));
|
||||||
|
int offset = rs.nextInt(0, intervals.size() - numOverlaps);
|
||||||
|
start = intervals.get(offset);
|
||||||
|
end = intervals.get(offset + numOverlaps);
|
||||||
|
}
|
||||||
|
while (start.compareStart(end) == 0 && start.size() == 1);
|
||||||
|
var a = rs.nextInt(unbc(start.start), unbc(start.end)) + 1;
|
||||||
|
var b = rs.nextInt(unbc(end.start), unbc(end.end)) + 1;
|
||||||
|
if (a == b && end.size() == 1)
|
||||||
|
{
|
||||||
|
while (a == b)
|
||||||
|
a = rs.nextInt(unbc(start.start), unbc(start.end)) + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (a == b)
|
||||||
|
b = rs.nextInt(unbc(end.start), unbc(end.end)) + 1;
|
||||||
|
}
|
||||||
|
if (a > b)
|
||||||
|
{
|
||||||
|
var tmp = a;
|
||||||
|
a = b;
|
||||||
|
b = tmp;
|
||||||
|
}
|
||||||
|
intervals.add(new DetailedInterval(bc(a), bc(b), bytes(i).array()));
|
||||||
|
intervals.sort(Comparator.naturalOrder()); // so partial can work next time
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no overlap
|
||||||
|
var start = delta * i;
|
||||||
|
var end = start + sub_delta;
|
||||||
|
intervals.add(new DetailedInterval(bc(start), bc(end), bytes(i).array()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown pattern: " + pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intervals.sort(Comparator.naturalOrder());
|
||||||
|
}
|
||||||
|
return intervals;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DetailedInterval> findMissingRanges(List<? extends Interval> intervals)
|
||||||
|
{
|
||||||
|
List<DetailedInterval> list = new ArrayList<>();
|
||||||
|
list.add(new DetailedInterval(bc(Integer.MIN_VALUE), intervals.get(0).start, bytes(0).array()));
|
||||||
|
// track current visable coverage
|
||||||
|
int end = unbc(intervals.get(0).end);
|
||||||
|
for (var i : intervals)
|
||||||
|
{
|
||||||
|
int istar = unbc(i.start);
|
||||||
|
int iend = unbc(i.end);
|
||||||
|
if (end >= istar)
|
||||||
|
{
|
||||||
|
// current scope includes this range
|
||||||
|
end = Math.max(end, iend);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// range doesn't intersect, and a new start/end are formed!
|
||||||
|
list.add(new DetailedInterval(bc(end), bc(istar), bytes(list.size()).array()));
|
||||||
|
end = iend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.add(new DetailedInterval(bc(end), bc(Integer.MAX_VALUE), bytes(list.size()).array()));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailedInterval extends Interval
|
||||||
|
{
|
||||||
|
public DetailedInterval(byte[] start, byte[] end, byte[] value)
|
||||||
|
{
|
||||||
|
super(start, end, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetailedInterval(Interval other)
|
||||||
|
{
|
||||||
|
super(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size()
|
||||||
|
{
|
||||||
|
return unbc(end) - unbc(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "[" + unbc(start) + ", " + unbc(end) + ") -> " + ByteBuffer.wrap(value).getInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] bc(int value)
|
||||||
|
{
|
||||||
|
ByteBuffer bb = bytes(value);
|
||||||
|
var bs = Int32Type.instance.asComparableBytes(ByteBufferAccessor.instance, bb, ByteComparable.Version.OSS50);
|
||||||
|
return ByteSourceInverse.readBytes(bs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int unbc(byte[] bc)
|
||||||
|
{
|
||||||
|
return Int32Type.instance.fromComparableBytes(ByteSource.peekable(ByteSource.fixedLength(bc)), ByteComparable.Version.OSS50).getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "IOResourceOpenedButNotSafelyClosed", "resource" })
|
||||||
|
private Searcher index(int bytesPerKey, int bytesPerValue, List<? extends Interval> sortedIntervals) throws IOException
|
||||||
|
{
|
||||||
|
IndexDescriptor descriptor = nextDescriptor();
|
||||||
|
|
||||||
|
var writer = new CheckpointIntervalArrayIndex.SegmentWriter(descriptor, bytesPerKey, bytesPerValue);
|
||||||
|
var metas = writer.write(sortedIntervals.toArray(Interval[]::new));
|
||||||
|
|
||||||
|
// going through the RouteIndexFormat isn't required for this test, but it helps improve coverage there...
|
||||||
|
Segment segment = new Segment(ImmutableMap.of(new Group(0, TableId.fromUUID(new UUID(0, 0))), new Segment.Metadata(metas, ByteArrayUtil.EMPTY_BYTE_ARRAY, ByteArrayUtil.EMPTY_BYTE_ARRAY)));
|
||||||
|
RouteIndexFormat.appendSegment(descriptor, segment);
|
||||||
|
|
||||||
|
Map<IndexComponent, FileHandle> files = new EnumMap<>(IndexComponent.class);
|
||||||
|
for (IndexComponent c : descriptor.getLiveComponents())
|
||||||
|
files.put(c, new FileHandle.Builder(descriptor.fileFor(c)).mmapped(true).complete());
|
||||||
|
List<Segment> segments = RouteIndexFormat.readSegements(files);
|
||||||
|
files.remove(IndexComponent.SEGMENT).close();
|
||||||
|
files.remove(IndexComponent.METADATA).close();
|
||||||
|
|
||||||
|
var searcher = new CheckpointIntervalArrayIndex.SegmentSearcher(files.get(IndexComponent.CINTIA_SORTED_LIST).sharedCopy(), metas.get(IndexComponent.CINTIA_SORTED_LIST).offset,
|
||||||
|
files.get(IndexComponent.CINTIA_CHECKPOINTS).sharedCopy(), metas.get(IndexComponent.CINTIA_CHECKPOINTS).offset);
|
||||||
|
return new Searcher()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public CheckpointIntervalArrayIndex.Stats intersects(byte[] start, byte[] end, Consumer<Interval> callback) throws IOException
|
||||||
|
{
|
||||||
|
return searcher.intersects(start, end, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close()
|
||||||
|
{
|
||||||
|
searcher.close();
|
||||||
|
for (var fh : files.values())
|
||||||
|
fh.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexDescriptor nextDescriptor()
|
||||||
|
{
|
||||||
|
return IndexDescriptor.create(new Descriptor(new File(folder.getRoot()), "test", "test", new SequenceBasedSSTableId(generation++)),
|
||||||
|
Murmur3Partitioner.instance,
|
||||||
|
new ClusteringComparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SearchContext
|
||||||
|
{
|
||||||
|
TreeSet<Interval> expected;
|
||||||
|
byte[] start, end;
|
||||||
|
int a, b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Searcher extends Closeable
|
||||||
|
{
|
||||||
|
CheckpointIntervalArrayIndex.Stats intersects(byte[] start, byte[] end, Consumer<Interval> callback) throws IOException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,508 @@
|
||||||
|
/*
|
||||||
|
* 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.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import accord.api.RoutingKey;
|
||||||
|
import accord.local.Node;
|
||||||
|
import accord.local.SaveStatus;
|
||||||
|
import accord.local.Status.Durability;
|
||||||
|
import accord.primitives.FullKeyRoute;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.Routable.Domain;
|
||||||
|
import accord.primitives.Route;
|
||||||
|
import accord.primitives.Txn;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.utils.Gen;
|
||||||
|
import accord.utils.Gens;
|
||||||
|
import accord.utils.Property.Command;
|
||||||
|
import accord.utils.Property.Commands;
|
||||||
|
import accord.utils.Property.UnitCommand;
|
||||||
|
import accord.utils.RandomSource;
|
||||||
|
import org.agrona.collections.Int2ObjectHashMap;
|
||||||
|
import org.agrona.collections.Long2ObjectHashMap;
|
||||||
|
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||||
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
|
import org.apache.cassandra.cql3.CQLTester;
|
||||||
|
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||||
|
import org.apache.cassandra.db.Keyspace;
|
||||||
|
import org.apache.cassandra.db.compaction.CompactionManager;
|
||||||
|
import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
|
||||||
|
import org.apache.cassandra.schema.SchemaConstants;
|
||||||
|
import org.apache.cassandra.schema.TableId;
|
||||||
|
import org.apache.cassandra.service.accord.AccordKeyspace;
|
||||||
|
import org.apache.cassandra.service.accord.AccordService;
|
||||||
|
import org.apache.cassandra.service.accord.TokenRange;
|
||||||
|
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
||||||
|
import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey;
|
||||||
|
import org.apache.cassandra.utils.FBUtilities;
|
||||||
|
import org.apache.cassandra.utils.RTree;
|
||||||
|
import org.apache.cassandra.utils.RangeTree;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.stateful;
|
||||||
|
|
||||||
|
public class RouteIndexTest extends CQLTester.InMemory
|
||||||
|
{
|
||||||
|
static
|
||||||
|
{
|
||||||
|
// since this test does frequent truncates, the info table gets updated and forced flushed... which is 90% of the cost of this test...
|
||||||
|
// this flag disables that flush
|
||||||
|
CassandraRelevantProperties.UNSAFE_SYSTEM.setBoolean(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Node.Id NODE = new Node.Id(42);
|
||||||
|
private static final int MIN_TOKEN = 0;
|
||||||
|
private static final int MAX_TOKEN = 1 << 18;
|
||||||
|
private static final int TOKEN_RANGE_SIZE = MAX_TOKEN - MIN_TOKEN + 1;
|
||||||
|
private static final Gen.IntGen NUM_STORES_GEN = Gens.ints().between(1, 10);
|
||||||
|
private static final Gen.IntGen NUM_TABLES_GEN = Gens.ints().between(1, 10);
|
||||||
|
private static final Gen<Gen.IntGen> TOKEN_DISTRIBUTION = Gens.mixedDistribution(MIN_TOKEN, MAX_TOKEN + 1);
|
||||||
|
private static final Gen<Gen.IntGen> RANGE_SIZE_DISTRIBUTION = Gens.mixedDistribution(10, (int) (TOKEN_RANGE_SIZE * .01));
|
||||||
|
private static final Gen<Gen<Domain>> DOMAIN_DISTRIBUTION = Gens.mixedDistribution(Domain.values());
|
||||||
|
private static RoutesSearcher ROUTES_SEARCHER = null;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUpClass()
|
||||||
|
{
|
||||||
|
CQLTester.InMemory.setUpClass();
|
||||||
|
DatabaseDescriptor.setIncrementalBackupsEnabled(false);
|
||||||
|
ROUTES_SEARCHER = new RoutesSearcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test()
|
||||||
|
{
|
||||||
|
cfs().disableAutoCompaction(); // let the test control compaction
|
||||||
|
//TODO (coverage): include with the ability to mark ranges as durable for compaction cleanup
|
||||||
|
AccordService.unsafeSetNoop(); // disable accord service since compaction touches it. It would be nice to include this for cleanup support....
|
||||||
|
stateful().withExamples(50).check(new Commands<State, ColumnFamilyStore>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Gen<State> genInitialState()
|
||||||
|
{
|
||||||
|
return rs -> new State(rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ColumnFamilyStore createSut(State state)
|
||||||
|
{
|
||||||
|
return cfs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Gen<Command<State, ColumnFamilyStore, ?>> commands(State state)
|
||||||
|
{
|
||||||
|
Map<Gen<Command<State, ColumnFamilyStore, ?>>, Integer> possible = new HashMap<>();
|
||||||
|
possible.put(ignore -> FLUSH, 1);
|
||||||
|
possible.put(ignore -> COMPACT, 1);
|
||||||
|
possible.put(rs -> {
|
||||||
|
int storeId = rs.nextInt(0, state.numStores);
|
||||||
|
Domain domain = state.domainGen.next(rs);
|
||||||
|
TxnId txnId = state.nextTxnId(domain);
|
||||||
|
Route<?> route = createRoute(state, rs, domain, rs.nextInt(1, 20));
|
||||||
|
return new InsertTxn(storeId, txnId, SaveStatus.PreAccepted, Durability.NotDurable, route);
|
||||||
|
}, 10);
|
||||||
|
possible.put(rs -> new RangeSearch(rs.nextInt(0, state.numStores), state.rangeGen.next(rs)), 1);
|
||||||
|
if (!state.storeToTableToRangesToTxns.isEmpty())
|
||||||
|
{
|
||||||
|
possible.put(rs -> {
|
||||||
|
int storeId = rs.pick(state.storeToTableToRangesToTxns.keySet());
|
||||||
|
var tables = state.storeToTableToRangesToTxns.get(storeId);
|
||||||
|
TableId tableId = rs.pick(tables.keySet());
|
||||||
|
var ranges = tables.get(tableId);
|
||||||
|
TreeSet<TokenRange> distinctRanges = ranges.stream().map(Map.Entry::getKey).collect(Collectors.toCollection(() -> new TreeSet<>(TokenRange::compareTo)));
|
||||||
|
TokenRange range;
|
||||||
|
if (distinctRanges.size() == 1)
|
||||||
|
{
|
||||||
|
range = Iterables.getFirst(distinctRanges, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (rs.nextInt(0, 2))
|
||||||
|
{
|
||||||
|
case 0: // perfect match
|
||||||
|
range = rs.pick(distinctRanges);
|
||||||
|
break;
|
||||||
|
case 1: // mutli-match
|
||||||
|
{
|
||||||
|
TokenRange a = rs.pick(distinctRanges);
|
||||||
|
TokenRange b = rs.pick(distinctRanges);
|
||||||
|
while (a.equals(b))
|
||||||
|
b = rs.pick(distinctRanges);
|
||||||
|
if (b.compareTo(a) < 0)
|
||||||
|
{
|
||||||
|
TokenRange tmp = a;
|
||||||
|
a = b;
|
||||||
|
b = tmp;
|
||||||
|
}
|
||||||
|
range = new TokenRange((AccordRoutingKey) a.start(), (AccordRoutingKey) b.end());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RangeSearch(storeId, range);
|
||||||
|
}, 5);
|
||||||
|
}
|
||||||
|
return Gens.oneOf(possible);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroySut(ColumnFamilyStore sut)
|
||||||
|
{
|
||||||
|
cfs().truncateBlocking();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ColumnFamilyStore cfs()
|
||||||
|
{
|
||||||
|
return Keyspace.open(SchemaConstants.ACCORD_KEYSPACE_NAME)
|
||||||
|
.getColumnFamilyStore(AccordKeyspace.COMMANDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Gen<TokenRange> rangeGen(RandomSource rand, List<TableId> tables)
|
||||||
|
{
|
||||||
|
Gen.IntGen tokenGen = TOKEN_DISTRIBUTION.next(rand);
|
||||||
|
Gen<TableId> tableIdGen = Gens.mixedDistribution(tables).next(rand);
|
||||||
|
switch (rand.nextInt(0, 3))
|
||||||
|
{
|
||||||
|
case 0: // pure random
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int b = tokenGen.nextInt(rs);
|
||||||
|
while (a == b)
|
||||||
|
b = tokenGen.nextInt(rs);
|
||||||
|
if (a > b)
|
||||||
|
{
|
||||||
|
int tmp = a;
|
||||||
|
a = b;
|
||||||
|
b = tmp;
|
||||||
|
}
|
||||||
|
TableId tableId = tableIdGen.next(rs);
|
||||||
|
return new TokenRange(new TokenKey(tableId, new LongToken(a)),
|
||||||
|
new TokenKey(tableId, new LongToken(b)));
|
||||||
|
};
|
||||||
|
case 1: // small range
|
||||||
|
Gen.IntGen rangeSizeGen = RANGE_SIZE_DISTRIBUTION.next(rand);
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int rangeSize = rangeSizeGen.nextInt(rs);
|
||||||
|
int b = a + rangeSize;
|
||||||
|
if (b > MAX_TOKEN)
|
||||||
|
{
|
||||||
|
b = a;
|
||||||
|
a = b - rangeSize;
|
||||||
|
}
|
||||||
|
TableId tableId = tableIdGen.next(rs);
|
||||||
|
return new TokenRange(new TokenKey(tableId, new LongToken(a)),
|
||||||
|
new TokenKey(tableId, new LongToken(b)));
|
||||||
|
};
|
||||||
|
case 2: // single element
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int b = a + 1;
|
||||||
|
TableId tableId = tableIdGen.next(rs);
|
||||||
|
return new TokenRange(new TokenKey(tableId, new LongToken(a)),
|
||||||
|
new TokenKey(tableId, new LongToken(b)));
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Route<?> createRoute(State state, RandomSource rs, Domain domain, int numKeys)
|
||||||
|
{
|
||||||
|
switch (domain)
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
{
|
||||||
|
TreeSet<AccordRoutingKey> keys = new TreeSet<>();
|
||||||
|
while (keys.size() < numKeys)
|
||||||
|
{
|
||||||
|
var table = rs.pick(state.tables);
|
||||||
|
var token = new LongToken(state.tokenGen.nextInt(rs));
|
||||||
|
keys.add(new TokenKey(table, token));
|
||||||
|
}
|
||||||
|
return new FullKeyRoute(keys.first(), true, keys.toArray(RoutingKey[]::new));
|
||||||
|
}
|
||||||
|
case Range:
|
||||||
|
{
|
||||||
|
TreeSet<TokenRange> set = new TreeSet<>(Range::compareTo);
|
||||||
|
while (set.size() < numKeys)
|
||||||
|
set.add(state.rangeGen.next(rs));
|
||||||
|
return Ranges.ofSorted(set.toArray(Range[]::new)).toRoute(set.first().end());
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown domain: " + domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InsertTxn implements UnitCommand<State, ColumnFamilyStore>
|
||||||
|
{
|
||||||
|
private static final String cql = "INSERT INTO system_accord.commands (store_id, domain, txn_id, status, route, durability) VALUES (?, ?, ?, ?, ?, ?)";
|
||||||
|
private final int storeId;
|
||||||
|
private final TxnId txnId;
|
||||||
|
private final SaveStatus saveStatus;
|
||||||
|
private final Durability durability;
|
||||||
|
private final Route<?> route;
|
||||||
|
|
||||||
|
private InsertTxn(int storeId, TxnId txnId, SaveStatus saveStatus, Durability durability, Route<?> route)
|
||||||
|
{
|
||||||
|
this.storeId = storeId;
|
||||||
|
this.txnId = txnId;
|
||||||
|
this.saveStatus = saveStatus;
|
||||||
|
this.durability = durability;
|
||||||
|
this.route = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUnit(State state)
|
||||||
|
{
|
||||||
|
for (var u : route)
|
||||||
|
{
|
||||||
|
switch (u.domain())
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
{
|
||||||
|
AccordRoutingKey key = (AccordRoutingKey) u;
|
||||||
|
var table = key.table();
|
||||||
|
var token = key.token().getLongValue();
|
||||||
|
state.storeToTableToRoutingKeysToTxns.computeIfAbsent(storeId, ignore -> new HashMap<>())
|
||||||
|
.computeIfAbsent(table, ignore -> new Long2ObjectHashMap<>())
|
||||||
|
.computeIfAbsent(token, ignore -> new ArrayList<>())
|
||||||
|
.add(txnId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Range:
|
||||||
|
{
|
||||||
|
TokenRange range = (TokenRange) u;
|
||||||
|
var table = range.table();
|
||||||
|
state.storeToTableToRangesToTxns.computeIfAbsent(storeId, ignore -> new HashMap<>())
|
||||||
|
.computeIfAbsent(table, ignore -> rangeTree())
|
||||||
|
.add(range, txnId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unexpected domain: " + u.domain());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runUnit(ColumnFamilyStore sut) throws Throwable
|
||||||
|
{
|
||||||
|
execute(cql, storeId, txnId.domain().ordinal(), AccordKeyspace.serializeTimestamp(txnId), saveStatus.ordinal(), AccordKeyspace.serializeRoute(route), durability.ordinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "InsertTxn{" +
|
||||||
|
"storeId=" + storeId +
|
||||||
|
", txnId=" + txnId +
|
||||||
|
", saveStatus=" + saveStatus +
|
||||||
|
", durability=" + durability +
|
||||||
|
", route=" + route +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RangeSearch implements Command<State, ColumnFamilyStore, Set<TxnId>>
|
||||||
|
{
|
||||||
|
private final int storeId;
|
||||||
|
private final TokenRange range;
|
||||||
|
|
||||||
|
private RangeSearch(int storeId, TokenRange range)
|
||||||
|
{
|
||||||
|
this.storeId = storeId;
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<TxnId> apply(State state) throws Throwable
|
||||||
|
{
|
||||||
|
var tables = state.storeToTableToRangesToTxns.get(storeId);
|
||||||
|
if (tables == null) return Collections.emptySet();
|
||||||
|
var ranges = tables.get(range.table());
|
||||||
|
if (ranges == null) return Collections.emptySet();
|
||||||
|
Set<TxnId> matches = new HashSet<>();
|
||||||
|
ranges.search(range, e -> matches.add(e.getValue()));
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<TxnId> run(ColumnFamilyStore sut) throws Throwable
|
||||||
|
{
|
||||||
|
return ROUTES_SEARCHER.intersects(storeId, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkPostconditions(State state, Set<TxnId> expected,
|
||||||
|
ColumnFamilyStore sut, Set<TxnId> actual)
|
||||||
|
{
|
||||||
|
Assertions.assertThat(actual).describedAs("Unexpected txns for range %s", range).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "RangeSearch{" +
|
||||||
|
"storeId=" + storeId +
|
||||||
|
", range=" + range +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static abstract class CassandraCommand implements UnitCommand<State, ColumnFamilyStore>
|
||||||
|
{
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
protected CassandraCommand(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUnit(State state)
|
||||||
|
{
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final CassandraCommand FLUSH = new CassandraCommand("Flush")
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void runUnit(ColumnFamilyStore sut)
|
||||||
|
{
|
||||||
|
sut.forceBlockingFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final CassandraCommand COMPACT = new CassandraCommand("Compact")
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void runUnit(ColumnFamilyStore sut)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sut.enableAutoCompaction();
|
||||||
|
FBUtilities.waitOnFutures(CompactionManager.instance.submitBackground(sut));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
sut.disableAutoCompaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static class State
|
||||||
|
{
|
||||||
|
private final Int2ObjectHashMap<Map<TableId, Long2ObjectHashMap<List<TxnId>>>> storeToTableToRoutingKeysToTxns = new Int2ObjectHashMap<>();
|
||||||
|
private final Int2ObjectHashMap<Map<TableId, RangeTree<AccordRoutingKey, TokenRange, TxnId>>> storeToTableToRangesToTxns = new Int2ObjectHashMap<>();
|
||||||
|
|
||||||
|
private final int numStores;
|
||||||
|
private final List<TableId> tables;
|
||||||
|
private final Gen.IntGen tokenGen;
|
||||||
|
private final Gen<TokenRange> rangeGen;
|
||||||
|
private final Gen<Domain> domainGen;
|
||||||
|
private int hlc = 1000;
|
||||||
|
|
||||||
|
public State(RandomSource rs)
|
||||||
|
{
|
||||||
|
numStores = NUM_STORES_GEN.nextInt(rs);
|
||||||
|
tables = IntStream.range(0, NUM_TABLES_GEN.nextInt(rs))
|
||||||
|
.mapToObj(i -> TableId.fromUUID(new UUID(0, i)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
tokenGen = TOKEN_DISTRIBUTION.next(rs);
|
||||||
|
rangeGen = rangeGen(rs, tables);
|
||||||
|
domainGen = DOMAIN_DISTRIBUTION.next(rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
TxnId nextTxnId(Domain domain)
|
||||||
|
{
|
||||||
|
return new TxnId(1, hlc++, Txn.Kind.Write, domain, NODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "State{" +
|
||||||
|
"numStores=" + numStores +
|
||||||
|
", tables=" + tables +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RangeTree<AccordRoutingKey, TokenRange, TxnId> rangeTree()
|
||||||
|
{
|
||||||
|
return RTree.create(ACCESSOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final RangeTree.Accessor<AccordRoutingKey, TokenRange> ACCESSOR = new RangeTree.Accessor<AccordRoutingKey, TokenRange>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public AccordRoutingKey start(TokenRange tokenRange)
|
||||||
|
{
|
||||||
|
return (AccordRoutingKey) tokenRange.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccordRoutingKey end(TokenRange tokenRange)
|
||||||
|
{
|
||||||
|
return (AccordRoutingKey) tokenRange.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(AccordRoutingKey start, AccordRoutingKey end, AccordRoutingKey accordRoutingKey)
|
||||||
|
{
|
||||||
|
return new TokenRange(start, end).contains(accordRoutingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean intersects(TokenRange tokenRange, AccordRoutingKey start, AccordRoutingKey end)
|
||||||
|
{
|
||||||
|
return tokenRange.compareIntersecting(new TokenRange(start, end)) == 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -569,25 +569,25 @@ public class CassandraIndexTest extends CQLTester
|
||||||
Awaitility.await()
|
Awaitility.await()
|
||||||
.atMost(1, TimeUnit.MINUTES)
|
.atMost(1, TimeUnit.MINUTES)
|
||||||
.pollDelay(1, TimeUnit.SECONDS)
|
.pollDelay(1, TimeUnit.SECONDS)
|
||||||
.untilAsserted(() -> assertRows(execute(selectBuiltIndexesQuery), row("system", "PaxosUncommittedIndex", null)));
|
.untilAsserted(() -> assertRows(execute(selectBuiltIndexesQuery), row("system", "PaxosUncommittedIndex", null), row("system_accord", "route", null)));
|
||||||
|
|
||||||
String indexName = "build_remove_test_idx";
|
String indexName = "build_remove_test_idx";
|
||||||
createTable("CREATE TABLE %s (a int, b int, c int, PRIMARY KEY (a, b))");
|
createTable("CREATE TABLE %s (a int, b int, c int, PRIMARY KEY (a, b))");
|
||||||
createIndex(String.format("CREATE INDEX %s ON %%s(c)", indexName));
|
createIndex(String.format("CREATE INDEX %s ON %%s(c)", indexName));
|
||||||
|
|
||||||
// check that there are no other rows in the built indexes table
|
// check that there are no other rows in the built indexes table
|
||||||
assertRows(execute(selectBuiltIndexesQuery), row(KEYSPACE, indexName, null), row("system", "PaxosUncommittedIndex", null));
|
assertRows(execute(selectBuiltIndexesQuery), row(KEYSPACE, indexName, null), row("system", "PaxosUncommittedIndex", null), row("system_accord", "route", null));
|
||||||
|
|
||||||
// rebuild the index and verify the built status table
|
// rebuild the index and verify the built status table
|
||||||
getCurrentColumnFamilyStore().rebuildSecondaryIndex(indexName);
|
getCurrentColumnFamilyStore().rebuildSecondaryIndex(indexName);
|
||||||
waitForIndexQueryable(indexName);
|
waitForIndexQueryable(indexName);
|
||||||
|
|
||||||
// check that there are no other rows in the built indexes table
|
// check that there are no other rows in the built indexes table
|
||||||
assertRows(execute(selectBuiltIndexesQuery), row(KEYSPACE, indexName, null), row("system", "PaxosUncommittedIndex", null));
|
assertRows(execute(selectBuiltIndexesQuery), row(KEYSPACE, indexName, null), row("system", "PaxosUncommittedIndex", null), row("system_accord", "route", null));
|
||||||
|
|
||||||
// check that dropping the index removes it from the built indexes table
|
// check that dropping the index removes it from the built indexes table
|
||||||
dropIndex("DROP INDEX %s." + indexName);
|
dropIndex("DROP INDEX %s." + indexName);
|
||||||
assertRows(execute(selectBuiltIndexesQuery), row("system", "PaxosUncommittedIndex", null));
|
assertRows(execute(selectBuiltIndexesQuery), row("system", "PaxosUncommittedIndex", null), row("system_accord", "route", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.io.util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.zip.CRC32C;
|
||||||
|
import java.util.zip.Checksum;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import accord.utils.Gen;
|
||||||
|
import accord.utils.Gens;
|
||||||
|
import accord.utils.Property.Command;
|
||||||
|
import accord.utils.Property.Commands;
|
||||||
|
import accord.utils.Property.UnitCommand;
|
||||||
|
import org.apache.cassandra.utils.FailingConsumer;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.stateful;
|
||||||
|
|
||||||
|
public class ChecksumedDataTest
|
||||||
|
{
|
||||||
|
public static final Supplier<Checksum> CHECKSUM_SUPPLIER = CRC32C::new;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void singleType()
|
||||||
|
{
|
||||||
|
DataOutputBuffer out = new DataOutputBuffer();
|
||||||
|
stateful().check(new Commands<DataOutputBuffer, DataOutputBuffer>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Gen<DataOutputBuffer> genInitialState()
|
||||||
|
{
|
||||||
|
return ignore -> {
|
||||||
|
out.clear();
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataOutputBuffer createSut(DataOutputBuffer dataOutputBuffer)
|
||||||
|
{
|
||||||
|
return dataOutputBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Gen<Command<DataOutputBuffer, DataOutputBuffer, ?>> commands(DataOutputBuffer dataOutputBuffer)
|
||||||
|
{
|
||||||
|
return Gens.oneOf(
|
||||||
|
rs -> {
|
||||||
|
boolean b = rs.nextBoolean();
|
||||||
|
return new StatelessChecksumCommand<>(out -> out.writeBoolean(b), DataInputPlus::readBoolean, () -> b);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
short s = (short) rs.nextInt(Short.MIN_VALUE, Short.MAX_VALUE);
|
||||||
|
return new StatelessChecksumCommand<>(out -> out.writeShort(s), DataInputPlus::readShort, () -> s);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
char c = (char) rs.nextInt(Character.MIN_VALUE, Character.MAX_VALUE);
|
||||||
|
return new StatelessChecksumCommand<>(out -> out.writeChar(c), DataInputPlus::readChar, () -> c);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
int value = rs.nextInt();
|
||||||
|
return new StatelessChecksumCommand<>(out -> out.writeInt(value), DataInputPlus::readInt, () -> value);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
float value = rs.nextFloat();
|
||||||
|
return new StatelessChecksumCommand<>(out -> out.writeFloat(value), DataInputPlus::readFloat, () -> value);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
double value = rs.nextDouble();
|
||||||
|
return new StatelessChecksumCommand<>(out -> out.writeDouble(value), DataInputPlus::readDouble, () -> value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withState()
|
||||||
|
{
|
||||||
|
DataOutputBuffer out = new DataOutputBuffer();
|
||||||
|
ChecksumedDataOutputPlus checksummedOut = new ChecksumedDataOutputPlus(out, CHECKSUM_SUPPLIER);
|
||||||
|
checksummedOut.resetChecksum();
|
||||||
|
stateful().check(new Commands<ChecksumedDataOutputPlus, List<StatefulChecksumCommand<?>>>() {
|
||||||
|
@Override
|
||||||
|
public Gen<ChecksumedDataOutputPlus> genInitialState()
|
||||||
|
{
|
||||||
|
return ignore -> {
|
||||||
|
out.clear();
|
||||||
|
checksummedOut.resetChecksum();
|
||||||
|
return checksummedOut;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StatefulChecksumCommand<?>> createSut(ChecksumedDataOutputPlus checksumedDataOutputPlus)
|
||||||
|
{
|
||||||
|
return new ArrayList<>(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Gen<Command<ChecksumedDataOutputPlus, List<StatefulChecksumCommand<?>>, ?>> commands(ChecksumedDataOutputPlus checksumedDataOutputPlus)
|
||||||
|
{
|
||||||
|
return Gens.oneOf(
|
||||||
|
rs -> {
|
||||||
|
boolean b = rs.nextBoolean();
|
||||||
|
return new StatefulChecksumCommand<>(out -> out.writeBoolean(b), DataInputPlus::readBoolean, () -> b);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
short s = (short) rs.nextInt(Short.MIN_VALUE, Short.MAX_VALUE);
|
||||||
|
return new StatefulChecksumCommand<>(out -> out.writeShort(s), DataInputPlus::readShort, () -> s);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
char c = (char) rs.nextInt(Character.MIN_VALUE, Character.MAX_VALUE);
|
||||||
|
return new StatefulChecksumCommand<>(out -> out.writeChar(c), DataInputPlus::readChar, () -> c);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
int value = rs.nextInt();
|
||||||
|
return new StatefulChecksumCommand<>(out -> out.writeInt(value), DataInputPlus::readInt, () -> value);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
float value = rs.nextFloat();
|
||||||
|
return new StatefulChecksumCommand<>(out -> out.writeFloat(value), DataInputPlus::readFloat, () -> value);
|
||||||
|
},
|
||||||
|
rs -> {
|
||||||
|
double value = rs.nextDouble();
|
||||||
|
return new StatefulChecksumCommand<>(out -> out.writeDouble(value), DataInputPlus::readDouble, () -> value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroySut(List<StatefulChecksumCommand<?>> sut) throws Throwable
|
||||||
|
{
|
||||||
|
ChecksumedDataInputPlus in = new ChecksumedDataInputPlus(new DataInputBuffer(out.unsafeGetBufferAndFlip(), false), CHECKSUM_SUPPLIER);
|
||||||
|
for (StatefulChecksumCommand<?> cmd : sut)
|
||||||
|
{
|
||||||
|
Assertions.assertThat(cmd.read.apply(in)).isEqualTo(cmd.expected.get());
|
||||||
|
Assertions.assertThat(in.checksum().getValue()).isEqualTo(cmd.checksum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface FailingFunction<I, O>
|
||||||
|
{
|
||||||
|
O apply(I input) throws Throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StatefulChecksumCommand<T> implements UnitCommand<ChecksumedDataOutputPlus, List<StatefulChecksumCommand<?>>>
|
||||||
|
{
|
||||||
|
private final FailingConsumer<DataOutputPlus> update;
|
||||||
|
private final FailingFunction<DataInputPlus, T> read;
|
||||||
|
private final Supplier<T> expected;
|
||||||
|
private Long checksum = null;
|
||||||
|
|
||||||
|
private StatefulChecksumCommand(FailingConsumer<DataOutputPlus> update, FailingFunction<DataInputPlus, T> read, Supplier<T> expected)
|
||||||
|
{
|
||||||
|
this.update = update;
|
||||||
|
this.read = read;
|
||||||
|
this.expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUnit(ChecksumedDataOutputPlus out) throws Throwable
|
||||||
|
{
|
||||||
|
update.doAccept(out);
|
||||||
|
checksum = out.checksum().getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runUnit(List<StatefulChecksumCommand<?>> sut)
|
||||||
|
{
|
||||||
|
sut.add(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StatelessChecksumCommand<T> implements Command<DataOutputBuffer, DataOutputBuffer, Long>
|
||||||
|
{
|
||||||
|
private final FailingConsumer<DataOutputPlus> update;
|
||||||
|
private final FailingFunction<DataInputPlus, T> read;
|
||||||
|
private final Supplier<T> expected;
|
||||||
|
|
||||||
|
private StatelessChecksumCommand(FailingConsumer<DataOutputPlus> update,
|
||||||
|
FailingFunction<DataInputPlus, T> read,
|
||||||
|
Supplier<T> expected)
|
||||||
|
{
|
||||||
|
this.update = update;
|
||||||
|
this.read = read;
|
||||||
|
this.expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long apply(DataOutputBuffer out) throws Throwable
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
ChecksumedDataOutputPlus c = new ChecksumedDataOutputPlus(out, CHECKSUM_SUPPLIER);
|
||||||
|
update.doAccept(c);
|
||||||
|
return c.checksum().getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long run(DataOutputBuffer out) throws Throwable
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
update.doAccept(out);
|
||||||
|
ChecksumedDataInputPlus i = new ChecksumedDataInputPlus(new DataInputBuffer(out.unsafeGetBufferAndFlip(), false), CHECKSUM_SUPPLIER);
|
||||||
|
Assertions.assertThat(read.apply(i)).isEqualTo(expected.get());
|
||||||
|
return i.checksum().getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkPostconditions(DataOutputBuffer dataOutputBuffer, Long expected,
|
||||||
|
DataOutputBuffer sut, Long actual)
|
||||||
|
{
|
||||||
|
Assertions.assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(DataOutputBuffer dataOutputBuffer)
|
||||||
|
{
|
||||||
|
return expected.get().getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -99,7 +99,6 @@ import org.apache.cassandra.locator.InetAddressAndPort;
|
||||||
import org.apache.cassandra.locator.LocalStrategy;
|
import org.apache.cassandra.locator.LocalStrategy;
|
||||||
import org.apache.cassandra.locator.Locator;
|
import org.apache.cassandra.locator.Locator;
|
||||||
import org.apache.cassandra.locator.RangesAtEndpoint;
|
import org.apache.cassandra.locator.RangesAtEndpoint;
|
||||||
import org.apache.cassandra.locator.Replica;
|
|
||||||
import org.apache.cassandra.net.ConnectionType;
|
import org.apache.cassandra.net.ConnectionType;
|
||||||
import org.apache.cassandra.net.IVerbHandler;
|
import org.apache.cassandra.net.IVerbHandler;
|
||||||
import org.apache.cassandra.net.Message;
|
import org.apache.cassandra.net.Message;
|
||||||
|
|
@ -117,7 +116,6 @@ import org.apache.cassandra.repair.state.SessionState;
|
||||||
import org.apache.cassandra.repair.state.ValidationState;
|
import org.apache.cassandra.repair.state.ValidationState;
|
||||||
import org.apache.cassandra.schema.KeyspaceMetadata;
|
import org.apache.cassandra.schema.KeyspaceMetadata;
|
||||||
import org.apache.cassandra.schema.KeyspaceParams;
|
import org.apache.cassandra.schema.KeyspaceParams;
|
||||||
import org.apache.cassandra.schema.ReplicationParams;
|
|
||||||
import org.apache.cassandra.schema.SchemaConstants;
|
import org.apache.cassandra.schema.SchemaConstants;
|
||||||
import org.apache.cassandra.schema.SystemDistributedKeyspace;
|
import org.apache.cassandra.schema.SystemDistributedKeyspace;
|
||||||
import org.apache.cassandra.schema.TableId;
|
import org.apache.cassandra.schema.TableId;
|
||||||
|
|
@ -125,7 +123,6 @@ import org.apache.cassandra.schema.TableMetadata;
|
||||||
import org.apache.cassandra.schema.Tables;
|
import org.apache.cassandra.schema.Tables;
|
||||||
import org.apache.cassandra.service.ActiveRepairService;
|
import org.apache.cassandra.service.ActiveRepairService;
|
||||||
import org.apache.cassandra.service.StorageService;
|
import org.apache.cassandra.service.StorageService;
|
||||||
import org.apache.cassandra.service.accord.AccordConfigurationService;
|
|
||||||
import org.apache.cassandra.service.accord.AccordService;
|
import org.apache.cassandra.service.accord.AccordService;
|
||||||
import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupComplete;
|
import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupComplete;
|
||||||
import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupHistory;
|
import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupHistory;
|
||||||
|
|
@ -141,21 +138,12 @@ import org.apache.cassandra.streaming.StreamState;
|
||||||
import org.apache.cassandra.streaming.StreamingChannel;
|
import org.apache.cassandra.streaming.StreamingChannel;
|
||||||
import org.apache.cassandra.streaming.StreamingDataInputPlus;
|
import org.apache.cassandra.streaming.StreamingDataInputPlus;
|
||||||
import org.apache.cassandra.tcm.ClusterMetadata;
|
import org.apache.cassandra.tcm.ClusterMetadata;
|
||||||
import org.apache.cassandra.tcm.ClusterMetadataService;
|
|
||||||
import org.apache.cassandra.tcm.Epoch;
|
|
||||||
import org.apache.cassandra.tcm.listeners.ChangeListener;
|
|
||||||
import org.apache.cassandra.tcm.membership.Directory;
|
|
||||||
import org.apache.cassandra.tcm.membership.Location;
|
import org.apache.cassandra.tcm.membership.Location;
|
||||||
import org.apache.cassandra.tcm.membership.NodeAddresses;
|
|
||||||
import org.apache.cassandra.tcm.membership.NodeId;
|
|
||||||
import org.apache.cassandra.tcm.ownership.DataPlacement;
|
|
||||||
import org.apache.cassandra.tcm.ownership.DataPlacements;
|
|
||||||
import org.apache.cassandra.tools.nodetool.Repair;
|
import org.apache.cassandra.tools.nodetool.Repair;
|
||||||
import org.apache.cassandra.utils.AbstractTypeGenerators;
|
import org.apache.cassandra.utils.AbstractTypeGenerators;
|
||||||
import org.apache.cassandra.utils.CassandraGenerators;
|
import org.apache.cassandra.utils.CassandraGenerators;
|
||||||
import org.apache.cassandra.utils.Clock;
|
import org.apache.cassandra.utils.Clock;
|
||||||
import org.apache.cassandra.utils.Closeable;
|
import org.apache.cassandra.utils.Closeable;
|
||||||
import org.apache.cassandra.utils.FBUtilities;
|
|
||||||
import org.apache.cassandra.utils.FailingBiConsumer;
|
import org.apache.cassandra.utils.FailingBiConsumer;
|
||||||
import org.apache.cassandra.utils.Generators;
|
import org.apache.cassandra.utils.Generators;
|
||||||
import org.apache.cassandra.utils.MBeanWrapper;
|
import org.apache.cassandra.utils.MBeanWrapper;
|
||||||
|
|
@ -587,29 +575,11 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
||||||
{
|
{
|
||||||
RepairType type = repairTypeGen.next(rs);
|
RepairType type = repairTypeGen.next(rs);
|
||||||
PreviewType previewType = previewTypeGen.next(rs);
|
PreviewType previewType = previewTypeGen.next(rs);
|
||||||
// TODO (required - IR) add this back and expand as part of IR integration
|
|
||||||
// boolean accordRepair = type == RepairType.FULL && previewType == PreviewType.NONE ? rs.nextBoolean() : false;
|
|
||||||
boolean accordRepair = false;
|
|
||||||
List<String> args = new ArrayList<>();
|
List<String> args = new ArrayList<>();
|
||||||
args.add(ks);
|
args.add(ks);
|
||||||
List<String> tables = tablesGen.next(rs);
|
List<String> tables = tablesGen.next(rs);
|
||||||
args.addAll(tables);
|
args.addAll(tables);
|
||||||
if (accordRepair)
|
|
||||||
{
|
|
||||||
List<Range<Token>> ranges = new ArrayList<>(StorageService.instance.getReplicas(ks, coordinator.broadcastAddressAndPort()).ranges());
|
|
||||||
ranges.sort(Comparator.naturalOrder());
|
|
||||||
Range<Token> range = ranges.get(rs.nextInt(0, ranges.size()));
|
|
||||||
args.add("--start-token");
|
|
||||||
args.add(range.left.toString());
|
|
||||||
args.add("--end-token");
|
|
||||||
Murmur3Partitioner.LongToken left = (Murmur3Partitioner.LongToken) range.left;
|
|
||||||
Token right = rs.nextBoolean() ? new Murmur3Partitioner.LongToken(left.token + 100) : range.right;
|
|
||||||
args.add(right.toString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
args.add("-pr");
|
args.add("-pr");
|
||||||
}
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case IR:
|
case IR:
|
||||||
|
|
@ -651,8 +621,6 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
||||||
}
|
}
|
||||||
if (rs.nextBoolean()) args.add("--optimise-streams");
|
if (rs.nextBoolean()) args.add("--optimise-streams");
|
||||||
RepairOption options = RepairOption.parse(Repair.parseOptionMap(() -> "test", args), DatabaseDescriptor.getPartitioner());
|
RepairOption options = RepairOption.parse(Repair.parseOptionMap(() -> "test", args), DatabaseDescriptor.getPartitioner());
|
||||||
if (accordRepair)
|
|
||||||
options = options.withAccordRepair(true);
|
|
||||||
if (options.getRanges().isEmpty())
|
if (options.getRanges().isEmpty())
|
||||||
{
|
{
|
||||||
if (options.isPrimaryRange())
|
if (options.isPrimaryRange())
|
||||||
|
|
@ -811,69 +779,11 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
||||||
}
|
}
|
||||||
List<InetAddressAndPort> addresses = new ArrayList<>(nodes.keySet());
|
List<InetAddressAndPort> addresses = new ArrayList<>(nodes.keySet());
|
||||||
addresses.sort(Comparator.naturalOrder());
|
addresses.sort(Comparator.naturalOrder());
|
||||||
NodeId tcmid = ClusterMetadata.current().directory.peerId(addresses.get(rs.nextInt(0, addresses.size())));
|
AccordService.unsafeSetNoop();
|
||||||
ServerTestUtils.recreateAccord(tcmid);
|
|
||||||
interceptTCMNotifications(tcmid);
|
|
||||||
|
|
||||||
setupSchema();
|
setupSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void interceptTCMNotifications(NodeId tcmid)
|
|
||||||
{
|
|
||||||
AccordService as = (AccordService) AccordService.instance();
|
|
||||||
AccordConfigurationService config = as.configurationService();
|
|
||||||
ClusterMetadataService.instance().log().removeListener(config);
|
|
||||||
ClusterMetadataService.instance().log().addListener(new ChangeListener()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot)
|
|
||||||
{
|
|
||||||
config.notifyPostCommit(sanitize(prev, tcmid), sanitize(next, tcmid), fromSnapshot);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClusterMetadata sanitize(ClusterMetadata metadata, NodeId tcmid)
|
|
||||||
{
|
|
||||||
if (metadata.directory.isEmpty())
|
|
||||||
return metadata;
|
|
||||||
ClusterMetadata sanitized = metadata.withDirectory(sanitize(metadata.directory, tcmid))
|
|
||||||
.withPlacements(sanitize(metadata.placements, FBUtilities.getBroadcastAddressAndPort()));
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Directory sanitize(Directory directory, NodeId tcmid)
|
|
||||||
{
|
|
||||||
if (directory.getNodeAddresses(tcmid) == null)
|
|
||||||
throw new AssertionError("Expected node " + tcmid + " but not found in " + directory);
|
|
||||||
for (NodeId peer : directory.peerIds())
|
|
||||||
{
|
|
||||||
if (peer.equals(tcmid))
|
|
||||||
continue;
|
|
||||||
directory = directory.without(peer);
|
|
||||||
}
|
|
||||||
directory = directory.withNodeAddresses(tcmid, NodeAddresses.current());
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DataPlacements sanitize(DataPlacements placements, InetAddressAndPort endpoint)
|
|
||||||
{
|
|
||||||
DataPlacements.Builder builder = DataPlacements.builder(placements.size());
|
|
||||||
for (Map.Entry<ReplicationParams, DataPlacement> e : placements)
|
|
||||||
builder.with(e.getKey(), sanitize(placements.lastModified(), e.getValue(), endpoint));
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private DataPlacement sanitize(Epoch epoch, DataPlacement value, InetAddressAndPort endpoint)
|
|
||||||
{
|
|
||||||
DataPlacement.Builder builder = DataPlacement.builder();
|
|
||||||
for (Range<Token> e : value.writes.ranges())
|
|
||||||
builder.withWriteReplica(epoch, new Replica(endpoint, e, true));
|
|
||||||
for (Range<Token> e : value.reads.ranges())
|
|
||||||
builder.withReadReplica(epoch, new Replica(endpoint, e, true));
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Closeable addListener(MessageListener listener)
|
public Closeable addListener(MessageListener listener)
|
||||||
{
|
{
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ public class AccordCommandStoreTest
|
||||||
Apply.SerializationSupport.create(txnId,
|
Apply.SerializationSupport.create(txnId,
|
||||||
route.slice(Ranges.of(TokenRange.fullRange(tableId))),
|
route.slice(Ranges.of(TokenRange.fullRange(tableId))),
|
||||||
1L,
|
1L,
|
||||||
Apply.Kind.Minimal,
|
Apply.Kind.Maximal,
|
||||||
depTxn.keys(),
|
depTxn.keys(),
|
||||||
executeAt,
|
executeAt,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,16 @@
|
||||||
|
|
||||||
package org.apache.cassandra.service.accord;
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
|
@ -43,18 +51,47 @@ import accord.primitives.Routable;
|
||||||
import accord.primitives.Timestamp;
|
import accord.primitives.Timestamp;
|
||||||
import accord.primitives.Txn;
|
import accord.primitives.Txn;
|
||||||
import accord.primitives.TxnId;
|
import accord.primitives.TxnId;
|
||||||
|
import accord.utils.async.AsyncChain;
|
||||||
|
import accord.utils.async.AsyncChains;
|
||||||
|
import accord.utils.async.Observable;
|
||||||
|
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||||
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.cql3.CQLTester;
|
import org.apache.cassandra.cql3.CQLTester;
|
||||||
|
import org.apache.cassandra.db.Keyspace;
|
||||||
import org.apache.cassandra.db.Mutation;
|
import org.apache.cassandra.db.Mutation;
|
||||||
|
import org.apache.cassandra.db.marshal.Int32Type;
|
||||||
|
import org.apache.cassandra.dht.IPartitioner;
|
||||||
|
import org.apache.cassandra.dht.LocalPartitioner;
|
||||||
|
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||||
|
import org.apache.cassandra.schema.MemtableParams;
|
||||||
import org.apache.cassandra.schema.Schema;
|
import org.apache.cassandra.schema.Schema;
|
||||||
|
import org.apache.cassandra.schema.SchemaProvider;
|
||||||
import org.apache.cassandra.schema.TableId;
|
import org.apache.cassandra.schema.TableId;
|
||||||
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
||||||
|
import org.apache.cassandra.service.accord.api.PartitionKey;
|
||||||
|
import org.apache.cassandra.utils.CassandraGenerators;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
import static org.apache.cassandra.config.DatabaseDescriptor.setSelectedSSTableFormat;
|
||||||
|
import static org.apache.cassandra.db.ColumnFamilyStore.FlushReason.UNIT_TESTS;
|
||||||
|
import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.setMemtable;
|
||||||
|
import static org.apache.cassandra.schema.SchemaConstants.ACCORD_KEYSPACE_NAME;
|
||||||
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
|
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
|
||||||
import static org.apache.cassandra.service.accord.AccordTestUtils.wrapInTxn;
|
import static org.apache.cassandra.utils.AbstractTypeGenerators.getTypeSupport;
|
||||||
|
import static org.apache.cassandra.utils.AccordGenerators.fromQT;
|
||||||
|
|
||||||
public class AccordKeyspaceTest extends CQLTester.InMemory
|
public class AccordKeyspaceTest extends CQLTester.InMemory
|
||||||
{
|
{
|
||||||
|
static
|
||||||
|
{
|
||||||
|
// since this test does frequent truncates, the info table gets updated and forced flushed... which is 90% of the cost of this test...
|
||||||
|
// this flag disables that flush
|
||||||
|
CassandraRelevantProperties.UNSAFE_SYSTEM.setBoolean(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serde()
|
public void serde()
|
||||||
{
|
{
|
||||||
|
|
@ -88,7 +125,7 @@ public class AccordKeyspaceTest extends CQLTester.InMemory
|
||||||
AccordSafeCommand safeCommand = new AccordSafeCommand(AccordTestUtils.loaded(id, null));
|
AccordSafeCommand safeCommand = new AccordSafeCommand(AccordTestUtils.loaded(id, null));
|
||||||
safeCommand.set(committed);
|
safeCommand.set(committed);
|
||||||
|
|
||||||
Commit commit = Commit.SerializerSupport.create(id, route.slice(scope), 1, Commit.Kind.StableFastPath, Ballot.ZERO, id, partialTxn.keys(), partialTxn, partialDeps, route, null);
|
Commit commit = Commit.SerializerSupport.create(id, route.slice(scope), 1, Commit.Kind.CommitSlowPath, Ballot.ZERO, id, partialTxn.keys(), partialTxn, partialDeps, route, null);
|
||||||
store.appendToJournal(commit);
|
store.appendToJournal(commit);
|
||||||
|
|
||||||
Mutation mutation = AccordKeyspace.getCommandMutation(store, safeCommand, 42);
|
Mutation mutation = AccordKeyspace.getCommandMutation(store, safeCommand, 42);
|
||||||
|
|
@ -97,4 +134,157 @@ public class AccordKeyspaceTest extends CQLTester.InMemory
|
||||||
Command loaded = AccordKeyspace.loadCommand(store, id);
|
Command loaded = AccordKeyspace.loadCommand(store, id);
|
||||||
Assertions.assertThat(loaded).isEqualTo(committed);
|
Assertions.assertThat(loaded).isEqualTo(committed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findOverlappingKeys()
|
||||||
|
{
|
||||||
|
var tableIdGen = fromQT(CassandraGenerators.TABLE_ID_GEN);
|
||||||
|
var partitionGen = fromQT(CassandraGenerators.partitioners());
|
||||||
|
|
||||||
|
var sstableFormats = DatabaseDescriptor.getSSTableFormats();
|
||||||
|
List<String> sstableFormatNames = new ArrayList<>(sstableFormats.keySet());
|
||||||
|
sstableFormatNames.sort(Comparator.naturalOrder());
|
||||||
|
|
||||||
|
List<String> memtableFormats = MemtableParams.knownDefinitions().stream()
|
||||||
|
.filter(name -> !name.startsWith("test_") && !name.equals("default"))
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
qt().check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
// control SSTable format
|
||||||
|
setSelectedSSTableFormat(sstableFormats.get(rs.pick(sstableFormatNames)));
|
||||||
|
// control memtable format
|
||||||
|
setMemtable(ACCORD_KEYSPACE_NAME, "commands_for_key", rs.pick(memtableFormats));
|
||||||
|
|
||||||
|
// define the tables w/ partitioners for the test
|
||||||
|
// this uses the ability to override the SchemaProvider for the keyspace and only defines the single API call expected: getTablePartitioner
|
||||||
|
TreeMap<TableId, IPartitioner> tables = new TreeMap<>();
|
||||||
|
int numTables = rs.nextInt(1, 3);
|
||||||
|
for (int i = 0; i < numTables; i++)
|
||||||
|
{
|
||||||
|
var tableId = tableIdGen.next(rs);
|
||||||
|
while (tables.containsKey(tableId))
|
||||||
|
tableId = tableIdGen.next(rs);
|
||||||
|
tables.put(tableId, partitionGen.next(rs));
|
||||||
|
}
|
||||||
|
SchemaProvider schema = Mockito.mock(SchemaProvider.class);
|
||||||
|
Mockito.when(schema.getTablePartitioner(Mockito.any())).thenAnswer((Answer<IPartitioner>) invocationOnMock -> tables.get(invocationOnMock.getArgument(0)));
|
||||||
|
AccordKeyspace.unsafeSetSchema(schema);
|
||||||
|
|
||||||
|
int numStores = rs.nextInt(1, 3);
|
||||||
|
|
||||||
|
// The model of the DB
|
||||||
|
TreeMap<Integer, SortedSet<PartitionKey>> storesToKeys = new TreeMap<>();
|
||||||
|
// write to the table and the model
|
||||||
|
for (int i = 0, numKeys = rs.nextInt(10, 20); i < numKeys; i++)
|
||||||
|
{
|
||||||
|
int store = rs.nextInt(0, numStores);
|
||||||
|
var keys = storesToKeys.computeIfAbsent(store, ignore -> new TreeSet<>());
|
||||||
|
PartitionKey pk = null;
|
||||||
|
// LocalPartitioner may have a type with a very small domain (boolean, vector<boolean, 1>, etc.), so need to bound the attempts
|
||||||
|
// else this will loop forever...
|
||||||
|
for (int attempt = 0; attempt < 10; attempt++)
|
||||||
|
{
|
||||||
|
TableId tableId = rs.pick(tables.keySet());
|
||||||
|
IPartitioner partitioner = tables.get(tableId);
|
||||||
|
ByteBuffer data = !(partitioner instanceof LocalPartitioner) ? Int32Type.instance.decompose(rs.nextInt())
|
||||||
|
: fromQT(getTypeSupport(partitioner.getTokenValidator()).bytesGen()).next(rs);
|
||||||
|
PartitionKey key = new PartitionKey(tableId, tables.get(tableId).decorateKey(data));
|
||||||
|
if (keys.add(key))
|
||||||
|
{
|
||||||
|
pk = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pk != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// using Mutation directly (what we do in Accord) can break when user data is too large; leading to data loss
|
||||||
|
// The memtable will allow the write, but it will be dropped when writing to the SSTable...
|
||||||
|
//TODO (now, correctness): since we store the user token + user key, if a key is close to the PK limits then we could tip over and loose our CFK
|
||||||
|
// new Mutation(AccordKeyspace.getCommandsForKeyPartitionUpdate(store, pk, 42, ByteBufferUtil.EMPTY_BYTE_BUFFER)).apply();
|
||||||
|
execute("INSERT INTO system_accord.commands_for_key (store_id, key_token, key) VALUES (?, ?, ?)",
|
||||||
|
store, AccordKeyspace.serializeRoutingKey(pk.toUnseekable()), AccordKeyspace.serializeKey(pk));
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException | InvalidRequestException e)
|
||||||
|
{
|
||||||
|
// Sometimes the types are too large (LocalPartitioner) so the mutation gets rejected... just ignore those cases
|
||||||
|
// Length 69912 > max length 65535
|
||||||
|
String msg = e.getMessage();
|
||||||
|
if (msg != null)
|
||||||
|
{
|
||||||
|
if ((msg.startsWith("Length ") && msg.endsWith("> max length 65535")) // Clustering was rejected
|
||||||
|
|| (msg.startsWith("Key length of ") && msg.endsWith(" is longer than maximum of 65535"))) // Partition was rejected
|
||||||
|
{
|
||||||
|
// failed to add
|
||||||
|
keys.remove(pk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read from the table and validate it matches the model
|
||||||
|
for (int read = 0; read < 2; read++) // read=0 is memtable, read=1 is sstable
|
||||||
|
{
|
||||||
|
{
|
||||||
|
// Make sure no data was lost
|
||||||
|
// An issue was found that system mutations bypass checks so make their way to the Memtable, but when we flush to SSTable
|
||||||
|
// they get filtered out, causing data loss... This check is here to make sure that the data is present (test covers Memtable + SStable)
|
||||||
|
// in the storage before checking if the filtering logic is correct
|
||||||
|
TreeMap<Integer, SortedSet<ByteBuffer>> expectedCqlStoresToKeys = new TreeMap<>();
|
||||||
|
for (var e : storesToKeys.entrySet())
|
||||||
|
{
|
||||||
|
int store = e.getKey();
|
||||||
|
expectedCqlStoresToKeys.put(store, new TreeSet<>(e.getValue().stream().map(p -> AccordKeyspace.serializeRoutingKey(p.toUnseekable())).collect(Collectors.toList())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure no data loss... when this test was written sstable had all the rows but the sstable didn't... this
|
||||||
|
// is mostly a santity check to detect that case early
|
||||||
|
var resultSet = execute("SELECT store_id, key_token FROM system_accord.commands_for_key ALLOW FILTERING");
|
||||||
|
TreeMap<Integer, SortedSet<ByteBuffer>> cqlStoresToKeys = new TreeMap<>();
|
||||||
|
for (var row : resultSet)
|
||||||
|
{
|
||||||
|
int storeId = row.getInt("store_id");
|
||||||
|
ByteBuffer bb = row.getBytes("key_token");
|
||||||
|
cqlStoresToKeys.computeIfAbsent(storeId, ignore -> new TreeSet<>()).add(bb);
|
||||||
|
}
|
||||||
|
Assertions.assertThat(cqlStoresToKeys).isEqualTo(expectedCqlStoresToKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0, queries = rs.nextInt(1, 5); i < queries; i++)
|
||||||
|
{
|
||||||
|
int store = rs.pick(storesToKeys.keySet());
|
||||||
|
var keysForStore = new ArrayList<>(storesToKeys.get(store));
|
||||||
|
|
||||||
|
int offset;
|
||||||
|
int offsetEnd;
|
||||||
|
if (keysForStore.size() == 1)
|
||||||
|
{
|
||||||
|
offset = 0;
|
||||||
|
offsetEnd = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
offset = rs.nextInt(0, keysForStore.size());
|
||||||
|
offsetEnd = rs.nextInt(offset, keysForStore.size()) + 1;
|
||||||
|
}
|
||||||
|
List<PartitionKey> expected = keysForStore.subList(offset, offsetEnd);
|
||||||
|
PartitionKey start = expected.get(0);
|
||||||
|
PartitionKey end = expected.get(expected.size() - 1);
|
||||||
|
|
||||||
|
AsyncChain<List<PartitionKey>> map = Observable.asChain(callback -> AccordKeyspace.findAllKeysBetween(store, start.toUnseekable(), true, end.toUnseekable(), true, callback));
|
||||||
|
List<PartitionKey> actual = AsyncChains.getUnchecked(map);
|
||||||
|
Assertions.assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read == 0)
|
||||||
|
Keyspace.open(ACCORD_KEYSPACE_NAME).getColumnFamilyStore("commands_for_key").forceBlockingFlush(UNIT_TESTS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ import static org.apache.cassandra.distributed.util.QueryResultUtil.assertThat;
|
||||||
|
|
||||||
public class AccordReadRepairTest extends AccordTestBase
|
public class AccordReadRepairTest extends AccordTestBase
|
||||||
{
|
{
|
||||||
private static final Logger logger = LoggerFactory.getLogger(org.apache.cassandra.distributed.test.accord.AccordCQLTest.class);
|
private static final Logger logger = LoggerFactory.getLogger(AccordReadRepairTest.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Logger logger()
|
protected Logger logger()
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ import static java.lang.String.format;
|
||||||
|
|
||||||
public class AccordTestUtils
|
public class AccordTestUtils
|
||||||
{
|
{
|
||||||
|
private static final AccordAgent AGENT = new AccordAgent();
|
||||||
public static final TableId TABLE_ID1 = TableId.fromString("00000000-0000-0000-0000-000000000001");
|
public static final TableId TABLE_ID1 = TableId.fromString("00000000-0000-0000-0000-000000000001");
|
||||||
|
|
||||||
public static class Commands
|
public static class Commands
|
||||||
|
|
@ -318,6 +319,11 @@ public class AccordTestUtils
|
||||||
return createTxn(key, key);
|
return createTxn(key, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Txn createTxn(Txn.Kind kind, Seekables<?, ?> seekables)
|
||||||
|
{
|
||||||
|
return AGENT.emptyTxn(kind, seekables);
|
||||||
|
}
|
||||||
|
|
||||||
public static Ranges fullRange(Txn txn)
|
public static Ranges fullRange(Txn txn)
|
||||||
{
|
{
|
||||||
return fullRange(txn.keys());
|
return fullRange(txn.keys());
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import accord.local.SaveStatus;
|
|
||||||
import accord.primitives.Ranges;
|
|
||||||
import accord.primitives.RoutableKey;
|
|
||||||
import accord.primitives.TxnId;
|
|
||||||
import accord.utils.AccordGens;
|
|
||||||
import accord.utils.Gen;
|
|
||||||
import accord.utils.Gens;
|
|
||||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
|
||||||
import org.apache.cassandra.dht.IPartitioner;
|
|
||||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
|
||||||
import org.apache.cassandra.dht.RandomPartitioner;
|
|
||||||
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
|
||||||
import org.apache.cassandra.utils.AccordGenerators;
|
|
||||||
import org.apache.cassandra.utils.Interval;
|
|
||||||
import org.apache.cassandra.utils.IntervalTree;
|
|
||||||
|
|
||||||
import static accord.utils.Property.qt;
|
|
||||||
import static org.apache.cassandra.simulator.RandomSource.Choices.choose;
|
|
||||||
import static org.apache.cassandra.service.accord.AccordTestUtils.TABLE_ID1;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
public class CommandsForRangesTest
|
|
||||||
{
|
|
||||||
private static Ranges FULL_RANGE = Ranges.of(new TokenRange(AccordRoutingKey.SentinelKey.min(TABLE_ID1), AccordRoutingKey.SentinelKey.max(TABLE_ID1)));
|
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
public static void setup() throws NoSuchFieldException, IllegalAccessException
|
|
||||||
{
|
|
||||||
DatabaseDescriptor.clientInitialization();
|
|
||||||
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void prune()
|
|
||||||
{
|
|
||||||
qt().forAll(cfr()).check(cfr -> {
|
|
||||||
// public void prune(TxnId pruneBefore, Ranges pruneRanges)
|
|
||||||
// private Timestamp maxRedundant;
|
|
||||||
List<TxnId> knownIds = new ArrayList<>(cfr.knownIds());
|
|
||||||
knownIds.sort(Comparator.naturalOrder());
|
|
||||||
|
|
||||||
assertThat(cfr.maxRedundant()).isNull();
|
|
||||||
|
|
||||||
TxnId min = knownIds.get(0);
|
|
||||||
TxnId max = knownIds.get(knownIds.size() - 1);
|
|
||||||
|
|
||||||
// should do nothing
|
|
||||||
IntervalTree<RoutableKey, CommandsForRanges.RangeCommandSummary, Interval<RoutableKey, CommandsForRanges.RangeCommandSummary>> tree = cfr.tree();
|
|
||||||
cfr.prune(min, FULL_RANGE);
|
|
||||||
assertThat(cfr.maxRedundant()).isNull();
|
|
||||||
assertThat(cfr.tree()).isEqualTo(tree);
|
|
||||||
|
|
||||||
cfr.prune(max, FULL_RANGE);
|
|
||||||
assertThat(cfr.knownIds()).containsExactly(max);
|
|
||||||
assertThat(cfr.maxRedundant()).isEqualTo(knownIds.size() == 1 ? null : knownIds.get(knownIds.size() - 2));
|
|
||||||
|
|
||||||
cfr.prune(new TxnId(max.logicalNext(max.node), max.kind(), max.domain()), FULL_RANGE);
|
|
||||||
assertThat(cfr.knownIds()).isEmpty();
|
|
||||||
assertThat(cfr.maxRedundant()).isEqualTo(max);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Gen<CommandsForRanges> cfr()
|
|
||||||
{
|
|
||||||
// TODO (coverage): once all partitioners work with regard to splitting, then should test all
|
|
||||||
Gen<IPartitioner> partitionerGen = rs -> choose(rs, Murmur3Partitioner.instance, RandomPartitioner.instance);
|
|
||||||
Gen<SaveStatus> statusGen = Gens.enums().all(SaveStatus.class);
|
|
||||||
return rs -> {
|
|
||||||
IPartitioner partitioner = partitionerGen.next(rs);
|
|
||||||
// some code reaches to the DD for partitioner...
|
|
||||||
DatabaseDescriptor.setPartitionerUnsafe(partitioner);
|
|
||||||
Gen<Ranges> rangesGen = AccordGenerators.ranges(ignore -> Collections.singleton(TABLE_ID1), ignore -> partitioner);
|
|
||||||
CommandsForRanges.Builder builder = new CommandsForRanges.Builder();
|
|
||||||
int numTxn = rs.nextInt(1, 10);
|
|
||||||
Set<TxnId> uniq = new HashSet<>();
|
|
||||||
for (int i = 0; i < numTxn; i++)
|
|
||||||
{
|
|
||||||
TxnId id;
|
|
||||||
while (!uniq.add(id = AccordGens.txnIds().next(rs))) {}
|
|
||||||
builder.put(id, rangesGen.next(rs), statusGen.next(rs), id, Collections.emptyList());
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.service.accord;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
|
import accord.local.SerializerSupport;
|
||||||
|
import accord.messages.Accept;
|
||||||
|
import accord.messages.Apply;
|
||||||
|
import accord.messages.BeginRecovery;
|
||||||
|
import accord.messages.Commit;
|
||||||
|
import accord.messages.Message;
|
||||||
|
import accord.messages.MessageType;
|
||||||
|
import accord.messages.PreAccept;
|
||||||
|
import accord.messages.Propagate;
|
||||||
|
import accord.primitives.Ballot;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import org.agrona.collections.ObjectHashSet;
|
||||||
|
|
||||||
|
import static accord.messages.MessageType.ACCEPT_REQ;
|
||||||
|
import static accord.messages.MessageType.APPLY_MAXIMAL_REQ;
|
||||||
|
import static accord.messages.MessageType.APPLY_MINIMAL_REQ;
|
||||||
|
import static accord.messages.MessageType.BEGIN_RECOVER_REQ;
|
||||||
|
import static accord.messages.MessageType.COMMIT_MAXIMAL_REQ;
|
||||||
|
import static accord.messages.MessageType.COMMIT_SLOW_PATH_REQ;
|
||||||
|
import static accord.messages.MessageType.PRE_ACCEPT_REQ;
|
||||||
|
import static accord.messages.MessageType.PROPAGATE_APPLY_MSG;
|
||||||
|
import static accord.messages.MessageType.PROPAGATE_PRE_ACCEPT_MSG;
|
||||||
|
import static accord.messages.MessageType.PROPAGATE_STABLE_MSG;
|
||||||
|
import static accord.messages.MessageType.STABLE_FAST_PATH_REQ;
|
||||||
|
import static accord.messages.MessageType.STABLE_MAXIMAL_REQ;
|
||||||
|
|
||||||
|
public class MockJournal implements IJournal
|
||||||
|
{
|
||||||
|
private final Map<AccordJournal.Key, Message> writes = new HashMap<>();
|
||||||
|
@Override
|
||||||
|
public SerializerSupport.MessageProvider makeMessageProvider(TxnId txnId)
|
||||||
|
{
|
||||||
|
return new SerializerSupport.MessageProvider()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Set<MessageType> test(Set<MessageType> messages)
|
||||||
|
{
|
||||||
|
Set<AccordJournal.Key> keys = new ObjectHashSet<>(messages.size() + 1, 0.9f);
|
||||||
|
for (MessageType message : messages)
|
||||||
|
for (AccordJournal.Type synonymousType : AccordJournal.Type.synonymousTypesFromMessageType(message))
|
||||||
|
keys.add(new AccordJournal.Key(txnId, synonymousType));
|
||||||
|
Set<AccordJournal.Key> presentKeys = Sets.intersection(writes.keySet(), keys);
|
||||||
|
Set<MessageType> presentMessages = new ObjectHashSet<>(presentKeys.size() + 1, 0.9f);
|
||||||
|
for (AccordJournal.Key key : presentKeys)
|
||||||
|
presentMessages.add(key.type.outgoingType);
|
||||||
|
return presentMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends Message> T get(AccordJournal.Key key)
|
||||||
|
{
|
||||||
|
return (T) writes.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends Message> T get(MessageType messageType)
|
||||||
|
{
|
||||||
|
for (AccordJournal.Type type : AccordJournal.Type.synonymousTypesFromMessageType(messageType))
|
||||||
|
{
|
||||||
|
T value = get(new AccordJournal.Key(txnId, type));
|
||||||
|
if (value != null) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreAccept preAccept()
|
||||||
|
{
|
||||||
|
return get(PRE_ACCEPT_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BeginRecovery beginRecover()
|
||||||
|
{
|
||||||
|
return get(BEGIN_RECOVER_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Propagate propagatePreAccept()
|
||||||
|
{
|
||||||
|
return get(PROPAGATE_PRE_ACCEPT_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Accept accept(Ballot ballot)
|
||||||
|
{
|
||||||
|
return get(ACCEPT_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Commit commitSlowPath()
|
||||||
|
{
|
||||||
|
return get(COMMIT_SLOW_PATH_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Commit commitMaximal()
|
||||||
|
{
|
||||||
|
return get(COMMIT_MAXIMAL_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Commit stableFastPath()
|
||||||
|
{
|
||||||
|
return get(STABLE_FAST_PATH_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Commit stableMaximal()
|
||||||
|
{
|
||||||
|
return get(STABLE_MAXIMAL_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Propagate propagateStable()
|
||||||
|
{
|
||||||
|
return get(PROPAGATE_STABLE_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Apply applyMinimal()
|
||||||
|
{
|
||||||
|
return get(APPLY_MINIMAL_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Apply applyMaximal()
|
||||||
|
{
|
||||||
|
return get(APPLY_MAXIMAL_REQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Propagate propagateApply()
|
||||||
|
{
|
||||||
|
return get(PROPAGATE_APPLY_MSG);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendMessageBlocking(Message message)
|
||||||
|
{
|
||||||
|
AccordJournal.Type type = AccordJournal.Type.fromMessageType(message.type());
|
||||||
|
AccordJournal.Key key = new AccordJournal.Key(type.txnId(message), type);
|
||||||
|
writes.put(key, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import accord.local.PreLoadContext;
|
||||||
|
import accord.local.SaveStatus;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.utils.AccordGens;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
|
||||||
|
public class SimpleSimulatedAccordCommandStoreTest extends SimulatedAccordCommandStoreTestBase
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void emptyTxns()
|
||||||
|
{
|
||||||
|
qt().withExamples(10).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
for (int i = 0, examples = 100; i < examples; i++)
|
||||||
|
{
|
||||||
|
TxnId id = AccordGens.txnIds().next(rs);
|
||||||
|
instance.process(PreLoadContext.contextFor(id), (safe) -> {
|
||||||
|
var safeCommand = safe.get(id, id, Ranges.EMPTY);
|
||||||
|
var command = safeCommand.current();
|
||||||
|
Assertions.assertThat(command.saveStatus()).isEqualTo(SaveStatus.Uninitialised);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,356 @@
|
||||||
|
/*
|
||||||
|
* 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.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.ToLongFunction;
|
||||||
|
|
||||||
|
import accord.impl.SizeOfIntersectionSorter;
|
||||||
|
import accord.impl.TestAgent;
|
||||||
|
import accord.local.Command;
|
||||||
|
import accord.local.CommandStore;
|
||||||
|
import accord.local.CommandStores;
|
||||||
|
import accord.local.Node;
|
||||||
|
import accord.local.NodeTimeService;
|
||||||
|
import accord.local.PreLoadContext;
|
||||||
|
import accord.local.SafeCommandStore;
|
||||||
|
import accord.messages.BeginRecovery;
|
||||||
|
import accord.messages.Message;
|
||||||
|
import accord.messages.PreAccept;
|
||||||
|
import accord.messages.TxnRequest;
|
||||||
|
import accord.primitives.Ballot;
|
||||||
|
import accord.primitives.FullRoute;
|
||||||
|
import accord.primitives.Keys;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.Routable;
|
||||||
|
import accord.primitives.RoutableKey;
|
||||||
|
import accord.primitives.Timestamp;
|
||||||
|
import accord.primitives.Txn;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.topology.Topologies;
|
||||||
|
import accord.topology.Topology;
|
||||||
|
import accord.utils.Gens;
|
||||||
|
import accord.utils.RandomSource;
|
||||||
|
import accord.utils.async.AsyncChains;
|
||||||
|
import accord.utils.async.AsyncResult;
|
||||||
|
import org.apache.cassandra.concurrent.ExecutorFactory;
|
||||||
|
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
|
||||||
|
import org.apache.cassandra.concurrent.SimulatedExecutorFactory;
|
||||||
|
import org.apache.cassandra.concurrent.Stage;
|
||||||
|
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||||
|
import org.apache.cassandra.db.DataRange;
|
||||||
|
import org.apache.cassandra.db.Keyspace;
|
||||||
|
import org.apache.cassandra.db.compaction.CompactionManager;
|
||||||
|
import org.apache.cassandra.db.filter.ColumnFilter;
|
||||||
|
import org.apache.cassandra.db.memtable.Memtable;
|
||||||
|
import org.apache.cassandra.metrics.AccordStateCacheMetrics;
|
||||||
|
import org.apache.cassandra.tcm.ClusterMetadata;
|
||||||
|
import org.apache.cassandra.utils.FBUtilities;
|
||||||
|
import org.apache.cassandra.utils.Generators;
|
||||||
|
import org.apache.cassandra.utils.Pair;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.db.ColumnFamilyStore.FlushReason.UNIT_TESTS;
|
||||||
|
import static org.apache.cassandra.schema.SchemaConstants.ACCORD_KEYSPACE_NAME;
|
||||||
|
import static org.apache.cassandra.utils.AccordGenerators.fromQT;
|
||||||
|
|
||||||
|
class SimulatedAccordCommandStore implements AutoCloseable
|
||||||
|
{
|
||||||
|
private final List<Throwable> failures = new ArrayList<>();
|
||||||
|
private final SimulatedExecutorFactory globalExecutor;
|
||||||
|
private final CommandStore.EpochUpdateHolder updateHolder;
|
||||||
|
private final BooleanSupplier shouldEvict, shouldFlush, shouldCompact;
|
||||||
|
|
||||||
|
public final NodeTimeService timeService;
|
||||||
|
public final AccordCommandStore store;
|
||||||
|
public final Node.Id nodeId;
|
||||||
|
public final Topology topology;
|
||||||
|
public final MockJournal journal;
|
||||||
|
public final ScheduledExecutorPlus unorderedScheduled;
|
||||||
|
public final List<String> evictions = new ArrayList<>();
|
||||||
|
|
||||||
|
SimulatedAccordCommandStore(RandomSource rs)
|
||||||
|
{
|
||||||
|
globalExecutor = new SimulatedExecutorFactory(accord.utilsfork.RandomSource.wrap(rs).fork(), fromQT(Generators.TIMESTAMP_GEN.map(java.sql.Timestamp::getTime)).mapToLong(TimeUnit.MILLISECONDS::toNanos).next(rs), failures::add);
|
||||||
|
this.unorderedScheduled = globalExecutor.scheduled("ignored");
|
||||||
|
ExecutorFactory.Global.unsafeSet(globalExecutor);
|
||||||
|
Stage.READ.unsafeSetExecutor(unorderedScheduled);
|
||||||
|
Stage.MUTATION.unsafeSetExecutor(unorderedScheduled);
|
||||||
|
for (Stage stage : Arrays.asList(Stage.MISC, Stage.ACCORD_MIGRATION))
|
||||||
|
stage.unsafeSetExecutor(globalExecutor.configureSequential("ignore").build());
|
||||||
|
|
||||||
|
this.updateHolder = new CommandStore.EpochUpdateHolder();
|
||||||
|
this.nodeId = AccordTopology.tcmIdToAccord(ClusterMetadata.currentNullable().myNodeId());
|
||||||
|
this.timeService = new NodeTimeService()
|
||||||
|
{
|
||||||
|
private final ToLongFunction<TimeUnit> unixWrapper = NodeTimeService.unixWrapper(TimeUnit.NANOSECONDS, this::now);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Node.Id id()
|
||||||
|
{
|
||||||
|
return nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long epoch()
|
||||||
|
{
|
||||||
|
return ClusterMetadata.current().epoch.getEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long now()
|
||||||
|
{
|
||||||
|
return globalExecutor.nanoTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long unix(TimeUnit unit)
|
||||||
|
{
|
||||||
|
return unixWrapper.applyAsLong(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Timestamp uniqueNow(Timestamp atLeast)
|
||||||
|
{
|
||||||
|
var now = Timestamp.fromValues(epoch(), now(), nodeId);
|
||||||
|
if (now.compareTo(atLeast) < 0)
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.journal = new MockJournal();
|
||||||
|
this.store = new AccordCommandStore(0,
|
||||||
|
timeService,
|
||||||
|
new TestAgent.RethrowAgent()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public boolean isExpired(TxnId initiated, long now)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
ignore -> AccordTestUtils.NOOP_PROGRESS_LOG,
|
||||||
|
updateHolder,
|
||||||
|
journal,
|
||||||
|
new AccordStateCacheMetrics("test"));
|
||||||
|
|
||||||
|
store.cache().instances().forEach(i -> {
|
||||||
|
i.register(new AccordStateCache.Listener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onAdd(AccordCachingState state)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease(AccordCachingState state)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvict(AccordCachingState state)
|
||||||
|
{
|
||||||
|
evictions.add(i + " evicted " + state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.topology = AccordTopology.createAccordTopology(ClusterMetadata.current());
|
||||||
|
var rangesForEpoch = new CommandStores.RangesForEpoch(topology.epoch(), topology.ranges(), store);
|
||||||
|
updateHolder.add(topology.epoch(), rangesForEpoch, topology.ranges());
|
||||||
|
updateHolder.updateGlobal(topology.ranges());
|
||||||
|
|
||||||
|
shouldEvict = boolSource(rs.fork());
|
||||||
|
shouldFlush = boolSource(rs.fork());
|
||||||
|
shouldCompact = boolSource(rs.fork());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BooleanSupplier boolSource(RandomSource rs)
|
||||||
|
{
|
||||||
|
var gen = Gens.bools().mixedDistribution().next(rs);
|
||||||
|
return () -> gen.next(rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxnId nextTxnId(Txn.Kind kind, Routable.Domain domain)
|
||||||
|
{
|
||||||
|
return new TxnId(timeService.epoch(), timeService.now(), kind, domain, nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void maybeCacheEvict(Keys keys, Ranges ranges)
|
||||||
|
{
|
||||||
|
AccordStateCache cache = store.cache();
|
||||||
|
cache.forEach(state -> {
|
||||||
|
Class<?> keyType = state.key().getClass();
|
||||||
|
if (TxnId.class.equals(keyType))
|
||||||
|
{
|
||||||
|
Command command = (Command) state.state().get();
|
||||||
|
if (command.known().definition.isKnown()
|
||||||
|
&& (command.partialTxn().keys().intersects(keys) || ranges.intersects(command.partialTxn().keys()))
|
||||||
|
&& shouldEvict.getAsBoolean())
|
||||||
|
cache.maybeEvict(state);
|
||||||
|
}
|
||||||
|
else if (RoutableKey.class.isAssignableFrom(keyType))
|
||||||
|
{
|
||||||
|
RoutableKey key = (RoutableKey) state.key();
|
||||||
|
if ((keys.contains(key) || ranges.intersects(key))
|
||||||
|
&& shouldEvict.getAsBoolean())
|
||||||
|
cache.maybeEvict(state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AssertionError("Unexpected key type: " + state.key().getClass());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var store : Keyspace.open(ACCORD_KEYSPACE_NAME).getColumnFamilyStores())
|
||||||
|
{
|
||||||
|
Memtable memtable = store.getCurrentMemtable();
|
||||||
|
if (memtable.partitionCount() == 0 || !intersects(store, memtable, keys, ranges))
|
||||||
|
continue;
|
||||||
|
if (shouldFlush.getAsBoolean())
|
||||||
|
store.forceBlockingFlush(UNIT_TESTS);
|
||||||
|
}
|
||||||
|
for (var store : Keyspace.open(ACCORD_KEYSPACE_NAME).getColumnFamilyStores())
|
||||||
|
{
|
||||||
|
if (store.getLiveSSTables().size() > 5 && shouldCompact.getAsBoolean())
|
||||||
|
{
|
||||||
|
// compaction no-op since auto-compaction is disabled... so need to enable quickly
|
||||||
|
store.enableAutoCompaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FBUtilities.waitOnFutures(CompactionManager.instance.submitBackground(store));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
store.disableAutoCompaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean intersects(ColumnFamilyStore store, Memtable memtable, Keys keys, Ranges ranges)
|
||||||
|
{
|
||||||
|
if (keys.isEmpty() && ranges.isEmpty()) // shouldn't happen, but just in case...
|
||||||
|
return false;
|
||||||
|
switch (store.name)
|
||||||
|
{
|
||||||
|
case "commands_for_key":
|
||||||
|
// pk = (store_id, key_token, key)
|
||||||
|
// since this is simulating a single store, store_id is a constant, so check key
|
||||||
|
try (var it = memtable.partitionIterator(ColumnFilter.NONE, DataRange.allData(store.getPartitioner()), null))
|
||||||
|
{
|
||||||
|
while (it.hasNext())
|
||||||
|
{
|
||||||
|
var key = AccordKeyspace.CommandsForKeysAccessor.getKey(it.next().partitionKey());
|
||||||
|
if (keys.contains(key) || ranges.intersects(key))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkFailures()
|
||||||
|
{
|
||||||
|
if (Thread.interrupted())
|
||||||
|
failures.add(new InterruptedException());
|
||||||
|
if (failures.isEmpty()) return;
|
||||||
|
AssertionError error = new AssertionError("Unexpected exceptions found");
|
||||||
|
failures.forEach(error::addSuppressed);
|
||||||
|
failures.clear();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T process(TxnRequest<T> request) throws ExecutionException, InterruptedException
|
||||||
|
{
|
||||||
|
return process(request, request::apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T process(PreLoadContext loadCtx, Function<? super SafeCommandStore, T> function) throws ExecutionException, InterruptedException
|
||||||
|
{
|
||||||
|
var result = processAsync(loadCtx, function);
|
||||||
|
processAll();
|
||||||
|
return AsyncChains.getBlocking(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> AsyncResult<T> processAsync(TxnRequest<T> request)
|
||||||
|
{
|
||||||
|
return processAsync(request, request::apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> AsyncResult<T> processAsync(PreLoadContext loadCtx, Function<? super SafeCommandStore, T> function)
|
||||||
|
{
|
||||||
|
if (loadCtx instanceof Message)
|
||||||
|
journal.appendMessageBlocking((Message) loadCtx);
|
||||||
|
return store.submit(loadCtx, function).beginAsResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair<TxnId, AsyncResult<PreAccept.PreAcceptOk>> enqueuePreAccept(Txn txn, FullRoute<?> route)
|
||||||
|
{
|
||||||
|
TxnId txnId = nextTxnId(txn.kind(), txn.keys().domain());
|
||||||
|
PreAccept preAccept = new PreAccept(nodeId, new Topologies.Single(SizeOfIntersectionSorter.SUPPLIER, topology), txnId, txn, route);
|
||||||
|
return Pair.create(txnId, processAsync(preAccept, safe -> {
|
||||||
|
var reply = preAccept.apply(safe);
|
||||||
|
Assertions.assertThat(reply.isOk()).isTrue();
|
||||||
|
return (PreAccept.PreAcceptOk) reply;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair<TxnId, AsyncResult<BeginRecovery.RecoverOk>> enqueueBeginRecovery(Txn txn, FullRoute<?> route)
|
||||||
|
{
|
||||||
|
TxnId txnId = nextTxnId(txn.kind(), txn.keys().domain());
|
||||||
|
Ballot ballot = Ballot.fromValues(timeService.epoch(), timeService.now(), nodeId);
|
||||||
|
BeginRecovery br = new BeginRecovery(nodeId, new Topologies.Single(SizeOfIntersectionSorter.SUPPLIER, topology), txnId, txn, route, ballot);
|
||||||
|
|
||||||
|
return Pair.create(txnId, processAsync(br, safe -> {
|
||||||
|
var reply = br.apply(safe);
|
||||||
|
Assertions.assertThat(reply.isOk()).isTrue();
|
||||||
|
return (BeginRecovery.RecoverOk) reply;
|
||||||
|
}).beginAsResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processAll()
|
||||||
|
{
|
||||||
|
while (processOne())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean processOne()
|
||||||
|
{
|
||||||
|
boolean result = globalExecutor.processOne();
|
||||||
|
checkFailures();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception
|
||||||
|
{
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,348 @@
|
||||||
|
/*
|
||||||
|
* 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.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
|
||||||
|
import accord.api.Key;
|
||||||
|
import accord.impl.SizeOfIntersectionSorter;
|
||||||
|
import accord.local.Node;
|
||||||
|
import accord.messages.BeginRecovery;
|
||||||
|
import accord.messages.PreAccept;
|
||||||
|
import accord.primitives.Ballot;
|
||||||
|
import accord.primitives.Deps;
|
||||||
|
import accord.primitives.FullRoute;
|
||||||
|
import accord.primitives.Keys;
|
||||||
|
import accord.primitives.LatestDeps;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.Txn;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.topology.Topologies;
|
||||||
|
import accord.utils.async.AsyncChains;
|
||||||
|
import accord.utils.async.AsyncResult;
|
||||||
|
import org.apache.cassandra.ServerTestUtils;
|
||||||
|
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||||
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
|
import org.apache.cassandra.cql3.CQLTester;
|
||||||
|
import org.apache.cassandra.db.Keyspace;
|
||||||
|
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||||
|
import org.apache.cassandra.schema.Schema;
|
||||||
|
import org.apache.cassandra.schema.TableId;
|
||||||
|
import org.apache.cassandra.schema.TableMetadata;
|
||||||
|
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
||||||
|
import org.apache.cassandra.tcm.ClusterMetadata;
|
||||||
|
import org.apache.cassandra.utils.Pair;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.schema.SchemaConstants.ACCORD_KEYSPACE_NAME;
|
||||||
|
|
||||||
|
public abstract class SimulatedAccordCommandStoreTestBase extends CQLTester
|
||||||
|
{
|
||||||
|
static
|
||||||
|
{
|
||||||
|
CassandraRelevantProperties.TEST_ACCORD_STORE_THREAD_CHECKS_ENABLED.setBoolean(false);
|
||||||
|
// since this test does frequent truncates, the info table gets updated and forced flushed... which is 90% of the cost of this test...
|
||||||
|
// this flag disables that flush
|
||||||
|
CassandraRelevantProperties.UNSAFE_SYSTEM.setBoolean(true);
|
||||||
|
// The plan is to migrate away from SAI, so rather than hacking around timeout issues; just disable for now
|
||||||
|
CassandraRelevantProperties.SAI_TEST_DISABLE_TIMEOUT.setBoolean(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected enum DepsMessage
|
||||||
|
{PreAccept, BeginRecovery, PreAcceptThenBeginRecovery}
|
||||||
|
|
||||||
|
protected static TableMetadata intTbl, reverseTokenTbl;
|
||||||
|
protected static Node.Id nodeId;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUpClass()
|
||||||
|
{
|
||||||
|
CQLTester.setUpClass();
|
||||||
|
DatabaseDescriptor.setIncrementalBackupsEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init()
|
||||||
|
{
|
||||||
|
if (intTbl != null)
|
||||||
|
return;
|
||||||
|
createKeyspace("CREATE KEYSPACE test WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 2 }");
|
||||||
|
createTable("test", "CREATE TABLE test.tbl1 (pk int PRIMARY KEY, value int) WITH transactional_mode='full'");
|
||||||
|
intTbl = Schema.instance.getTableMetadata("test", "tbl1");
|
||||||
|
|
||||||
|
createTable("test", "CREATE TABLE test.tbl2 (pk vector<bigint, 2> PRIMARY KEY, value int) WITH transactional_mode='full'");
|
||||||
|
reverseTokenTbl = Schema.instance.getTableMetadata("test", "tbl2");
|
||||||
|
|
||||||
|
nodeId = AccordTopology.tcmIdToAccord(ClusterMetadata.current().myNodeId());
|
||||||
|
|
||||||
|
// tests may flush, which triggers compaction... since compaction is not simulated this adds a form of non-deterministic behavior
|
||||||
|
for (var store : Keyspace.open(ACCORD_KEYSPACE_NAME).getColumnFamilyStores())
|
||||||
|
store.disableAutoCompaction();
|
||||||
|
|
||||||
|
AccordService.unsafeSetNoop();
|
||||||
|
|
||||||
|
ServerTestUtils.markCMS();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void safeBlock(List<AsyncResult<?>> asyncs) throws InterruptedException, ExecutionException
|
||||||
|
{
|
||||||
|
int counter = 0;
|
||||||
|
for (var chain : asyncs)
|
||||||
|
{
|
||||||
|
Assertions.assertThat(chain.isDone())
|
||||||
|
.describedAs("The %dth async task is blocked!", counter++)
|
||||||
|
.isTrue();
|
||||||
|
AsyncChains.getBlocking(chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void safeBlock(List<AsyncResult<?>> asyncs, List<?> details) throws InterruptedException, ExecutionException
|
||||||
|
{
|
||||||
|
int counter = 0;
|
||||||
|
for (var chain : asyncs)
|
||||||
|
{
|
||||||
|
Assertions.assertThat(chain.isDone())
|
||||||
|
.describedAs("The %dth async task %s is blocked!", counter, details.get(counter++))
|
||||||
|
.isTrue();
|
||||||
|
AsyncChains.getBlocking(chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static TokenRange fullRange(TableId id)
|
||||||
|
{
|
||||||
|
return new TokenRange(AccordRoutingKey.SentinelKey.min(id), AccordRoutingKey.SentinelKey.max(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static TokenRange tokenRange(TableId id, long start, long end)
|
||||||
|
{
|
||||||
|
return new TokenRange(start == Long.MIN_VALUE ? AccordRoutingKey.SentinelKey.min(id) : tokenKey(id, start), tokenKey(id, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static AccordRoutingKey.TokenKey tokenKey(TableId id, long token)
|
||||||
|
{
|
||||||
|
return new AccordRoutingKey.TokenKey(id, new Murmur3Partitioner.LongToken(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Map<Key, List<TxnId>> keyConflicts(List<TxnId> list, Keys keys)
|
||||||
|
{
|
||||||
|
Map<Key, List<TxnId>> kc = Maps.newHashMapWithExpectedSize(keys.size());
|
||||||
|
for (Key key : keys)
|
||||||
|
{
|
||||||
|
if (list.isEmpty())
|
||||||
|
continue;
|
||||||
|
kc.put(key, list);
|
||||||
|
}
|
||||||
|
return kc;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Map<Range, List<TxnId>> rangeConflicts(List<TxnId> list, Ranges ranges)
|
||||||
|
{
|
||||||
|
Map<Range, List<TxnId>> kc = Maps.newHashMapWithExpectedSize(ranges.size());
|
||||||
|
for (Range range : ranges)
|
||||||
|
{
|
||||||
|
if (list.isEmpty())
|
||||||
|
continue;
|
||||||
|
kc.put(range, list);
|
||||||
|
}
|
||||||
|
return kc;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static TxnId assertDepsMessage(SimulatedAccordCommandStore instance,
|
||||||
|
DepsMessage messageType,
|
||||||
|
Txn txn, FullRoute<?> route,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts) throws ExecutionException, InterruptedException
|
||||||
|
{
|
||||||
|
return assertDepsMessage(instance, messageType, txn, route, keyConflicts, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static TxnId assertDepsMessage(SimulatedAccordCommandStore instance,
|
||||||
|
DepsMessage messageType,
|
||||||
|
Txn txn, FullRoute<?> route,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts,
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts) throws ExecutionException, InterruptedException
|
||||||
|
{
|
||||||
|
var pair = assertDepsMessageAsync(instance, messageType, txn, route, keyConflicts, rangeConflicts);
|
||||||
|
instance.processAll();
|
||||||
|
AsyncChains.getBlocking(pair.right);
|
||||||
|
|
||||||
|
return pair.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Pair<TxnId, AsyncResult<?>> assertDepsMessageAsync(SimulatedAccordCommandStore instance,
|
||||||
|
DepsMessage messageType,
|
||||||
|
Txn txn, FullRoute<?> route,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts)
|
||||||
|
{
|
||||||
|
return assertDepsMessageAsync(instance, messageType, txn, route, keyConflicts, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Pair<TxnId, AsyncResult<?>> assertDepsMessageAsync(SimulatedAccordCommandStore instance,
|
||||||
|
DepsMessage messageType,
|
||||||
|
Txn txn, FullRoute<?> route,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts,
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts)
|
||||||
|
{
|
||||||
|
switch (messageType)
|
||||||
|
{
|
||||||
|
case PreAccept:
|
||||||
|
return assertPreAcceptAsync(instance, txn, route, keyConflicts, rangeConflicts);
|
||||||
|
case BeginRecovery:
|
||||||
|
return assertBeginRecoveryAsync(instance, txn, route, keyConflicts, rangeConflicts);
|
||||||
|
case PreAcceptThenBeginRecovery:
|
||||||
|
return assertBeginRecoveryAfterPreAcceptAsync(instance, txn, route, keyConflicts, rangeConflicts);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown message type: " + messageType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Pair<TxnId, AsyncResult<?>> assertPreAcceptAsync(SimulatedAccordCommandStore instance,
|
||||||
|
Txn txn, FullRoute<?> route,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts,
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts)
|
||||||
|
{
|
||||||
|
Map<Key, List<TxnId>> cloneKeyConflicts = keyConflicts.entrySet().stream()
|
||||||
|
.filter(e -> !e.getValue().isEmpty())
|
||||||
|
.collect(Collectors.toMap(e -> e.getKey(), e -> new ArrayList(e.getValue())));
|
||||||
|
Map<Range, List<TxnId>> cloneRangeConflicts = rangeConflicts.entrySet().stream()
|
||||||
|
.filter(e -> !e.getValue().isEmpty())
|
||||||
|
.collect(Collectors.toMap(e -> e.getKey(), e -> new ArrayList(e.getValue())));
|
||||||
|
var pair = instance.enqueuePreAccept(txn, route);
|
||||||
|
return Pair.create(pair.left, pair.right.map(success -> {
|
||||||
|
assertDeps(success.txnId, success.deps, cloneKeyConflicts, cloneRangeConflicts);
|
||||||
|
return null;
|
||||||
|
}).beginAsResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Pair<TxnId, AsyncResult<?>> assertBeginRecoveryAsync(SimulatedAccordCommandStore instance,
|
||||||
|
Txn txn, FullRoute<?> route,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts,
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts)
|
||||||
|
{
|
||||||
|
Map<Key, List<TxnId>> cloneKeyConflicts = keyConflicts.entrySet().stream()
|
||||||
|
.filter(e -> !e.getValue().isEmpty())
|
||||||
|
.collect(Collectors.toMap(e -> e.getKey(), e -> new ArrayList(e.getValue())));
|
||||||
|
Map<Range, List<TxnId>> cloneRangeConflicts = rangeConflicts.entrySet().stream()
|
||||||
|
.filter(e -> !e.getValue().isEmpty())
|
||||||
|
.collect(Collectors.toMap(e -> e.getKey(), e -> new ArrayList(e.getValue())));
|
||||||
|
var pair = instance.enqueueBeginRecovery(txn, route);
|
||||||
|
return Pair.create(pair.left, pair.right.map(success -> {
|
||||||
|
Deps proposeDeps = LatestDeps.mergeProposal(Collections.singletonList(success), ok -> ok.deps);
|
||||||
|
assertDeps(success.txnId, proposeDeps, cloneKeyConflicts, cloneRangeConflicts);
|
||||||
|
return null;
|
||||||
|
}).beginAsResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Pair<TxnId, AsyncResult<?>> assertBeginRecoveryAfterPreAcceptAsync(SimulatedAccordCommandStore instance,
|
||||||
|
Txn txn, FullRoute<?> route,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts,
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts)
|
||||||
|
{
|
||||||
|
Map<Key, List<TxnId>> cloneKeyConflicts = keyConflicts.entrySet().stream()
|
||||||
|
.filter(e -> !e.getValue().isEmpty())
|
||||||
|
.collect(Collectors.toMap(e -> e.getKey(), e -> new ArrayList(e.getValue())));
|
||||||
|
Map<Range, List<TxnId>> cloneRangeConflicts = rangeConflicts.entrySet().stream()
|
||||||
|
.filter(e -> !e.getValue().isEmpty())
|
||||||
|
.collect(Collectors.toMap(e -> e.getKey(), e -> new ArrayList(e.getValue())));
|
||||||
|
|
||||||
|
TxnId txnId = instance.nextTxnId(txn.kind(), txn.keys().domain());
|
||||||
|
PreAccept preAccept = new PreAccept(nodeId, new Topologies.Single(SizeOfIntersectionSorter.SUPPLIER, instance.topology), txnId, txn, route);
|
||||||
|
|
||||||
|
var preAcceptAsync = instance.processAsync(preAccept, safe -> {
|
||||||
|
var reply = preAccept.apply(safe);
|
||||||
|
Assertions.assertThat(reply.isOk()).isTrue();
|
||||||
|
PreAccept.PreAcceptOk success = (PreAccept.PreAcceptOk) reply;
|
||||||
|
assertDeps(success.txnId, success.deps, cloneKeyConflicts, cloneRangeConflicts);
|
||||||
|
return success;
|
||||||
|
});
|
||||||
|
var delay = preAcceptAsync.flatMap(ignore -> AsyncChains.ofCallable(instance.unorderedScheduled, () -> {
|
||||||
|
Ballot ballot = Ballot.fromValues(instance.timeService.epoch(), instance.timeService.now(), nodeId);
|
||||||
|
return new BeginRecovery(nodeId, new Topologies.Single(SizeOfIntersectionSorter.SUPPLIER, instance.topology), txnId, txn, route, ballot);
|
||||||
|
}));
|
||||||
|
var recoverAsync = delay.flatMap(br -> instance.processAsync(br, safe -> {
|
||||||
|
var reply = br.apply(safe);
|
||||||
|
Assertions.assertThat(reply.isOk()).isTrue();
|
||||||
|
BeginRecovery.RecoverOk success = (BeginRecovery.RecoverOk) reply;
|
||||||
|
Deps proposeDeps = LatestDeps.mergeProposal(Collections.singletonList(success), ok -> ok.deps);
|
||||||
|
assertDeps(success.txnId, proposeDeps, cloneKeyConflicts, cloneRangeConflicts);
|
||||||
|
return success;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Pair.create(txnId, recoverAsync.beginAsResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void assertDeps(TxnId txnId, Deps deps,
|
||||||
|
Map<Key, List<TxnId>> keyConflicts,
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts)
|
||||||
|
{
|
||||||
|
if (rangeConflicts.isEmpty())
|
||||||
|
{
|
||||||
|
Assertions.assertThat(deps.rangeDeps.isEmpty()).describedAs("Txn %s rangeDeps was not empty; %s", txnId, deps.rangeDeps).isTrue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<Range> actualRanges = IntStream.range(0, deps.rangeDeps.rangeCount()).mapToObj(i -> deps.rangeDeps.range(i)).collect(Collectors.toList());
|
||||||
|
// Assertions.assertThat(deps.rangeDeps.rangeCount()).describedAs("Txn %s Expected ranges size; %s", txnId, deps.rangeDeps).isEqualTo(rangeConflicts.size());
|
||||||
|
Assertions.assertThat(Ranges.of(actualRanges.toArray(Range[]::new)))
|
||||||
|
.describedAs("Txn %s had different ranges than expected", txnId)
|
||||||
|
.isEqualTo(Ranges.of(rangeConflicts.keySet().toArray(Range[]::new)));
|
||||||
|
AssertionError errors = null;
|
||||||
|
for (int i = 0; i < rangeConflicts.size(); i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var range = deps.rangeDeps.range(i);
|
||||||
|
Assertions.assertThat(rangeConflicts).describedAs("Txn %s had an unexpected range", txnId).containsKey(range);
|
||||||
|
var conflict = deps.rangeDeps.txnIdsForRangeIndex(i);
|
||||||
|
List<TxnId> expectedConflict = rangeConflicts.get(range);
|
||||||
|
Assertions.assertThat(conflict).describedAs("Txn %s Expected range %s to have different conflicting txns", txnId, range).isEqualTo(expectedConflict);
|
||||||
|
}
|
||||||
|
catch (AssertionError e)
|
||||||
|
{
|
||||||
|
if (errors == null)
|
||||||
|
errors = e;
|
||||||
|
else
|
||||||
|
errors.addSuppressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors != null)
|
||||||
|
throw errors;
|
||||||
|
}
|
||||||
|
if (keyConflicts.isEmpty())
|
||||||
|
{
|
||||||
|
Assertions.assertThat(deps.keyDeps.isEmpty()).describedAs("Txn %s keyDeps was not empty", txnId).isTrue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assertions.assertThat(deps.keyDeps.keys()).describedAs("Txn %s Keys", txnId).isEqualTo(Keys.of(keyConflicts.keySet()));
|
||||||
|
for (var key : keyConflicts.keySet())
|
||||||
|
Assertions.assertThat(deps.keyDeps.txnIds(key)).describedAs("Txn %s for key %s", txnId, key).isEqualTo(keyConflicts.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
/*
|
||||||
|
* 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.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import accord.api.Key;
|
||||||
|
import accord.primitives.FullKeyRoute;
|
||||||
|
import accord.primitives.FullRangeRoute;
|
||||||
|
import accord.primitives.FullRoute;
|
||||||
|
import accord.primitives.Keys;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.Txn;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.utils.async.AsyncResult;
|
||||||
|
import org.apache.cassandra.db.marshal.Int32Type;
|
||||||
|
import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
|
||||||
|
import org.apache.cassandra.schema.TableMetadata;
|
||||||
|
import org.apache.cassandra.service.accord.api.PartitionKey;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
|
||||||
|
|
||||||
|
public class SimulatedDepsTest extends SimulatedAccordCommandStoreTestBase
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void keyConflicts()
|
||||||
|
{
|
||||||
|
TableMetadata tbl = intTbl;
|
||||||
|
int numSamples = 100;
|
||||||
|
|
||||||
|
qt().withExamples(10).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
int key = rs.nextInt();
|
||||||
|
PartitionKey pk = new PartitionKey(tbl.id, tbl.partitioner.decorateKey(Int32Type.instance.decompose(key)));
|
||||||
|
Keys keys = Keys.of(pk);
|
||||||
|
FullKeyRoute route = keys.toRoute(pk.toUnseekable());
|
||||||
|
Txn txn = createTxn(wrapInTxn("INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)"), Arrays.asList(key, 42));
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
List<TxnId> conflicts = new ArrayList<>(numSamples);
|
||||||
|
boolean concurrent = rs.nextBoolean();
|
||||||
|
List<AsyncResult<?>> asyncs = !concurrent ? null : new ArrayList<>(numSamples);
|
||||||
|
for (int i = 0; i < numSamples; i++)
|
||||||
|
{
|
||||||
|
instance.maybeCacheEvict(keys, Ranges.EMPTY);
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
var pair = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), txn, route, keyConflicts(conflicts, keys));
|
||||||
|
conflicts.add(pair.left);
|
||||||
|
asyncs.add(pair.right);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
conflicts.add(assertDepsMessage(instance, rs.pick(DepsMessage.values()), txn, route, keyConflicts(conflicts, keys)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
instance.processAll();
|
||||||
|
safeBlock(asyncs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentRangePartialKeyMatch()
|
||||||
|
{
|
||||||
|
var tbl = reverseTokenTbl;
|
||||||
|
int numSamples = 250;
|
||||||
|
int numConflictKeyTxns = 10;
|
||||||
|
|
||||||
|
qt().withExamples(10).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
long token = rs.nextLong(Long.MIN_VALUE + 1, Long.MAX_VALUE);
|
||||||
|
Ranges partialRange = Ranges.of(tokenRange(tbl.id, token - 1, token));
|
||||||
|
long outOfRangeToken = token - 10;
|
||||||
|
if (outOfRangeToken == Long.MIN_VALUE) // if this wraps around that is fine, just can't be min
|
||||||
|
outOfRangeToken++;
|
||||||
|
Key key = new PartitionKey(tbl.id, tbl.partitioner.decorateKey(LongToken.keyForToken(token)));
|
||||||
|
Key outOfRangeKey = new PartitionKey(tbl.id, tbl.partitioner.decorateKey(LongToken.keyForToken(outOfRangeToken)));
|
||||||
|
Txn keyTxn = createTxn(wrapInTxn("INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)",
|
||||||
|
"INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)"),
|
||||||
|
Arrays.asList(LongToken.keyForToken(token), 42,
|
||||||
|
LongToken.keyForToken(outOfRangeToken), 42));
|
||||||
|
Keys keys = (Keys) keyTxn.keys();
|
||||||
|
FullRoute<?> keyRoute = keys.toRoute(keys.get(0).toUnseekable());
|
||||||
|
|
||||||
|
Txn conflictingKeyTxn = createTxn(wrapInTxn("INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)"),
|
||||||
|
Arrays.asList(LongToken.keyForToken(outOfRangeToken), 42));
|
||||||
|
Keys conflictingKeys = (Keys) conflictingKeyTxn.keys();
|
||||||
|
FullRoute<?> conflictingRoute = conflictingKeys.toRoute(conflictingKeys.get(0).toUnseekable());
|
||||||
|
|
||||||
|
FullRangeRoute rangeRoute = partialRange.toRoute(keys.get(0).toUnseekable());
|
||||||
|
Txn rangeTxn = createTxn(Txn.Kind.ExclusiveSyncPoint, partialRange);
|
||||||
|
|
||||||
|
List<TxnId> keyConflicts = new ArrayList<>(numSamples);
|
||||||
|
List<TxnId> outOfRangeKeyConflicts = new ArrayList<>(numSamples);
|
||||||
|
List<TxnId> rangeConflicts = new ArrayList<>(numSamples);
|
||||||
|
List<AsyncResult<?>> asyncs = new ArrayList<>(numSamples * 2 + numSamples * numConflictKeyTxns);
|
||||||
|
List<TxnId> asyncIds = new ArrayList<>(numSamples * 2 + numSamples * numConflictKeyTxns);
|
||||||
|
for (int i = 0; i < numSamples; i++)
|
||||||
|
{
|
||||||
|
instance.maybeCacheEvict((Keys) keyTxn.keys(), partialRange);
|
||||||
|
for (int j = 0; j < numConflictKeyTxns; j++)
|
||||||
|
{
|
||||||
|
var p = instance.enqueuePreAccept(conflictingKeyTxn, conflictingRoute);
|
||||||
|
outOfRangeKeyConflicts.add(p.left);
|
||||||
|
asyncs.add(p.right);
|
||||||
|
asyncIds.add(p.left);
|
||||||
|
}
|
||||||
|
|
||||||
|
var k = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, Map.of(key, keyConflicts, outOfRangeKey, outOfRangeKeyConflicts), Collections.emptyMap());
|
||||||
|
keyConflicts.add(k.left);
|
||||||
|
outOfRangeKeyConflicts.add(k.left);
|
||||||
|
asyncs.add(k.right);
|
||||||
|
asyncIds.add(k.left);
|
||||||
|
|
||||||
|
var r = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, Map.of(key, keyConflicts), rangeConflicts(rangeConflicts, partialRange));
|
||||||
|
rangeConflicts.add(r.left);
|
||||||
|
asyncs.add(r.right);
|
||||||
|
asyncIds.add(r.left);
|
||||||
|
}
|
||||||
|
instance.processAll();
|
||||||
|
safeBlock(asyncs, asyncIds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleRangeConflicts()
|
||||||
|
{
|
||||||
|
var tbl = reverseTokenTbl;
|
||||||
|
Ranges wholeRange = Ranges.of(fullRange(tbl.id));
|
||||||
|
int numSamples = 100;
|
||||||
|
|
||||||
|
qt().withExamples(10).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
long token = rs.nextLong(Long.MIN_VALUE + 1, Long.MAX_VALUE);
|
||||||
|
ByteBuffer key = LongToken.keyForToken(token);
|
||||||
|
PartitionKey pk = new PartitionKey(tbl.id, tbl.partitioner.decorateKey(key));
|
||||||
|
Keys keys = Keys.of(pk);
|
||||||
|
FullKeyRoute keyRoute = keys.toRoute(pk.toUnseekable());
|
||||||
|
Txn keyTxn = createTxn(wrapInTxn("INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)"), Arrays.asList(key, 42));
|
||||||
|
|
||||||
|
Ranges partialRange = Ranges.of(tokenRange(tbl.id, token - 1, token));
|
||||||
|
boolean useWholeRange = rs.nextBoolean();
|
||||||
|
Ranges ranges = useWholeRange ? wholeRange : partialRange;
|
||||||
|
FullRangeRoute rangeRoute = ranges.toRoute(pk.toUnseekable());
|
||||||
|
Txn rangeTxn = createTxn(Txn.Kind.ExclusiveSyncPoint, ranges);
|
||||||
|
|
||||||
|
List<TxnId> keyConflicts = new ArrayList<>(numSamples);
|
||||||
|
List<TxnId> rangeConflicts = new ArrayList<>(numSamples);
|
||||||
|
boolean concurrent = rs.nextBoolean();
|
||||||
|
List<AsyncResult<?>> asyncs = !concurrent ? null : new ArrayList<>(numSamples * 2);
|
||||||
|
for (int i = 0; i < numSamples; i++)
|
||||||
|
{
|
||||||
|
instance.maybeCacheEvict(keys, ranges);
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
var k = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, keyConflicts(keyConflicts, keys));
|
||||||
|
keyConflicts.add(k.left);
|
||||||
|
asyncs.add(k.right);
|
||||||
|
var r = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, keyConflicts(keyConflicts, keys), rangeConflicts(rangeConflicts, ranges));
|
||||||
|
rangeConflicts.add(r.left);
|
||||||
|
asyncs.add(r.right);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
keyConflicts.add(assertDepsMessage(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, keyConflicts(keyConflicts, keys)));
|
||||||
|
rangeConflicts.add(assertDepsMessage(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, keyConflicts(keyConflicts, keys), rangeConflicts(rangeConflicts, ranges)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
instance.processAll();
|
||||||
|
safeBlock(asyncs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void expandingRangeConflicts()
|
||||||
|
{
|
||||||
|
var tbl = reverseTokenTbl;
|
||||||
|
int numSamples = 100;
|
||||||
|
|
||||||
|
qt().withSeed(6484101342775432632L).withExamples(10).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
long token = rs.nextLong(Long.MIN_VALUE + numSamples + 1, Long.MAX_VALUE - numSamples);
|
||||||
|
ByteBuffer key = LongToken.keyForToken(token);
|
||||||
|
PartitionKey pk = new PartitionKey(tbl.id, tbl.partitioner.decorateKey(key));
|
||||||
|
Keys keys = Keys.of(pk);
|
||||||
|
FullKeyRoute keyRoute = keys.toRoute(pk.toUnseekable());
|
||||||
|
Txn keyTxn = createTxn(wrapInTxn("INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)"), Arrays.asList(key, 42));
|
||||||
|
|
||||||
|
List<TxnId> keyConflicts = new ArrayList<>(numSamples);
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts = new HashMap<>();
|
||||||
|
boolean concurrent = rs.nextBoolean();
|
||||||
|
List<AsyncResult<?>> asyncs = !concurrent ? null : new ArrayList<>(numSamples);
|
||||||
|
List<TxnId> info = !concurrent ? null : new ArrayList<>(numSamples);
|
||||||
|
for (int i = 0; i < numSamples; i++)
|
||||||
|
{
|
||||||
|
Ranges partialRange = Ranges.of(tokenRange(tbl.id, token - i - 1, token + i));
|
||||||
|
FullRangeRoute rangeRoute = partialRange.toRoute(pk.toUnseekable());
|
||||||
|
Txn rangeTxn = createTxn(Txn.Kind.ExclusiveSyncPoint, partialRange);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
instance.maybeCacheEvict(keys, partialRange);
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
var pair = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, keyConflicts(keyConflicts, keys));
|
||||||
|
info.add(pair.left);
|
||||||
|
keyConflicts.add(pair.left);
|
||||||
|
asyncs.add(pair.right);
|
||||||
|
|
||||||
|
pair = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, keyConflicts(keyConflicts, keys), rangeConflicts);
|
||||||
|
info.add(pair.left);
|
||||||
|
rangeConflicts.put(partialRange.get(0), Collections.singletonList(pair.left));
|
||||||
|
asyncs.add(pair.right);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
keyConflicts.add(assertDepsMessage(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, keyConflicts(keyConflicts, keys)));
|
||||||
|
rangeConflicts.put(partialRange.get(0), Collections.singletonList(assertDepsMessage(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, keyConflicts(keyConflicts, keys), rangeConflicts)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Throwable t)
|
||||||
|
{
|
||||||
|
AssertionError error = new AssertionError("Unexpected error: i=" + i + ", token=" + token + ", range=" + partialRange.get(0));
|
||||||
|
t.addSuppressed(error);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
instance.processAll();
|
||||||
|
safeBlock(asyncs, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overlappingRangeConflicts()
|
||||||
|
{
|
||||||
|
var tbl = reverseTokenTbl;
|
||||||
|
int numSamples = 100;
|
||||||
|
|
||||||
|
qt().withExamples(10).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
long token = rs.nextLong(Long.MIN_VALUE + numSamples + 1, Long.MAX_VALUE - numSamples);
|
||||||
|
ByteBuffer key = LongToken.keyForToken(token);
|
||||||
|
PartitionKey pk = new PartitionKey(tbl.id, tbl.partitioner.decorateKey(key));
|
||||||
|
Keys keys = Keys.of(pk);
|
||||||
|
FullKeyRoute keyRoute = keys.toRoute(pk.toUnseekable());
|
||||||
|
Txn keyTxn = createTxn(wrapInTxn("INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)"), Arrays.asList(key, 42));
|
||||||
|
|
||||||
|
Range left = tokenRange(tbl.id, token - 10, token + 5);
|
||||||
|
Range right = tokenRange(tbl.id, token - 5, token + 10);
|
||||||
|
|
||||||
|
List<TxnId> keyConflicts = new ArrayList<>(numSamples);
|
||||||
|
Map<Range, List<TxnId>> rangeConflicts = new HashMap<>();
|
||||||
|
rangeConflicts.put(left, new ArrayList<>());
|
||||||
|
rangeConflicts.put(right, new ArrayList<>());
|
||||||
|
for (int i = 0; i < numSamples; i++)
|
||||||
|
{
|
||||||
|
Ranges partialRange = Ranges.of(rs.nextBoolean() ? left : right);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
instance.maybeCacheEvict(keys, partialRange);
|
||||||
|
keyConflicts.add(assertDepsMessage(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, keyConflicts(keyConflicts, keys)));
|
||||||
|
|
||||||
|
FullRangeRoute rangeRoute = partialRange.toRoute(pk.toUnseekable());
|
||||||
|
Txn rangeTxn = createTxn(Txn.Kind.ExclusiveSyncPoint, partialRange);
|
||||||
|
rangeConflicts.get(partialRange.get(0)).add(assertDepsMessage(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, keyConflicts(keyConflicts, keys), rangeConflicts));
|
||||||
|
}
|
||||||
|
catch (Throwable t)
|
||||||
|
{
|
||||||
|
AssertionError error = new AssertionError("Unexpected error: i=" + i + ", token=" + token + ", range=" + partialRange.get(0));
|
||||||
|
t.addSuppressed(error);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.cassandra.service.accord;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import accord.api.Key;
|
||||||
|
import accord.api.RoutingKey;
|
||||||
|
import accord.primitives.FullRangeRoute;
|
||||||
|
import accord.primitives.FullRoute;
|
||||||
|
import accord.primitives.Keys;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.Routable.Domain;
|
||||||
|
import accord.primitives.Txn;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.utils.Gen;
|
||||||
|
import accord.utils.Gens;
|
||||||
|
import accord.utils.async.AsyncResult;
|
||||||
|
import org.apache.cassandra.service.accord.api.PartitionKey;
|
||||||
|
import org.apache.cassandra.utils.RTree;
|
||||||
|
import org.apache.cassandra.utils.RangeTree;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
import static org.apache.cassandra.dht.Murmur3Partitioner.LongToken.keyForToken;
|
||||||
|
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
|
||||||
|
|
||||||
|
public class SimulatedMultiKeyAndRangeTest extends SimulatedAccordCommandStoreTestBase
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void test()
|
||||||
|
{
|
||||||
|
var tbl = reverseTokenTbl;
|
||||||
|
int numSamples = 300;
|
||||||
|
long minToken = 0;
|
||||||
|
long maxToken = 100;
|
||||||
|
Gen<Gen.LongGen> tokenDistribution = Gens.mixedDistribution(minToken, maxToken + 1);
|
||||||
|
Gen<Gen.IntGen> keyDistribution = Gens.mixedDistribution(1, 5);
|
||||||
|
Gen<Gen.IntGen> rangeDistribution = Gens.mixedDistribution(1, 5);
|
||||||
|
Gen<Gen<Domain>> domainDistribution = Gens.mixedDistribution(Domain.values());
|
||||||
|
Gen<Gen<DepsMessage>> msgDistribution = Gens.mixedDistribution(DepsMessage.values());
|
||||||
|
|
||||||
|
qt().withExamples(100).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
Gen.LongGen tokenGen = tokenDistribution.next(rs);
|
||||||
|
Gen<Domain> domainGen = domainDistribution.next(rs);
|
||||||
|
Gen<DepsMessage> msgGen = msgDistribution.next(rs);
|
||||||
|
Map<Key, List<TxnId>> keyConflicts = new HashMap<>();
|
||||||
|
RangeTree<RoutingKey, Range, TxnId> rangeConflicts = RTree.create(RangeTreeRangeAccessor.instance);
|
||||||
|
List<AsyncResult<?>> asyncs = new ArrayList<>(numSamples);
|
||||||
|
|
||||||
|
Gen.IntGen keyCountGen = keyDistribution.next(rs);
|
||||||
|
Gen.IntGen rangeCountGen = rangeDistribution.next(rs);
|
||||||
|
|
||||||
|
for (int i = 0; i < numSamples; i++)
|
||||||
|
{
|
||||||
|
switch (domainGen.next(rs))
|
||||||
|
{
|
||||||
|
case Key:
|
||||||
|
{
|
||||||
|
int numKeys = keyCountGen.nextInt(rs);
|
||||||
|
TreeSet<Key> set = new TreeSet<>();
|
||||||
|
while (set.size() != numKeys)
|
||||||
|
set.add(new PartitionKey(tbl.id, tbl.partitioner.decorateKey(keyForToken(tokenGen.nextLong(rs)))));
|
||||||
|
Keys keys = Keys.of(set);
|
||||||
|
List<String> inserts = IntStream.range(0, numKeys).mapToObj(ignore -> "INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)").collect(Collectors.toList());
|
||||||
|
List<Object> binds = new ArrayList<>(numKeys * 2);
|
||||||
|
keys.forEach(k -> {
|
||||||
|
binds.add(((PartitionKey) k.asKey()).partitionKey().getKey());
|
||||||
|
binds.add(42);
|
||||||
|
});
|
||||||
|
Txn txn = createTxn(wrapInTxn(inserts), binds);
|
||||||
|
FullRoute<?> route = keys.toRoute(keys.get(0).toUnseekable());
|
||||||
|
|
||||||
|
Map<Key, List<TxnId>> expectedConflicts = new HashMap<>();
|
||||||
|
keys.forEach(k -> expectedConflicts.put(k, keyConflicts.computeIfAbsent(k, ignore -> new ArrayList<>())));
|
||||||
|
|
||||||
|
var p = assertDepsMessageAsync(instance, msgGen.next(rs), txn, route, expectedConflicts, Collections.emptyMap());
|
||||||
|
keys.forEach(k -> keyConflicts.get(k).add(p.left));
|
||||||
|
asyncs.add(p.right);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Range:
|
||||||
|
{
|
||||||
|
int numRanges = rangeCountGen.nextInt(rs);
|
||||||
|
Set<Range> set = new HashSet<>();
|
||||||
|
while (set.size() != numRanges)
|
||||||
|
{
|
||||||
|
long token = tokenGen.nextLong(rs);
|
||||||
|
int offset = rs.nextInt(1, 10);
|
||||||
|
long start, end;
|
||||||
|
if (token + offset > maxToken)
|
||||||
|
{
|
||||||
|
end = token;
|
||||||
|
start = end - offset;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
start = token;
|
||||||
|
end = start + offset;
|
||||||
|
}
|
||||||
|
set.add(tokenRange(tbl.id, start, end));
|
||||||
|
}
|
||||||
|
// The property ranges.size() == numRanges is not true as this logic will sort + deoverlap
|
||||||
|
// so if the ranges were overlapped, we could have more or less than numRanges
|
||||||
|
Ranges ranges = Ranges.of(set.toArray(Range[]::new));
|
||||||
|
FullRangeRoute route = ranges.toRoute(ranges.get(0).end());
|
||||||
|
Txn txn = createTxn(Txn.Kind.ExclusiveSyncPoint, ranges);
|
||||||
|
|
||||||
|
Map<Key, List<TxnId>> expectedKeyConflicts = keyConflicts.entrySet().stream()
|
||||||
|
.filter(e -> ranges.contains(e.getKey()))
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
Map<Range, List<TxnId>> expectedRangeConflicts = new HashMap<>();
|
||||||
|
ranges.forEach(r ->
|
||||||
|
rangeConflicts.search(r, e ->
|
||||||
|
expectedRangeConflicts.computeIfAbsent(e.getKey(), ignore -> new ArrayList<>()).add(e.getValue())));
|
||||||
|
// need to dedup/sort txns
|
||||||
|
expectedRangeConflicts.values().forEach(l -> {
|
||||||
|
var sortedDedup = new ArrayList<>(new TreeSet<>(l));
|
||||||
|
l.clear();
|
||||||
|
l.addAll(sortedDedup);
|
||||||
|
});
|
||||||
|
var p = assertDepsMessageAsync(instance, msgGen.next(rs), txn, route, expectedKeyConflicts, expectedRangeConflicts);
|
||||||
|
asyncs.add(p.right);
|
||||||
|
ranges.forEach(r -> rangeConflicts.add(r, p.left));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instance.processAll();
|
||||||
|
safeBlock(asyncs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* 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.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import accord.api.Key;
|
||||||
|
import accord.primitives.FullRangeRoute;
|
||||||
|
import accord.primitives.FullRoute;
|
||||||
|
import accord.primitives.Keys;
|
||||||
|
import accord.primitives.Ranges;
|
||||||
|
import accord.primitives.Txn;
|
||||||
|
import accord.primitives.TxnId;
|
||||||
|
import accord.utils.async.AsyncResult;
|
||||||
|
import org.apache.cassandra.service.accord.api.PartitionKey;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
import static org.apache.cassandra.dht.Murmur3Partitioner.LongToken.keyForToken;
|
||||||
|
import static org.apache.cassandra.service.accord.AccordTestUtils.createTxn;
|
||||||
|
|
||||||
|
public class SimulatedRandomKeysWithRangeConflictTest extends SimulatedAccordCommandStoreTestBase
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void keysAllOverConflictingWithRange()
|
||||||
|
{
|
||||||
|
var tbl = reverseTokenTbl;
|
||||||
|
Ranges wholeRange = Ranges.of(fullRange(tbl.id));
|
||||||
|
FullRangeRoute rangeRoute = wholeRange.toRoute(wholeRange.get(0).end());
|
||||||
|
Txn rangeTxn = createTxn(Txn.Kind.ExclusiveSyncPoint, wholeRange);
|
||||||
|
int numSamples = 300;
|
||||||
|
|
||||||
|
qt().withExamples(10).check(rs -> {
|
||||||
|
AccordKeyspace.unsafeClear();
|
||||||
|
try (var instance = new SimulatedAccordCommandStore(rs))
|
||||||
|
{
|
||||||
|
Map<Key, List<TxnId>> keyConflicts = new HashMap<>();
|
||||||
|
List<TxnId> rangeConflicts = new ArrayList<>(numSamples);
|
||||||
|
boolean concurrent = rs.nextBoolean();
|
||||||
|
List<AsyncResult<?>> asyncs = !concurrent ? null : new ArrayList<>(numSamples * 2);
|
||||||
|
for (int i = 0; i < numSamples; i++)
|
||||||
|
{
|
||||||
|
long token = rs.nextLong(Long.MIN_VALUE + 1, Long.MAX_VALUE);
|
||||||
|
Key key = new PartitionKey(tbl.id, tbl.partitioner.decorateKey(keyForToken(token)));
|
||||||
|
Txn keyTxn = createTxn(wrapInTxn("INSERT INTO " + tbl + "(pk, value) VALUES (?, ?)"),
|
||||||
|
Arrays.asList(keyForToken(token), 42));
|
||||||
|
Keys keys = (Keys) keyTxn.keys();
|
||||||
|
FullRoute<?> keyRoute = keys.toRoute(keys.get(0).toUnseekable());
|
||||||
|
|
||||||
|
instance.maybeCacheEvict((Keys) keyTxn.keys(), wholeRange);
|
||||||
|
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
var k = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, Map.of(key, keyConflicts.computeIfAbsent(key, ignore -> new ArrayList<>())), Collections.emptyMap());
|
||||||
|
keyConflicts.get(key).add(k.left);
|
||||||
|
asyncs.add(k.right);
|
||||||
|
|
||||||
|
var r = assertDepsMessageAsync(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, keyConflicts, rangeConflicts(rangeConflicts, wholeRange));
|
||||||
|
rangeConflicts.add(r.left);
|
||||||
|
asyncs.add(r.right);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var k = assertDepsMessage(instance, rs.pick(DepsMessage.values()), keyTxn, keyRoute, Map.of(key, keyConflicts.computeIfAbsent(key, ignore -> new ArrayList<>())), Collections.emptyMap());
|
||||||
|
keyConflicts.get(key).add(k);
|
||||||
|
rangeConflicts.add(assertDepsMessage(instance, rs.pick(DepsMessage.values()), rangeTxn, rangeRoute, keyConflicts, rangeConflicts(rangeConflicts, wholeRange)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (concurrent)
|
||||||
|
{
|
||||||
|
instance.processAll();
|
||||||
|
safeBlock(asyncs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,8 +26,12 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import accord.utils.DefaultRandom;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
import org.apache.cassandra.concurrent.SimulatedExecutorFactory;
|
||||||
|
import org.apache.cassandra.concurrent.Stage;
|
||||||
|
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
|
@ -101,6 +105,11 @@ public class AsyncOperationTest
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AsyncOperationTest.class);
|
private static final Logger logger = LoggerFactory.getLogger(AsyncOperationTest.class);
|
||||||
private static final AtomicLong clock = new AtomicLong(0);
|
private static final AtomicLong clock = new AtomicLong(0);
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
CassandraRelevantProperties.TEST_ACCORD_STORE_THREAD_CHECKS_ENABLED.setBoolean(false);
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void beforeClass() throws Throwable
|
public static void beforeClass() throws Throwable
|
||||||
{
|
{
|
||||||
|
|
@ -314,7 +323,8 @@ public class AsyncOperationTest
|
||||||
@Test
|
@Test
|
||||||
public void testFutureCleanup() throws Throwable
|
public void testFutureCleanup() throws Throwable
|
||||||
{
|
{
|
||||||
AccordCommandStore commandStore = createAccordCommandStore(clock::incrementAndGet, "ks", "tbl");
|
SimulatedExecutorFactory factory = new SimulatedExecutorFactory(accord.utilsfork.RandomSource.wrap(new DefaultRandom(42)), 42);
|
||||||
|
AccordCommandStore commandStore = createAccordCommandStore(clock::incrementAndGet, "ks", "tbl", factory.scheduled("ignored"), Stage.MUTATION.executor());
|
||||||
|
|
||||||
TxnId txnId = txnId(1, clock.incrementAndGet(), 1);
|
TxnId txnId = txnId(1, clock.incrementAndGet(), 1);
|
||||||
|
|
||||||
|
|
@ -342,12 +352,15 @@ public class AsyncOperationTest
|
||||||
{
|
{
|
||||||
case SETUP:
|
case SETUP:
|
||||||
assertFutureState(cache(), txnId, false, false, false);
|
assertFutureState(cache(), txnId, false, false, false);
|
||||||
|
factory.processAll();
|
||||||
break;
|
break;
|
||||||
case FINISHED:
|
case FINISHED:
|
||||||
assertFutureState(cache(), txnId, true, false, false);
|
assertFutureState(cache(), txnId, true, false, false);
|
||||||
|
factory.processAll();
|
||||||
break;
|
break;
|
||||||
case LOADING:
|
case LOADING:
|
||||||
assertFutureState(cache(), txnId, true, true, false);
|
assertFutureState(cache(), txnId, true, true, false);
|
||||||
|
factory.processAll();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
super.state(state);
|
super.state(state);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.cassandra.service.accord.serializers;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
|
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
|
||||||
|
import org.apache.cassandra.dht.ByteOrderedPartitioner;
|
||||||
|
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||||
|
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
|
||||||
|
import org.apache.cassandra.service.accord.serializers.AccordRoutingKeyByteSource.FixedLength;
|
||||||
|
import org.apache.cassandra.utils.AccordGenerators;
|
||||||
|
import org.apache.cassandra.utils.ByteArrayUtil;
|
||||||
|
import org.apache.cassandra.utils.CassandraGenerators;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||||
|
import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
import static org.apache.cassandra.utils.AccordGenerators.fromQT;
|
||||||
|
import static org.apache.cassandra.utils.CassandraGenerators.token;
|
||||||
|
|
||||||
|
public class AccordRoutingKeyByteSourceTest
|
||||||
|
{
|
||||||
|
static
|
||||||
|
{
|
||||||
|
DatabaseDescriptor.clientInitialization();
|
||||||
|
// AccordRoutingKey$TokenKey reaches into DD to get partitioner, so need to set that up...
|
||||||
|
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tokenSerde()
|
||||||
|
{
|
||||||
|
qt().forAll(fromQT(token())).check(token -> {
|
||||||
|
var serializer = AccordRoutingKeyByteSource.create(token.getPartitioner());
|
||||||
|
byte[] min = ByteSourceInverse.readBytes(serializer.minAsComparableBytes());
|
||||||
|
byte[] max = ByteSourceInverse.readBytes(serializer.maxAsComparableBytes());
|
||||||
|
|
||||||
|
var bytes = serializer.serialize(token);
|
||||||
|
if (serializer instanceof FixedLength)
|
||||||
|
{
|
||||||
|
FixedLength fl = (FixedLength) serializer;
|
||||||
|
Assertions.assertThat(bytes)
|
||||||
|
.hasSize(fl.valueSize())
|
||||||
|
.hasSize(min.length)
|
||||||
|
.hasSize(max.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assertions.assertThat(ByteArrayUtil.compareUnsigned(min, 0, bytes, 0, bytes.length)).isLessThan(0);
|
||||||
|
Assertions.assertThat(ByteArrayUtil.compareUnsigned(max, 0, bytes, 0, bytes.length)).isGreaterThan(0);
|
||||||
|
|
||||||
|
var read = serializer.tokenFromComparableBytes(ByteArrayAccessor.instance, bytes);
|
||||||
|
Assertions.assertThat(read).isEqualTo(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accordRoutingKeySerde()
|
||||||
|
{
|
||||||
|
qt().forAll(AccordGenerators.routingKeyGen(fromQT(CassandraGenerators.TABLE_ID_GEN), fromQT(token()))).check(key -> {
|
||||||
|
AccordRoutingKeyByteSource.Serializer serializer = key.kindOfRoutingKey() == AccordRoutingKey.RoutingKeyKind.SENTINEL ?
|
||||||
|
// doesn't really matter...
|
||||||
|
new AccordRoutingKeyByteSource.VariableLength(ByteOrderedPartitioner.instance, ByteComparable.Version.OSS50)
|
||||||
|
: AccordRoutingKeyByteSource.create(key.asTokenKey().token().getPartitioner());
|
||||||
|
|
||||||
|
var read = serializer.fromComparableBytes(ByteArrayAccessor.instance, serializer.serialize(key));
|
||||||
|
Assertions.assertThat(read).isEqualTo(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,568 @@
|
||||||
|
/*
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.LongUnaryOperator;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import accord.impl.IntKey;
|
||||||
|
import accord.impl.IntKey.Routing;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.utils.Gen;
|
||||||
|
import accord.utils.Gens;
|
||||||
|
import accord.utils.RandomSource;
|
||||||
|
import accord.utils.SearchableRangeList;
|
||||||
|
import org.agrona.collections.IntArrayList;
|
||||||
|
import org.agrona.collections.LongArrayList;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.qt;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class RangeTreeTest
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RangeTreeTest.class);
|
||||||
|
private static final Comparator<Routing> COMPARATOR = Comparator.naturalOrder();
|
||||||
|
private static final RangeTree.Accessor<Routing, Range> END_INCLUSIVE = new RangeTree.Accessor<>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Routing start(Range range)
|
||||||
|
{
|
||||||
|
return (Routing) range.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Routing end(Range range)
|
||||||
|
{
|
||||||
|
return (Routing) range.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(Range range, Routing routing)
|
||||||
|
{
|
||||||
|
return range.contains(routing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(Routing start, Routing end, Routing routing)
|
||||||
|
{
|
||||||
|
if (routing.compareTo(start) <= 0)
|
||||||
|
return false;
|
||||||
|
if (routing.compareTo(end) > 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean intersects(Range range, Routing start, Routing end)
|
||||||
|
{
|
||||||
|
return range.compareIntersecting(IntKey.range(start, end)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean intersects(Range left, Range right)
|
||||||
|
{
|
||||||
|
return left.compareIntersecting(right) == 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private static final RangeTree.Accessor<Routing, Range> ALL_INCLUSIVE = new RangeTree.Accessor<>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Routing start(Range range)
|
||||||
|
{
|
||||||
|
return (Routing) range.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Routing end(Range range)
|
||||||
|
{
|
||||||
|
return (Routing) range.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(Range range, Routing routing)
|
||||||
|
{
|
||||||
|
return range.contains(routing) || range.start().equals(routing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(Routing start, Routing end, Routing routing)
|
||||||
|
{
|
||||||
|
if (routing.compareTo(start) < 0)
|
||||||
|
return false;
|
||||||
|
if (routing.compareTo(end) > 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean intersects(Range range, Routing start, Routing end)
|
||||||
|
{
|
||||||
|
return range.compareIntersecting(IntKey.range(start, end)) == 0 || range.end().equals(start) || range.start().equals(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean intersects(Range left, Range right)
|
||||||
|
{
|
||||||
|
return left.compareIntersecting(right) == 0 || left.end().equals(right.start()) || left.start().equals(right.end());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final Gen.IntGen SMALL_INT_GEN = rs -> rs.nextInt(0, 10);
|
||||||
|
private static final int MIN_TOKEN = 0, MAX_TOKEN = 1 << 16;
|
||||||
|
private static final int TOKEN_RANGE_SIZE = MAX_TOKEN - MIN_TOKEN + 1;
|
||||||
|
private static final Gen<Gen.IntGen> TOKEN_DISTRIBUTION = Gens.mixedDistribution(MIN_TOKEN, MAX_TOKEN + 1);
|
||||||
|
private static final Gen<Gen.IntGen> RANGE_SIZE_DISTRIBUTION = Gens.mixedDistribution(10, (int) (TOKEN_RANGE_SIZE * .01));
|
||||||
|
|
||||||
|
// Used to test different worse case patterns and see how the tree performs.
|
||||||
|
private enum Pattern
|
||||||
|
{
|
||||||
|
RANDOM, // tends to have high selectivity: matches 50-100% of the tree in testing
|
||||||
|
NO_OVERLP, // tests to have low selectivity; matches 1-2 elements in testing
|
||||||
|
SMALL_RANGES // lower selectivity than RANDOM but still matches ~30% of the tree in testing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Having different models makes sure that the tree is flexiable enough and can be used with the semantics the user
|
||||||
|
// needs (with regard to inclusivity). It also adds more confidence that the search logic is correct as different
|
||||||
|
// algorithems help validate this.
|
||||||
|
private enum ModelType {List, IntervalTree, SearchableRangeList}
|
||||||
|
private final Pattern pattern;
|
||||||
|
private final ModelType modelType;
|
||||||
|
|
||||||
|
public RangeTreeTest(Pattern pattern, ModelType modelType)
|
||||||
|
{
|
||||||
|
this.pattern = pattern;
|
||||||
|
this.modelType = modelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parameterized.Parameters(name = "{0}, {1}")
|
||||||
|
public static Collection<Object[]> data() {
|
||||||
|
return Stream.of(Pattern.values())
|
||||||
|
.flatMap(p ->
|
||||||
|
Stream.of(ModelType.values())
|
||||||
|
.map(m -> new Object[]{ p, m }))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test()
|
||||||
|
{
|
||||||
|
int samples = 3_000;
|
||||||
|
int examples = 10;
|
||||||
|
LongArrayList byToken = new LongArrayList(samples * examples, -1);
|
||||||
|
LongArrayList modelByToken = new LongArrayList(samples * examples, -1);
|
||||||
|
LongArrayList byTokenLength = new LongArrayList(samples * examples, -1);
|
||||||
|
LongArrayList byRange = new LongArrayList(samples * examples, -1);
|
||||||
|
LongArrayList modelByRange = new LongArrayList(samples * examples, -1);
|
||||||
|
LongArrayList byRangeLength = new LongArrayList(samples * examples, -1);
|
||||||
|
qt().withExamples(examples).check(rs -> {
|
||||||
|
var map = create(modelType);
|
||||||
|
var model = createModel(modelType);
|
||||||
|
|
||||||
|
Gen<Range> rangeGen = rangeGen(rs, pattern, samples);
|
||||||
|
for (int i = 0; i < samples; i++)
|
||||||
|
{
|
||||||
|
var range = rangeGen.next(rs);
|
||||||
|
var value = SMALL_INT_GEN.nextInt(rs);
|
||||||
|
map.put(range, value);
|
||||||
|
model.put(range, value);
|
||||||
|
}
|
||||||
|
model.done();
|
||||||
|
Assertions.assertThat(map.actual()).hasSize(samples);
|
||||||
|
if (rangeGen instanceof NoOverlap)
|
||||||
|
((NoOverlap) rangeGen).reset();
|
||||||
|
Gen.IntGen tokenGe = TOKEN_DISTRIBUTION.next(rs);
|
||||||
|
for (int i = 0; i < samples; i++)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
// key lookup
|
||||||
|
var lookup = IntKey.routing(tokenGe.nextInt(rs));
|
||||||
|
var actual = timed(byToken, () -> map.intersectsToken(lookup));
|
||||||
|
var expected = timed(modelByToken, () -> model.intersectsToken(lookup));
|
||||||
|
byTokenLength.addLong(expected.size());
|
||||||
|
Assertions.assertThat(sort(actual))
|
||||||
|
.describedAs("Write=%d; token=%s", i, lookup)
|
||||||
|
.isEqualTo(sort(expected));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// range lookup
|
||||||
|
var lookup = rangeGen.next(rs);
|
||||||
|
var actual = timed(byRange, () -> map.intersects(lookup));
|
||||||
|
var expected = timed(modelByRange, () -> model.intersects(lookup));
|
||||||
|
byRangeLength.addLong(expected.size());
|
||||||
|
Assertions.assertThat(sort(actual))
|
||||||
|
.describedAs("Write=%d; range=%s", i, lookup)
|
||||||
|
.isEqualTo(sort(expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("=======");
|
||||||
|
sb.append("\nPattern: " + pattern);
|
||||||
|
sb.append("\nModel: " + modelType);
|
||||||
|
sb.append("\nBy Token:");
|
||||||
|
sb.append("\n\tSizes: " + stats(byTokenLength, false));
|
||||||
|
sb.append("\n\t" + modelType + ": " + stats(modelByToken, true));
|
||||||
|
sb.append("\n\tTree: " + stats(byToken, true));
|
||||||
|
sb.append("\nBy Range:");
|
||||||
|
sb.append("\n\tSizes: " + stats(byRangeLength, false));
|
||||||
|
sb.append("\n\t" + modelType + ": " + stats(modelByRange, true));
|
||||||
|
sb.append("\n\tTree: " + stats(byRange, true));
|
||||||
|
logger.info(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoOverlap implements Gen<Range>
|
||||||
|
{
|
||||||
|
private final int delta;
|
||||||
|
private int idx = 0;
|
||||||
|
|
||||||
|
public NoOverlap(int samples)
|
||||||
|
{
|
||||||
|
this.delta = TOKEN_RANGE_SIZE / samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Range next(RandomSource random)
|
||||||
|
{
|
||||||
|
int a = delta * idx++;
|
||||||
|
int b = a + delta;
|
||||||
|
return IntKey.range(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset()
|
||||||
|
{
|
||||||
|
idx = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Gen<Range> rangeGen(RandomSource randomSource, Pattern pattern, int samples)
|
||||||
|
{
|
||||||
|
Gen.IntGen tokenGen = TOKEN_DISTRIBUTION.next(randomSource);
|
||||||
|
switch (pattern)
|
||||||
|
{
|
||||||
|
case RANDOM:
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int b = tokenGen.nextInt(rs);
|
||||||
|
while (a == b)
|
||||||
|
b = tokenGen.nextInt(rs);
|
||||||
|
if (a > b)
|
||||||
|
{
|
||||||
|
int tmp = a;
|
||||||
|
a = b;
|
||||||
|
b = tmp;
|
||||||
|
}
|
||||||
|
return IntKey.range(a, b);
|
||||||
|
};
|
||||||
|
case SMALL_RANGES:
|
||||||
|
Gen.IntGen rangeSizeGen = RANGE_SIZE_DISTRIBUTION.next(randomSource);
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int rangeSize = rangeSizeGen.nextInt(rs);
|
||||||
|
int b = a + rangeSize;
|
||||||
|
if (b > MAX_TOKEN)
|
||||||
|
{
|
||||||
|
b = a;
|
||||||
|
a = b - rangeSize;
|
||||||
|
}
|
||||||
|
return IntKey.range(a, b);
|
||||||
|
};
|
||||||
|
case NO_OVERLP:
|
||||||
|
return new NoOverlap(samples);
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stats(LongArrayList list, boolean isTime)
|
||||||
|
{
|
||||||
|
LongUnaryOperator fn = isTime ? TimeUnit.NANOSECONDS::toMicros : l -> l;
|
||||||
|
String postfix = isTime ? "micro" : "";
|
||||||
|
long[] array = list.toLongArray();
|
||||||
|
Arrays.sort(array);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Min: ").append(fn.applyAsLong(array[0])).append(postfix);
|
||||||
|
sb.append(", Median: ").append(fn.applyAsLong(array[array.length / 2])).append(postfix);
|
||||||
|
sb.append(", Max: ").append(fn.applyAsLong(array[array.length - 1])).append(postfix);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T timed(LongArrayList target, Supplier<T> fn)
|
||||||
|
{
|
||||||
|
long nowNs = System.nanoTime();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return fn.get();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
target.add(System.nanoTime() - nowNs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Map.Entry<Range, Integer>> sort(List<Map.Entry<Range, Integer>> array)
|
||||||
|
{
|
||||||
|
array.sort((a, b) -> {
|
||||||
|
int rc = a.getKey().compare(b.getKey());
|
||||||
|
if (rc == 0)
|
||||||
|
rc = a.getValue().compareTo(b.getValue());
|
||||||
|
return rc;
|
||||||
|
});
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface Model
|
||||||
|
{
|
||||||
|
Object actual();
|
||||||
|
|
||||||
|
void put(Range range, int value);
|
||||||
|
|
||||||
|
List<Map.Entry<Range, Integer>> intersectsToken(Routing key);
|
||||||
|
|
||||||
|
List<Map.Entry<Range, Integer>> intersects(Range range);
|
||||||
|
|
||||||
|
void done();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RangeTreeModel create(ModelType modelType)
|
||||||
|
{
|
||||||
|
switch (modelType)
|
||||||
|
{
|
||||||
|
case List:
|
||||||
|
case SearchableRangeList:
|
||||||
|
return new RangeTreeModel(new RTree<>(COMPARATOR, END_INCLUSIVE));
|
||||||
|
case IntervalTree: return new RangeTreeModel(new RTree<>(COMPARATOR, ALL_INCLUSIVE));
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unknown type: " + modelType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Model createModel(ModelType modelType)
|
||||||
|
{
|
||||||
|
switch (modelType)
|
||||||
|
{
|
||||||
|
case List: return new ListModel();
|
||||||
|
case SearchableRangeList: return new SearchableRangeListModel();
|
||||||
|
case IntervalTree: return new IntervalTreeModel();
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unknown type: " + modelType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RangeTreeModel implements Model
|
||||||
|
{
|
||||||
|
private final RangeTree<Routing, Range, Integer> tree;
|
||||||
|
|
||||||
|
private RangeTreeModel(RangeTree<Routing, Range, Integer> tree)
|
||||||
|
{
|
||||||
|
this.tree = tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RangeTree<Routing, Range, Integer> actual()
|
||||||
|
{
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(Range range, int value)
|
||||||
|
{
|
||||||
|
tree.add(range, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersectsToken(Routing key)
|
||||||
|
{
|
||||||
|
return tree.searchToken(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersects(Range range)
|
||||||
|
{
|
||||||
|
return tree.search(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ListModel implements Model
|
||||||
|
{
|
||||||
|
List<Map.Entry<Range, Integer>> actual = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> actual()
|
||||||
|
{
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(Range range, int value)
|
||||||
|
{
|
||||||
|
actual.add(Map.entry(range, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersectsToken(Routing key)
|
||||||
|
{
|
||||||
|
return actual.stream()
|
||||||
|
.filter(p -> p.getKey().contains(key))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersects(Range range)
|
||||||
|
{
|
||||||
|
return actual.stream()
|
||||||
|
.filter(p -> p.getKey().compareIntersecting(range) == 0)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class IntervalTreeModel implements Model
|
||||||
|
{
|
||||||
|
IntervalTree.Builder<Routing, Integer, Interval<Routing, Integer>> builder = IntervalTree.builder();
|
||||||
|
IntervalTree<Routing, Integer, Interval<Routing, Integer>> actual = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IntervalTree<Routing, Integer, Interval<Routing, Integer>> actual()
|
||||||
|
{
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(Range range, int value)
|
||||||
|
{
|
||||||
|
builder.add(new Interval<>((Routing) range.start(), (Routing) range.end(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersectsToken(Routing key)
|
||||||
|
{
|
||||||
|
return map(actual.matches(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersects(Range range)
|
||||||
|
{
|
||||||
|
return map(actual.matches(new Interval<>((Routing) range.start(), (Routing) range.end(), null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Map.Entry<Range, Integer>> map(List<Interval<Routing, Integer>> matches)
|
||||||
|
{
|
||||||
|
return matches.stream().map(i -> Map.entry(IntKey.range(i.min, i.max), i.data)).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done()
|
||||||
|
{
|
||||||
|
assert builder != null;
|
||||||
|
actual = builder.build();
|
||||||
|
builder = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SearchableRangeListModel implements Model
|
||||||
|
{
|
||||||
|
private final Map<Range, IntArrayList> map = new HashMap<>();
|
||||||
|
private Range[] ranges;
|
||||||
|
private SearchableRangeList list = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object actual()
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(Range range, int value)
|
||||||
|
{
|
||||||
|
map.computeIfAbsent(range, ignore -> new IntArrayList()).addInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersectsToken(Routing key)
|
||||||
|
{
|
||||||
|
List<Map.Entry<Range, Integer>> matches = new ArrayList<>();
|
||||||
|
// find ranges, then add the values
|
||||||
|
list.forEach(key, (a, b, c, d, idx) -> {
|
||||||
|
Range match = ranges[idx];
|
||||||
|
map.get(match).forEachInt(v -> matches.add(Map.entry(match, v)));
|
||||||
|
}, (a, b, c, d, start, end) -> {
|
||||||
|
for (int i = start; i < end; i++)
|
||||||
|
{
|
||||||
|
Range match = ranges[i];
|
||||||
|
map.get(match).forEachInt(v -> matches.add(Map.entry(match, v)));
|
||||||
|
}
|
||||||
|
}, 0, 0, 0, 0, 0);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> intersects(Range range)
|
||||||
|
{
|
||||||
|
List<Map.Entry<Range, Integer>> matches = new ArrayList<>();
|
||||||
|
// find ranges, then add the values
|
||||||
|
list.forEach(range, (a, b, c, d, idx) -> {
|
||||||
|
Range match = ranges[idx];
|
||||||
|
map.get(match).forEachInt(v -> matches.add(Map.entry(match, v)));
|
||||||
|
}, (a, b, c, d, start, end) -> {
|
||||||
|
for (int i = start; i < end; i++)
|
||||||
|
{
|
||||||
|
Range match = ranges[i];
|
||||||
|
map.get(match).forEachInt(v -> matches.add(Map.entry(match, v)));
|
||||||
|
}
|
||||||
|
}, 0, 0, 0, 0, 0);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done()
|
||||||
|
{
|
||||||
|
List<Range> ranges = new ArrayList<>(map.keySet());
|
||||||
|
ranges.sort(Range::compare);
|
||||||
|
list = SearchableRangeList.build(this.ranges = ranges.toArray(Range[]::new));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,509 @@
|
||||||
|
/*
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import accord.api.RoutingKey;
|
||||||
|
import accord.impl.IntKey;
|
||||||
|
import accord.primitives.Range;
|
||||||
|
import accord.utils.Gen;
|
||||||
|
import accord.utils.Gens;
|
||||||
|
import accord.utils.Property.Command;
|
||||||
|
import accord.utils.Property.Commands;
|
||||||
|
import accord.utils.Property.UnitCommand;
|
||||||
|
import accord.utils.RandomSource;
|
||||||
|
import org.apache.cassandra.service.accord.RangeTreeRangeAccessor;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
|
import static accord.utils.Property.stateful;
|
||||||
|
|
||||||
|
public class StatefulRangeTreeTest
|
||||||
|
{
|
||||||
|
private static final Gen.IntGen SMALL_INT_GEN = rs -> rs.nextInt(0, 10);
|
||||||
|
private static final Gen.IntGen NUM_CHILDREN_GEN = rs -> rs.nextInt(2, 12);
|
||||||
|
private static final Gen<Gen.IntGen> SIZE_TARGET_DISTRIBUTION = Gens.mixedDistribution(1 << 3, 1 << 9);
|
||||||
|
private static final int MIN_TOKEN = 0, MAX_TOKEN = 1 << 16;
|
||||||
|
private static final int TOKEN_RANGE_SIZE = MAX_TOKEN - MIN_TOKEN + 1;
|
||||||
|
private static final Gen<Gen.IntGen> TOKEN_DISTRIBUTION = Gens.mixedDistribution(MIN_TOKEN, MAX_TOKEN + 1);
|
||||||
|
private static final Gen<Gen.IntGen> RANGE_SIZE_DISTRIBUTION = Gens.mixedDistribution(10, (int) (TOKEN_RANGE_SIZE * .01));
|
||||||
|
static final Comparator<Map.Entry<Range, Integer>> COMPARATOR = (a, b) -> {
|
||||||
|
int rc = a.getKey().compare(b.getKey());
|
||||||
|
if (rc == 0)
|
||||||
|
rc = a.getValue().compareTo(b.getValue());
|
||||||
|
return rc;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateful test for RTree.
|
||||||
|
*
|
||||||
|
* This test is very similar to {@link RangeTreeTest#test} but is fully mutable, so can not
|
||||||
|
* use the immutable search trees (else rebuidling becomes a large cost). Both tests should exist as they use different
|
||||||
|
* models, which helps build confidence that the RTree does the correct thing; that test also covers start and end
|
||||||
|
* inclusive, which this test does not.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void test()
|
||||||
|
{
|
||||||
|
stateful().check(new Commands<State, Sut>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Gen<State> genInitialState()
|
||||||
|
{
|
||||||
|
return rs -> {
|
||||||
|
Gen<Range> rangeGen = rangeGen(rs);
|
||||||
|
int numChildren = NUM_CHILDREN_GEN.nextInt(rs);
|
||||||
|
int sizeTarget = SIZE_TARGET_DISTRIBUTION.next(rs).filter(s -> s > numChildren).nextInt(rs);
|
||||||
|
int createWeight = rs.nextInt(1, 100);
|
||||||
|
int updateWeight = rs.nextInt(1, 20);
|
||||||
|
int deleteWeight = rs.nextInt(1, 20);
|
||||||
|
int clearWeight = rs.nextInt(0, 2); // either disabled or enabled with weight=1
|
||||||
|
int readWeight = rs.nextInt(1, 20);
|
||||||
|
return new State(sizeTarget, numChildren,
|
||||||
|
TOKEN_DISTRIBUTION.next(rs), rangeGen,
|
||||||
|
createWeight, updateWeight, deleteWeight, clearWeight, readWeight);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Sut createSut(State state)
|
||||||
|
{
|
||||||
|
return new Sut(state.sizeTarget, state.numChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Gen<Command<State, Sut, ?>> commands(State state)
|
||||||
|
{
|
||||||
|
Map<Gen<Command<State, Sut, ?>>, Integer> possible = new HashMap<>();
|
||||||
|
possible.put(rs -> new Create(state.newRange(rs), SMALL_INT_GEN.nextInt(rs)), state.createWeight);
|
||||||
|
possible.put(rs -> new Read(state.newRange(rs)), state.readWeight);
|
||||||
|
possible.put(rs -> new KeyRead(IntKey.routing(state.tokenGen.nextInt(rs))), state.readWeight);
|
||||||
|
possible.put(rs -> new RangeRead(state.rangeGen.next(rs)), state.readWeight);
|
||||||
|
possible.put(ignore -> Iterate.instance, state.readWeight);
|
||||||
|
possible.put(ignore -> Clear.instance, state.clearWeight);
|
||||||
|
if (!state.uniqRanges.isEmpty())
|
||||||
|
{
|
||||||
|
possible.put(rs -> new Read(rs.pick(state.uniqRanges)), state.readWeight);
|
||||||
|
possible.put(rs -> {
|
||||||
|
Range range = rs.pick(state.uniqRanges);
|
||||||
|
int token = rs.nextInt(((IntKey.Routing) range.start()).key, ((IntKey.Routing) range.end()).key) + 1;
|
||||||
|
return new KeyRead(IntKey.routing(token));
|
||||||
|
}, state.readWeight);
|
||||||
|
possible.put(rs -> new RangeRead(rs.pick(state.uniqRanges)), state.readWeight);
|
||||||
|
possible.put(rs -> new Update(rs.pick(state.uniqRanges), SMALL_INT_GEN.nextInt(rs)), state.updateWeight);
|
||||||
|
possible.put(rs -> new Delete(rs.pick(state.uniqRanges)), state.deleteWeight);
|
||||||
|
}
|
||||||
|
return Gens.oneOf(possible);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Gen<Range> rangeGen(RandomSource rand)
|
||||||
|
{
|
||||||
|
Gen.IntGen tokenGen = TOKEN_DISTRIBUTION.next(rand);
|
||||||
|
switch (rand.nextInt(0, 3))
|
||||||
|
{
|
||||||
|
case 0: // pure random
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int b = tokenGen.nextInt(rs);
|
||||||
|
while (a == b)
|
||||||
|
b = tokenGen.nextInt(rs);
|
||||||
|
if (a > b)
|
||||||
|
{
|
||||||
|
int tmp = a;
|
||||||
|
a = b;
|
||||||
|
b = tmp;
|
||||||
|
}
|
||||||
|
return IntKey.range(a, b);
|
||||||
|
};
|
||||||
|
case 1: // small range
|
||||||
|
Gen.IntGen rangeSizeGen = RANGE_SIZE_DISTRIBUTION.next(rand);
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int rangeSize = rangeSizeGen.nextInt(rs);
|
||||||
|
int b = a + rangeSize;
|
||||||
|
if (b > MAX_TOKEN)
|
||||||
|
{
|
||||||
|
b = a;
|
||||||
|
a = b - rangeSize;
|
||||||
|
}
|
||||||
|
return IntKey.range(a, b);
|
||||||
|
};
|
||||||
|
case 2: // single element
|
||||||
|
return rs -> {
|
||||||
|
int a = tokenGen.nextInt(rs);
|
||||||
|
int b = a + 1;
|
||||||
|
return IntKey.range(a, b);
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Create implements UnitCommand<State, Sut>
|
||||||
|
{
|
||||||
|
private final Range range;
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
Create(Range range, int value)
|
||||||
|
{
|
||||||
|
this.range = range;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUnit(State state)
|
||||||
|
{
|
||||||
|
state.add(range, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runUnit(Sut sut)
|
||||||
|
{
|
||||||
|
sut.tree.add(range, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkPostconditions(State state, Void expected,
|
||||||
|
Sut sut, Void actual)
|
||||||
|
{
|
||||||
|
Assertions.assertThat(sut.tree.size()).isEqualTo(state.list.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Create(" + range + ", " + value + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static abstract class AbstractRead<T> implements Command<State, Sut, List<T>>
|
||||||
|
{
|
||||||
|
private final Comparator<T> comparator;
|
||||||
|
|
||||||
|
protected AbstractRead(Comparator<T> comparator)
|
||||||
|
{
|
||||||
|
this.comparator = comparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkPostconditions(State state, List<T> expected,
|
||||||
|
Sut sut, List<T> actual)
|
||||||
|
{
|
||||||
|
expected.sort(comparator);
|
||||||
|
actual.sort(comparator);
|
||||||
|
Assertions.assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Read extends AbstractRead<Integer>
|
||||||
|
{
|
||||||
|
private final Range range;
|
||||||
|
|
||||||
|
Read(Range range)
|
||||||
|
{
|
||||||
|
super(Comparator.naturalOrder());
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> apply(State state)
|
||||||
|
{
|
||||||
|
return state.get(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> run(Sut sut)
|
||||||
|
{
|
||||||
|
return sut.tree.get(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Read(" + range + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RangeRead extends AbstractRead<Map.Entry<Range, Integer>>
|
||||||
|
{
|
||||||
|
private final Range range;
|
||||||
|
|
||||||
|
RangeRead(Range range)
|
||||||
|
{
|
||||||
|
super(COMPARATOR);
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> apply(State state)
|
||||||
|
{
|
||||||
|
return state.list.stream().filter(e -> e.getKey().compareIntersecting(range) == 0).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> run(Sut sut)
|
||||||
|
{
|
||||||
|
return sut.tree.search(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Range Read(" + range + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeyRead extends AbstractRead<Map.Entry<Range, Integer>>
|
||||||
|
{
|
||||||
|
final RoutingKey key;
|
||||||
|
|
||||||
|
KeyRead(RoutingKey key)
|
||||||
|
{
|
||||||
|
super(COMPARATOR);
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> apply(State state)
|
||||||
|
{
|
||||||
|
return state.list.stream().filter(e -> e.getKey().contains(key)).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> run(Sut sut)
|
||||||
|
{
|
||||||
|
return sut.tree.searchToken(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Token Read(" + key + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Update implements UnitCommand<State, Sut>
|
||||||
|
{
|
||||||
|
private final Range range;
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
Update(Range range, int value)
|
||||||
|
{
|
||||||
|
this.range = range;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUnit(State state)
|
||||||
|
{
|
||||||
|
state.update(range, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runUnit(Sut sut)
|
||||||
|
{
|
||||||
|
sut.tree.get(range, e -> e.setValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Update(" + range + ", " + value + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Delete implements UnitCommand<State, Sut>
|
||||||
|
{
|
||||||
|
private final Range range;
|
||||||
|
|
||||||
|
Delete(Range range)
|
||||||
|
{
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUnit(State state)
|
||||||
|
{
|
||||||
|
state.remove(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runUnit(Sut sut)
|
||||||
|
{
|
||||||
|
sut.tree.remove(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkPostconditions(State state, Void expected,
|
||||||
|
Sut sut, Void actual)
|
||||||
|
{
|
||||||
|
Assertions.assertThat(sut.tree.size()).isEqualTo(state.list.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Delete(" + range + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Clear implements UnitCommand<State, Sut>
|
||||||
|
{
|
||||||
|
static final Clear instance = new Clear();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUnit(State state)
|
||||||
|
{
|
||||||
|
state.uniqRanges.clear();
|
||||||
|
state.list.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runUnit(Sut sut)
|
||||||
|
{
|
||||||
|
sut.tree.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Clear(size=" + state.list.size() + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Iterate extends AbstractRead<Map.Entry<Range, Integer>>
|
||||||
|
{
|
||||||
|
static final Iterate instance = new Iterate();
|
||||||
|
public Iterate()
|
||||||
|
{
|
||||||
|
super(COMPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> apply(State state)
|
||||||
|
{
|
||||||
|
return state.list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map.Entry<Range, Integer>> run(Sut sut)
|
||||||
|
{
|
||||||
|
return sut.tree.stream().collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String detailed(State state)
|
||||||
|
{
|
||||||
|
return "Iterate(size=" + state.list.size() + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class State
|
||||||
|
{
|
||||||
|
private final List<Map.Entry<Range, Integer>> list = new ArrayList<>();
|
||||||
|
private final TreeSet<Range> uniqRanges = new TreeSet<>(Range::compare);
|
||||||
|
private final int sizeTarget, numChildren;
|
||||||
|
private final Gen.IntGen tokenGen;
|
||||||
|
private final Gen<Range> rangeGen;
|
||||||
|
private final int createWeight, updateWeight, deleteWeight, clearWeight, readWeight;
|
||||||
|
|
||||||
|
private State(int sizeTarget, int numChildren,
|
||||||
|
Gen.IntGen tokenGen, Gen<Range> rangeGen,
|
||||||
|
int createWeight, int updateWeight, int deleteWeight, int clearWeight, int readWeight)
|
||||||
|
{
|
||||||
|
this.sizeTarget = sizeTarget;
|
||||||
|
this.numChildren = numChildren;
|
||||||
|
this.tokenGen = tokenGen;
|
||||||
|
this.rangeGen = rangeGen;
|
||||||
|
this.createWeight = createWeight;
|
||||||
|
this.updateWeight = updateWeight;
|
||||||
|
this.deleteWeight = deleteWeight;
|
||||||
|
this.clearWeight = clearWeight;
|
||||||
|
this.readWeight = readWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Range newRange(RandomSource rs)
|
||||||
|
{
|
||||||
|
Range range;
|
||||||
|
while ((uniqRanges.contains(range = rangeGen.next(rs)))) {}
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(Range range, int value)
|
||||||
|
{
|
||||||
|
list.add(new MutableEntry<>(range, value));
|
||||||
|
uniqRanges.add(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> get(Range range)
|
||||||
|
{
|
||||||
|
if (!uniqRanges.contains(range))
|
||||||
|
return Collections.emptyList();
|
||||||
|
return list.stream().filter(e -> e.getKey().equals(range)).map(e -> e.getValue()).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(Range range, int value)
|
||||||
|
{
|
||||||
|
if (!uniqRanges.contains(range))
|
||||||
|
return;
|
||||||
|
list.forEach(e -> {
|
||||||
|
if (e.getKey().equals(range))
|
||||||
|
e.setValue(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(Range range)
|
||||||
|
{
|
||||||
|
if (!uniqRanges.contains(range))
|
||||||
|
return;
|
||||||
|
uniqRanges.remove(range);
|
||||||
|
list.removeIf(e -> e.getKey().equals(range));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "State{" +
|
||||||
|
"sizeTarget=" + sizeTarget +
|
||||||
|
", numChildren=" + numChildren +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Sut
|
||||||
|
{
|
||||||
|
private final RangeTree<RoutingKey, Range, Integer> tree;
|
||||||
|
|
||||||
|
private Sut(int sizeTarget, int numChildren)
|
||||||
|
{
|
||||||
|
tree = new RTree(Comparator.naturalOrder(), RangeTreeRangeAccessor.instance, sizeTarget, numChildren);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue