Merge branch 'cassandra-6.0' into trunk

This commit is contained in:
Benedict Elliott Smith 2026-06-08 14:30:32 +01:00
commit 45eee68f55
14 changed files with 436 additions and 22 deletions

View File

@ -3,6 +3,7 @@
* Allow nodetool garbagecollect to take a user defined list of SSTables (CASSANDRA-16767) * Allow nodetool garbagecollect to take a user defined list of SSTables (CASSANDRA-16767)
* Add a guardrail for misprepared statements (CASSANDRA-21139) * Add a guardrail for misprepared statements (CASSANDRA-21139)
Merged from 6.0: Merged from 6.0:
* Safely regain ranges and delete retired command stores (CASSANDRA-21212)
* Reduce memory allocations in miscellaneous places along read path (CASSANDRA-21360) * Reduce memory allocations in miscellaneous places along read path (CASSANDRA-21360)
* Avoid ByteBuffer wrapping in cql3.selection.Selector.InputRow to reduce memory allocation rate (CASSANDRA-21362) * Avoid ByteBuffer wrapping in cql3.selection.Selector.InputRow to reduce memory allocation rate (CASSANDRA-21362)
* Reduce cost to calculate BTreeRow.minDeletionTime (CASSANDRA-21414) * Reduce cost to calculate BTreeRow.minDeletionTime (CASSANDRA-21414)

@ -1 +1 @@
Subproject commit 3d6392f4eca41d3036a704f1513020c24689f1bb Subproject commit 0a10cd056794c05588114f45ce86d49d6d6538db

View File

@ -1008,9 +1008,9 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte
if (read.kind() == Repeat && !hasWritten) if (read.kind() == Repeat && !hasWritten)
{ {
Invariants.require(lastImage != null); Invariants.require(lastImage != null);
write = new TopologyImage(read.epoch(), Image, lastImage.getUpdate()); write = new TopologyImage(read.epoch(), Image, lastImage.update());
} }
else if (hasWritten && read.kind() == Repeat && lastImage.getUpdate().isEquivalent(read.getUpdate())) else if (hasWritten && read.kind() == Repeat && lastImage.update().isEquivalent(read.update()))
{ {
write = read.asRepeat(); write = read.asRepeat();
} }

View File

@ -127,7 +127,9 @@ class AccordDurableOnFlush implements BiConsumer<Long, TableMetadata>
for (Map.Entry<Integer, ReportDurable> e : notify.entrySet()) for (Map.Entry<Integer, ReportDurable> e : notify.entrySet())
{ {
ReportDurable durable = e.getValue(); ReportDurable durable = e.getValue();
notifyInOrder(memtableId, metadata, commandStores.forId(e.getKey()), durable); CommandStore commandStore = commandStores.forId(e.getKey());
if (commandStore != null)
notifyInOrder(memtableId, metadata, commandStore, durable);
} }
} }

View File

@ -40,6 +40,7 @@ import static org.apache.cassandra.db.TypeSizes.LONG_SIZE;
import static org.apache.cassandra.service.accord.journal.MergeSerializers.BootstrapBeganAtSerializer; import static org.apache.cassandra.service.accord.journal.MergeSerializers.BootstrapBeganAtSerializer;
import static org.apache.cassandra.service.accord.journal.MergeSerializers.CommandChangeSerializer; import static org.apache.cassandra.service.accord.journal.MergeSerializers.CommandChangeSerializer;
import static org.apache.cassandra.service.accord.journal.MergeSerializers.DurableBeforeSerializer; import static org.apache.cassandra.service.accord.journal.MergeSerializers.DurableBeforeSerializer;
import static org.apache.cassandra.service.accord.journal.MergeSerializers.PermanentlyUnsafeToReadSerializer;
import static org.apache.cassandra.service.accord.journal.MergeSerializers.RangesForEpochSerializer; import static org.apache.cassandra.service.accord.journal.MergeSerializers.RangesForEpochSerializer;
import static org.apache.cassandra.service.accord.journal.MergeSerializers.RedundantBeforeSerializer; import static org.apache.cassandra.service.accord.journal.MergeSerializers.RedundantBeforeSerializer;
import static org.apache.cassandra.service.accord.journal.MergeSerializers.SafeToReadSerializer; import static org.apache.cassandra.service.accord.journal.MergeSerializers.SafeToReadSerializer;
@ -273,7 +274,8 @@ public final class JournalKey
SAFE_TO_READ (3, new SafeToReadSerializer(), false), SAFE_TO_READ (3, new SafeToReadSerializer(), false),
BOOTSTRAP_BEGAN_AT (4, new BootstrapBeganAtSerializer(), false), BOOTSTRAP_BEGAN_AT (4, new BootstrapBeganAtSerializer(), false),
RANGES_FOR_EPOCH (5, new RangesForEpochSerializer(), false), RANGES_FOR_EPOCH (5, new RangesForEpochSerializer(), false),
TOPOLOGY_UPDATE (6, new TopologySerializer(), true), TOPOLOGY_UPDATE (6, TopologySerializer.INSTANCE, true),
PERMANENTLY_UNSAFE_TO_READ (7, new PermanentlyUnsafeToReadSerializer(), false)
; ;
public final int id; public final int id;

View File

@ -288,12 +288,13 @@ public class AccordJournal implements accord.api.Journal, RangeSearcher.Supplier
logger.error("Encountered TopologyImage Repeat record for epoch {}, but no prior image record was found", ref.key().id.epoch()); logger.error("Encountered TopologyImage Repeat record for epoch {}, but no prior image record was found", ref.key().id.epoch());
return null; return null;
} }
prev = reader.read().asImage(Invariants.nonNull(prev.getUpdate())); prev = reader.read().asImage(Invariants.nonNull(prev.update()));
} }
else prev = reader.read(); else prev = reader.read();
return new accord.api.Journal.TopologyUpdate(prev.getUpdate().commandStores, return new accord.api.Journal.TopologyUpdate(prev.update().commandStores,
prev.getUpdate().global); prev.update().global,
prev.update().previouslyOwned);
} }
@Override @Override
@ -358,6 +359,13 @@ public class AccordJournal implements accord.api.Journal, RangeSearcher.Supplier
return accumulator.get(); return accumulator.get();
} }
@Override
public Ranges loadPermanentlyUnsafeToRead(int commandStoreId)
{
KeepFirst<Ranges> accumulator = readLast(new JournalKey(TxnId.NONE, JournalKey.Type.PERMANENTLY_UNSAFE_TO_READ, commandStoreId));
return accumulator.get();
}
@Override @Override
public PersistentField.Persister<DurableBefore, DurableBefore> durableBeforePersister() public PersistentField.Persister<DurableBefore, DurableBefore> durableBeforePersister()
{ {
@ -376,6 +384,8 @@ public class AccordJournal implements accord.api.Journal, RangeSearcher.Supplier
pointer = appendInternal(new JournalKey(TxnId.NONE, JournalKey.Type.SAFE_TO_READ, commandStoreId), fieldUpdates.newSafeToRead); pointer = appendInternal(new JournalKey(TxnId.NONE, JournalKey.Type.SAFE_TO_READ, commandStoreId), fieldUpdates.newSafeToRead);
if (fieldUpdates.newRangesForEpoch != null) if (fieldUpdates.newRangesForEpoch != null)
pointer = appendInternal(new JournalKey(TxnId.NONE, JournalKey.Type.RANGES_FOR_EPOCH, commandStoreId), fieldUpdates.newRangesForEpoch); pointer = appendInternal(new JournalKey(TxnId.NONE, JournalKey.Type.RANGES_FOR_EPOCH, commandStoreId), fieldUpdates.newRangesForEpoch);
if (fieldUpdates.newPermanentlyUnsafeToRead != null)
pointer = appendInternal(new JournalKey(TxnId.NONE, JournalKey.Type.PERMANENTLY_UNSAFE_TO_READ, commandStoreId), fieldUpdates.newPermanentlyUnsafeToRead);
if (onFlush == null) if (onFlush == null)
return; return;

View File

@ -37,6 +37,7 @@ import org.apache.cassandra.service.accord.JournalKey;
import org.apache.cassandra.service.accord.journal.Merger.KeepFirst; import org.apache.cassandra.service.accord.journal.Merger.KeepFirst;
import org.apache.cassandra.service.accord.journal.Merger.SimpleMerger; import org.apache.cassandra.service.accord.journal.Merger.SimpleMerger;
import org.apache.cassandra.service.accord.serializers.CommandStoreSerializers; import org.apache.cassandra.service.accord.serializers.CommandStoreSerializers;
import org.apache.cassandra.service.accord.serializers.KeySerializers;
import org.apache.cassandra.service.accord.serializers.Version; import org.apache.cassandra.service.accord.serializers.Version;
import static accord.local.CommandStores.RangesForEpoch; import static accord.local.CommandStores.RangesForEpoch;
@ -250,7 +251,6 @@ public class MergeSerializers
SimpleMerger<?, ? super RangesForEpoch>, SimpleMerger<?, ? super RangesForEpoch>,
SimpleMerger<RangesForEpoch, RangesForEpoch>> SimpleMerger<RangesForEpoch, RangesForEpoch>>
{ {
public static final RangesForEpochSerializer instance = new RangesForEpochSerializer();
public KeepFirst<RangesForEpoch> mergerFor() public KeepFirst<RangesForEpoch> mergerFor()
{ {
return new KeepFirst<>(null); return new KeepFirst<>(null);
@ -275,11 +275,40 @@ public class MergeSerializers
} }
} }
public static class PermanentlyUnsafeToReadSerializer
implements MergeSerializer<Ranges,
SimpleMerger<?, ? super Ranges>,
SimpleMerger<Ranges, Ranges>>
{
public KeepFirst<Ranges> mergerFor()
{
return new KeepFirst<>(Ranges.EMPTY);
}
@Override
public void serialize(JournalKey key, Ranges from, DataOutputPlus out, Version userVersion) throws IOException
{
KeySerializers.ranges.serialize(from, out);
}
@Override
public void reserialize(JournalKey key, SimpleMerger<Ranges, Ranges> from, DataOutputPlus out, Version userVersion) throws IOException
{
serialize(key, from.get(), out, userVersion);
}
@Override
public void deserialize(JournalKey key, SimpleMerger<?, ? super Ranges> into, DataInputPlus in, Version userVersion) throws IOException
{
into.update(KeySerializers.ranges.deserialize(in));
}
}
public static class TopologySerializer implements MergeSerializer<TopologyRecord, TopologyMerger, TopologyMerger> public static class TopologySerializer implements MergeSerializer<TopologyRecord, TopologyMerger, TopologyMerger>
{ {
public static final TopologySerializer INSTANCE = new TopologySerializer(); public static final TopologySerializer INSTANCE = new TopologySerializer();
public TopologySerializer() {} private TopologySerializer() {}
@Override @Override
public TopologyMerger mergerFor() public TopologyMerger mergerFor()
@ -309,6 +338,7 @@ public class MergeSerializers
public static class TopologyMerger implements Merger public static class TopologyMerger implements Merger
{ {
TopologyRecord.TopologyImage read, write; TopologyRecord.TopologyImage read, write;
boolean hasRead;
public TopologyMerger() public TopologyMerger()
{ {
@ -317,6 +347,7 @@ public class MergeSerializers
@Override @Override
public void reset(JournalKey key) public void reset(JournalKey key)
{ {
hasRead = false;
read = write = null; read = write = null;
} }
@ -327,11 +358,15 @@ public class MergeSerializers
public void read(TopologyRecord update) public void read(TopologyRecord update)
{ {
if (hasRead)
return;
if (Objects.requireNonNull(update.kind()) == TopologyRecord.Kind.New) if (Objects.requireNonNull(update.kind()) == TopologyRecord.Kind.New)
read = new TopologyRecord.TopologyImage(update.epoch(), TopologyRecord.Kind.Image, update.getUpdate()); read = new TopologyRecord.TopologyImage(update.epoch(), TopologyRecord.Kind.Image, update.update());
else else
read = (TopologyRecord.TopologyImage) update; read = (TopologyRecord.TopologyImage) update;
write = read; write = read;
hasRead = true;
} }
public void write(TopologyRecord.TopologyImage image) public void write(TopologyRecord.TopologyImage image)

View File

@ -26,6 +26,7 @@ import org.agrona.collections.Int2ObjectHashMap;
import accord.api.Journal; import accord.api.Journal;
import accord.local.CommandStores; import accord.local.CommandStores;
import accord.local.CommandStores.PreviouslyOwned;
import accord.local.Node; import accord.local.Node;
import accord.primitives.Ranges; import accord.primitives.Ranges;
import accord.primitives.TxnId; import accord.primitives.TxnId;
@ -50,7 +51,7 @@ public interface TopologyRecord
long epoch(); long epoch();
TopologyRecord asRepeat(); TopologyRecord asRepeat();
Journal.TopologyUpdate getUpdate(); Journal.TopologyUpdate update();
static TopologyRecord newTopology(Journal.TopologyUpdate update) static TopologyRecord newTopology(Journal.TopologyUpdate update)
{ {
return new NewTopology(update); return new NewTopology(update);
@ -121,7 +122,7 @@ public interface TopologyRecord
} }
@Override @Override
public Journal.TopologyUpdate getUpdate() public Journal.TopologyUpdate update()
{ {
return update; return update;
} }
@ -182,7 +183,7 @@ public interface TopologyRecord
} }
@Override @Override
public Journal.TopologyUpdate getUpdate() public Journal.TopologyUpdate update()
{ {
return update; return update;
} }
@ -225,12 +226,20 @@ public interface TopologyRecord
class TopologyUpdateSerializer implements UnversionedSerializer<Journal.TopologyUpdate> class TopologyUpdateSerializer implements UnversionedSerializer<Journal.TopologyUpdate>
{ {
private static final int TOP_BIT = 0x40000000;
public static final TopologyUpdateSerializer instance = new TopologyUpdateSerializer(); public static final TopologyUpdateSerializer instance = new TopologyUpdateSerializer();
@Override @Override
public void serialize(Journal.TopologyUpdate from, DataOutputPlus out) throws IOException public void serialize(Journal.TopologyUpdate from, DataOutputPlus out) throws IOException
{ {
out.writeUnsignedVInt32(from.commandStores.size()); out.writeUnsignedVInt32(from.commandStores.size() | TOP_BIT);
out.writeUnsignedVInt32(0);
out.writeUnsignedVInt32(from.previouslyOwned.size());
for (int i = 0 ; i < from.previouslyOwned.size() ; ++i)
{
out.writeUnsignedVInt(from.previouslyOwned.epochs(i));
KeySerializers.ranges.serialize(from.previouslyOwned.ranges(i), out);
}
for (Map.Entry<Integer, CommandStores.RangesForEpoch> e : from.commandStores.entrySet()) for (Map.Entry<Integer, CommandStores.RangesForEpoch> e : from.commandStores.entrySet())
{ {
out.writeUnsignedVInt32(e.getKey()); out.writeUnsignedVInt32(e.getKey());
@ -243,6 +252,23 @@ public interface TopologyRecord
public Journal.TopologyUpdate deserialize(DataInputPlus in) throws IOException public Journal.TopologyUpdate deserialize(DataInputPlus in) throws IOException
{ {
int commandStoresSize = in.readUnsignedVInt32(); int commandStoresSize = in.readUnsignedVInt32();
int flags = 0;
PreviouslyOwned previouslyOwned = PreviouslyOwned.EMPTY;
if ((commandStoresSize & TOP_BIT) != 0)
{
commandStoresSize ^= TOP_BIT;
// future proofing
flags = in.readUnsignedVInt32();
int previouslyOwnedSize = in.readUnsignedVInt32();
long[] epochs = new long[previouslyOwnedSize];
Ranges[] ranges = new Ranges[previouslyOwnedSize];
for (int i = 0 ; i < previouslyOwnedSize ; ++i)
{
epochs[i] = in.readUnsignedVInt();
ranges[i] = KeySerializers.ranges.deserialize(in);
}
previouslyOwned = new PreviouslyOwned(epochs.length == 0 ? 0 : epochs[0], epochs, ranges);
}
Int2ObjectHashMap<CommandStores.RangesForEpoch> commandStores = new Int2ObjectHashMap<>(); Int2ObjectHashMap<CommandStores.RangesForEpoch> commandStores = new Int2ObjectHashMap<>();
for (int j = 0; j < commandStoresSize; j++) for (int j = 0; j < commandStoresSize; j++)
{ {
@ -251,13 +277,20 @@ public interface TopologyRecord
commandStores.put(commandStoreId, rfe); commandStores.put(commandStoreId, rfe);
} }
Topology global = TopologySerializers.compactTopology.deserialize(in); Topology global = TopologySerializers.compactTopology.deserialize(in);
return new Journal.TopologyUpdate(commandStores, global); return new Journal.TopologyUpdate(commandStores, global, previouslyOwned);
} }
@Override @Override
public long serializedSize(Journal.TopologyUpdate from) public long serializedSize(Journal.TopologyUpdate from)
{ {
long size = TypeSizes.sizeofUnsignedVInt(from.commandStores.size()); long size = TypeSizes.sizeofUnsignedVInt(from.commandStores.size() | TOP_BIT);
size += TypeSizes.sizeofUnsignedVInt(0);
size += TypeSizes.sizeofUnsignedVInt(from.previouslyOwned.size());
for (int i = 0 ; i < from.previouslyOwned.size() ; ++i)
{
size += TypeSizes.sizeofUnsignedVInt(from.previouslyOwned.epochs(i));
size += KeySerializers.ranges.serializedSize(from.previouslyOwned.ranges(i));
}
for (Map.Entry<Integer, CommandStores.RangesForEpoch> e : from.commandStores.entrySet()) for (Map.Entry<Integer, CommandStores.RangesForEpoch> e : from.commandStores.entrySet())
{ {
size += TypeSizes.sizeofUnsignedVInt(e.getKey()); size += TypeSizes.sizeofUnsignedVInt(e.getKey());

View File

@ -27,12 +27,20 @@ import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import accord.api.TopologyListener;
import accord.primitives.Ranges;
import accord.topology.ActiveEpoch;
import accord.topology.EpochReady; import accord.topology.EpochReady;
import accord.topology.Topology;
import accord.topology.TopologyManager;
import accord.topology.TopologyManager.RegainingEpochRange;
import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.SystemKeyspace;
@ -54,6 +62,7 @@ import org.apache.cassandra.schema.ReplicationParams;
import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.AccordService; import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.topology.AccordTopology;
import org.apache.cassandra.streaming.StreamOperation; import org.apache.cassandra.streaming.StreamOperation;
import org.apache.cassandra.streaming.StreamPlan; import org.apache.cassandra.streaming.StreamPlan;
import org.apache.cassandra.streaming.StreamResultFuture; import org.apache.cassandra.streaming.StreamResultFuture;
@ -75,10 +84,13 @@ import org.apache.cassandra.tcm.serialization.Version;
import org.apache.cassandra.tcm.transformations.PrepareMove; import org.apache.cassandra.tcm.transformations.PrepareMove;
import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.concurrent.Condition;
import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.FutureCombiner; import org.apache.cassandra.utils.concurrent.FutureCombiner;
import org.apache.cassandra.utils.vint.VIntCoding; import org.apache.cassandra.utils.vint.VIntCoding;
import static accord.primitives.AbstractRanges.UnionMode.MERGE_ADJACENT;
import static accord.primitives.Routables.Slice.Minimal;
import static com.google.common.collect.ImmutableList.of; import static com.google.common.collect.ImmutableList.of;
import static org.apache.cassandra.tcm.MultiStepOperation.Kind.MOVE; import static org.apache.cassandra.tcm.MultiStepOperation.Kind.MOVE;
import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_MOVE; import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_MOVE;
@ -86,6 +98,7 @@ import static org.apache.cassandra.tcm.Transformation.Kind.MID_MOVE;
import static org.apache.cassandra.tcm.Transformation.Kind.START_MOVE; import static org.apache.cassandra.tcm.Transformation.Kind.START_MOVE;
import static org.apache.cassandra.tcm.sequences.SequenceState.continuable; import static org.apache.cassandra.tcm.sequences.SequenceState.continuable;
import static org.apache.cassandra.tcm.sequences.SequenceState.error; import static org.apache.cassandra.tcm.sequences.SequenceState.error;
import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition;
public class Move extends MultiStepOperation<Epoch> public class Move extends MultiStepOperation<Epoch>
{ {
@ -196,6 +209,44 @@ public class Move extends MultiStepOperation<Epoch>
return applyMultipleTransformations(metadata, next, of(startMove, midMove, finishMove)); return applyMultipleTransformations(metadata, next, of(startMove, midMove, finishMove));
} }
static class WaitForEpochAndRangeRetirement implements TopologyListener
{
final Condition condition = newOneTimeCondition();
final long waitingForEpoch;
final Ranges waitingForRanges;
Ranges retiredRanges;
public WaitForEpochAndRangeRetirement(long waitingForEpoch, Ranges waitingForRanges)
{
this.waitingForEpoch = waitingForEpoch;
this.waitingForRanges = waitingForRanges;
this.retiredRanges = Ranges.EMPTY;
}
synchronized void updateRetiredRanges(Ranges ranges)
{
ranges = ranges.slice(waitingForRanges, Minimal).without(retiredRanges);
if (!ranges.isEmpty())
{
retiredRanges = retiredRanges.union(MERGE_ADJACENT, ranges);
if (retiredRanges.containsAll(waitingForRanges))
condition.signal();
}
}
@Override
public synchronized void onEpochRetired(Ranges ranges, long epoch, @Nullable Topology topology)
{
if (epoch >= waitingForEpoch)
updateRetiredRanges(ranges);
}
public void waitForRetirement()
{
condition.awaitThrowUncheckedOnInterrupt();
}
}
@Override @Override
public Set<NodeId> affectedPeers(Directory directory) public Set<NodeId> affectedPeers(Directory directory)
{ {
@ -212,9 +263,38 @@ public class Move extends MultiStepOperation<Epoch>
switch (next) switch (next)
{ {
case START_MOVE: case START_MOVE:
WaitForEpochAndRangeRetirement wait = null;
try try
{ {
ClusterMetadata metadata = ClusterMetadata.current(); ClusterMetadata metadata = ClusterMetadata.current();
if (AccordService.isStarted() && metadata.schema.hasAccordKeyspaces())
{
TopologyManager topologyManager = AccordService.instance().topology();
AccordService.toFuture(topologyManager.await(metadata.epoch.getEpoch(), null))
.awaitThrowUncheckedOnInterrupt().rethrowIfFailed();
Topology current = topologyManager.active().globalForEpoch(metadata.epoch.getEpoch());
RegainingEpochRange regaining = topologyManager.computeRegaining(current, AccordTopology.createAccordTopology(applyTo(metadata).success().metadata));
if (regaining != null)
{
wait = new WaitForEpochAndRangeRetirement(regaining.epoch(), regaining.ranges());
topologyManager.addListener(wait);
ActiveEpoch e = topologyManager.active().ifExists(regaining.epoch());
// We have already checked that our activeEpochs is caught up to metadata.epoch.getEpoch()
// which is greater than regaining.epoch() so if e == null then the only case we can fall
// in is that regaining.epoch() is already retired
if (e != null)
{
wait.updateRetiredRanges(e.retired());
logger.info("Waiting for previous ownership of ranges {} to retire before regaining", regaining.ranges());
wait.waitForRetirement();
logger.info("Previously owned ranges {} now retired", regaining.ranges());
}
}
}
logger.info("Moving {} from {} to {}.", logger.info("Moving {} from {} to {}.",
metadata.directory.endpoint(startMove.nodeId()), metadata.directory.endpoint(startMove.nodeId()),
metadata.tokenMap.tokens(startMove.nodeId()), metadata.tokenMap.tokens(startMove.nodeId()),
@ -226,6 +306,11 @@ public class Move extends MultiStepOperation<Epoch>
JVMStabilityInspector.inspectThrowable(t); JVMStabilityInspector.inspectThrowable(t);
return continuable(); return continuable();
} }
finally
{
if (wait != null)
AccordService.instance().topology().removeListener(wait);
}
break; break;
case MID_MOVE: case MID_MOVE:
try try

View File

@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.test.accord;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.Util;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.AccordService;
import static com.google.common.collect.Iterables.getOnlyElement;
public class AccordDeleteCommandStoreTest extends AccordTestBase
{
private static final Logger logger = LoggerFactory.getLogger(AccordDeleteCommandStoreTest.class);
@Override
protected Logger logger()
{
return logger;
}
@BeforeClass
public static void setupClass() throws IOException
{
AccordTestBase.setupCluster(builder -> builder
.withoutVNodes()
.withConfig(config ->
config
.set("accord.shard_durability_cycle", "20s")
.set("accord.topology_sync_propagator_enabled_pre_start", true)
.with(Feature.NETWORK, Feature.GOSSIP)), 4);
}
@Test
public void deleteCommandStoresTest() throws Throwable
{
List<String> ddls = Arrays.asList("DROP KEYSPACE IF EXISTS " + KEYSPACE + ';',
"CREATE KEYSPACE " + KEYSPACE + " WITH REPLICATION={'class':'SimpleStrategy', 'replication_factor': 2}",
"CREATE TABLE " + qualifiedAccordTableName + " (k int PRIMARY KEY, v int) WITH transactional_mode='full'");
test(ddls, cluster -> {
String newToken = cluster.get(1).callOnInstance(() -> getOnlyElement(StorageService.instance.getTokens()));
int numberOfCommandStores = cluster.get(2).callOnInstance(() -> {
Util.spinUntilTrue(() -> AccordService.instance().node().commandStores().all().length > 0);
return AccordService.instance().node().commandStores().all().length;
});
cluster.get(2).runOnInstance(() -> {
StorageService.instance.move(Long.toString(Long.parseLong(newToken) + 1));
Util.spinUntilTrue(() -> AccordService.instance().node().commandStores().all().length < numberOfCommandStores,
60);
});
});
}
}

View File

@ -0,0 +1,138 @@
/*
* 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 java.io.IOException;
import java.util.Arrays;
import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.local.CommandStore;
import accord.local.CommandStores.PreviouslyOwned;
import accord.local.PreLoadContext;
import accord.primitives.AbstractRanges;
import accord.primitives.Ranges;
import accord.topology.ActiveEpochs;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.SimpleQueryResult;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.TokenRange;
import org.apache.cassandra.service.accord.api.TokenKey;
import static com.google.common.collect.Iterables.getOnlyElement;
import static org.apache.cassandra.service.accord.AccordService.getBlocking;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class AccordRegainRangesTest extends AccordTestBase
{
private static final Logger logger = LoggerFactory.getLogger(AccordRegainRangesTest.class);
@Override
protected Logger logger()
{
return logger;
}
@BeforeClass
public static void setupClass() throws IOException
{
AccordTestBase.setupCluster(builder -> builder
.withoutVNodes()
.withConfig(config ->
config
.set("accord.shard_durability_target_splits", "1")
.set("accord.shard_durability_cycle", "20s")
.with(Feature.NETWORK, Feature.GOSSIP)), 6);
}
@Test
public void regainRangesTest() throws Throwable
{
List<String> ddls = Arrays.asList("DROP KEYSPACE IF EXISTS " + KEYSPACE + ';',
"CREATE KEYSPACE " + KEYSPACE + " WITH REPLICATION={'class':'SimpleStrategy', 'replication_factor': 3}",
"CREATE TABLE " + qualifiedAccordTableName + " (k int PRIMARY KEY, v int) WITH transactional_mode='full'");
test(ddls, cluster -> {
cluster.coordinator(2).execute(wrapInTxn("INSERT INTO " + qualifiedAccordTableName + " (k, v) VALUES (?, ?)"), ConsistencyLevel.SERIAL, 1, 2);
SimpleQueryResult result = cluster.coordinator(2).executeWithResult("SELECT token(k) FROM " + qualifiedAccordTableName + " WHERE k = 1 LIMIT 1", ConsistencyLevel.SERIAL);
String originalToken = cluster.get(2).callOnInstance(() -> getOnlyElement(StorageService.instance.getTokens()));
long token = (Long) result.toObjectArrays()[0][0];
assertTrue(token < Long.parseLong(originalToken));
// Loses range (-4069959284402364209,-3074457345618258603]
cluster.get(2).runOnInstance(() -> {
StorageService.instance.move(Long.toString(token - 1000));
});
// Regains range (-4069959284402364209,-3074457345618258603]
cluster.get(2).runOnInstance(() -> {
StorageService.instance.move(originalToken);
});
String tableName = accordTableName;
// Ensure no overlapping safeToRead ranges
cluster.get(2).runOnInstance(() -> {
TableId tid = Schema.instance.getTableMetadata(KEYSPACE, tableName).id();
TokenKey start = TokenKey.parse(tid, String.valueOf(token), Murmur3Partitioner.instance);
TokenKey end = TokenKey.parse(tid, originalToken, Murmur3Partitioner.instance);
Ranges regainedRange = Ranges.of(TokenRange.create(start, end));
ActiveEpochs activeEpochs = AccordService.instance().topology().active();
PreviouslyOwned previouslyOwned = AccordService.instance().node().commandStores().getPreviouslyOwnedFromSnapshot();
assertEquals(previouslyOwned.regains(regainedRange), regainedRange);
for (int i = 0; i < previouslyOwned.size(); i++)
{
long epoch = previouslyOwned.epochs(i);
if (!activeEpochs.hasAtLeastEpoch(epoch))
continue;
assertTrue(activeEpochs.getKnown(epoch).retired().containsAll(previouslyOwned.ranges(i)));
}
Ranges range = Ranges.EMPTY;
for (CommandStore commandStore : AccordService.instance().node().commandStores().all()) {
Ranges safeToReadRanges = getBlocking(commandStore.submit((PreLoadContext.Empty) () -> "No overlapping safeToReadRanges", safeCommandStore -> {
Ranges mergedRanges = Ranges.EMPTY;
for (Ranges r : safeCommandStore.safeToReadAt().values())
mergedRanges = mergedRanges.union(AbstractRanges.UnionMode.MERGE_ADJACENT, r);
return mergedRanges;
}));
assertTrue(range.overlapping(safeToReadRanges).isEmpty());
range = range.union(AbstractRanges.UnionMode.MERGE_ADJACENT, safeToReadRanges);
}
});
});
}
}

View File

@ -92,10 +92,12 @@ public class AccordJournalCompactionTest
NavigableMap<Timestamp, Ranges> safeToReadAtAccumulator = ImmutableSortedMap.of(Timestamp.NONE, Ranges.EMPTY); NavigableMap<Timestamp, Ranges> safeToReadAtAccumulator = ImmutableSortedMap.of(Timestamp.NONE, Ranges.EMPTY);
NavigableMap<TxnId, Ranges> bootstrapBeganAtAccumulator = ImmutableSortedMap.of(TxnId.NONE, Ranges.EMPTY); NavigableMap<TxnId, Ranges> bootstrapBeganAtAccumulator = ImmutableSortedMap.of(TxnId.NONE, Ranges.EMPTY);
RangesForEpoch rangesForEpochAccumulator = null; RangesForEpoch rangesForEpochAccumulator = null;
Ranges permanentlyUnsafeToReadAccumulator = Ranges.EMPTY;
Gen<DurableBefore> durableBeforeGen = AccordGenerators.durableBeforeGen(DatabaseDescriptor.getPartitioner()); Gen<DurableBefore> durableBeforeGen = AccordGenerators.durableBeforeGen(DatabaseDescriptor.getPartitioner());
Gen<NavigableMap<Timestamp, Ranges>> safeToReadGen = AccordGenerators.safeToReadGen(DatabaseDescriptor.getPartitioner()); Gen<NavigableMap<Timestamp, Ranges>> safeToReadGen = AccordGenerators.safeToReadGen(DatabaseDescriptor.getPartitioner());
Gen<RangesForEpoch> rangesForEpochGen = AccordGenerators.rangesForEpoch(DatabaseDescriptor.getPartitioner()); Gen<RangesForEpoch> rangesForEpochGen = AccordGenerators.rangesForEpoch(DatabaseDescriptor.getPartitioner());
Gen<Ranges> permanentlyUnsafeToRead = AccordGenerators.ranges(DatabaseDescriptor.getPartitioner());
AccordJournal journal = new AccordJournal(new TestParams() AccordJournal journal = new AccordJournal(new TestParams()
{ {
@ -129,6 +131,7 @@ public class AccordJournalCompactionTest
// updates.addRedundantBefore = redundantBeforeGen.next(rs); // updates.addRedundantBefore = redundantBeforeGen.next(rs);
updates.newSafeToRead = safeToReadGen.next(rs); updates.newSafeToRead = safeToReadGen.next(rs);
updates.newRangesForEpoch = rangesForEpochGen.next(rs); updates.newRangesForEpoch = rangesForEpochGen.next(rs);
updates.newPermanentlyUnsafeToRead = permanentlyUnsafeToRead.next(rs);
journal.durableBeforePersister().persist(addDurableBefore, null); journal.durableBeforePersister().persist(addDurableBefore, null);
journal.saveStoreState(1, updates, null); journal.saveStoreState(1, updates, null);
@ -141,6 +144,8 @@ public class AccordJournalCompactionTest
safeToReadAtAccumulator = updates.newSafeToRead; safeToReadAtAccumulator = updates.newSafeToRead;
if (updates.newRangesForEpoch != null) if (updates.newRangesForEpoch != null)
rangesForEpochAccumulator = updates.newRangesForEpoch; rangesForEpochAccumulator = updates.newRangesForEpoch;
if (updates.newPermanentlyUnsafeToRead != null)
permanentlyUnsafeToReadAccumulator = updates.newPermanentlyUnsafeToRead;
if (i % 100 == 0) if (i % 100 == 0)
journal.closeCurrentSegmentForTestingIfNonEmpty(); journal.closeCurrentSegmentForTestingIfNonEmpty();
@ -153,6 +158,7 @@ public class AccordJournalCompactionTest
Assert.assertEquals(bootstrapBeganAtAccumulator, journal.loadBootstrapBeganAt(1)); Assert.assertEquals(bootstrapBeganAtAccumulator, journal.loadBootstrapBeganAt(1));
Assert.assertEquals(safeToReadAtAccumulator, journal.loadSafeToRead(1)); Assert.assertEquals(safeToReadAtAccumulator, journal.loadSafeToRead(1));
Assert.assertEquals(rangesForEpochAccumulator, journal.loadRangesForEpoch(1)); Assert.assertEquals(rangesForEpochAccumulator, journal.loadRangesForEpoch(1));
Assert.assertEquals(permanentlyUnsafeToReadAccumulator, journal.loadPermanentlyUnsafeToRead(1));
} }
finally finally
{ {

View File

@ -40,6 +40,7 @@ import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.utils.AccordGenerators; import org.apache.cassandra.utils.AccordGenerators;
import static accord.utils.Property.qt; import static accord.utils.Property.qt;
import static org.apache.cassandra.service.accord.serializers.CommandStoreSerializersTest.previouslyOwnedGen;
import static org.apache.cassandra.service.accord.serializers.CommandStoreSerializersTest.rangesForEpochGen; import static org.apache.cassandra.service.accord.serializers.CommandStoreSerializersTest.rangesForEpochGen;
public class TopologyRecordTest public class TopologyRecordTest
@ -85,14 +86,13 @@ public class TopologyRecordTest
Gen<Ranges> rangesGen = AccordGenerators.ranges(TBL1, partitioner); Gen<Ranges> rangesGen = AccordGenerators.ranges(TBL1, partitioner);
Gen<CommandStores.RangesForEpoch> rangesForEpochGen = rangesForEpochGen(rangesGen); Gen<CommandStores.RangesForEpoch> rangesForEpochGen = rangesForEpochGen(rangesGen);
Topology topology = AccordGenerators.topologyGen(rangesGen).next(rs); Topology topology = AccordGenerators.topologyGen(rangesGen).next(rs);
CommandStores.PreviouslyOwned previouslyOwned = previouslyOwnedGen(rangesGen).next(rs);
Int2ObjectHashMap<CommandStores.RangesForEpoch> commandStores = new Int2ObjectHashMap<>(); Int2ObjectHashMap<CommandStores.RangesForEpoch> commandStores = new Int2ObjectHashMap<>();
for (Node.Id node : topology.nodes()) for (Node.Id node : topology.nodes())
commandStores.put(node.id, rangesForEpochGen.next(rs)); commandStores.put(node.id, rangesForEpochGen.next(rs));
Node.Id self = rs.pick(topology.nodes()); return new Journal.TopologyUpdate(commandStores, topology, previouslyOwned);
return new Journal.TopologyUpdate(commandStores, topology);
}; };
} }
@ -120,8 +120,8 @@ public class TopologyRecordTest
private static void maybeUpdatePartitioner(TopologyRecord expected) private static void maybeUpdatePartitioner(TopologyRecord expected)
{ {
Journal.TopologyUpdate update = expected.getUpdate(); Journal.TopologyUpdate update = expected.update();
if (update != null) if (update != null)
maybeUpdatePartitioner(expected.getUpdate()); maybeUpdatePartitioner(expected.update());
} }
} }

View File

@ -127,6 +127,27 @@ public class CommandStoreSerializersTest
}; };
} }
public static Gen<CommandStores.PreviouslyOwned> previouslyOwnedGen(Gen<Ranges> rangesGen)
{
Gen.IntGen sizeGen = Gens.ints().between(0, 10);
Gen.LongGen epochGen = AccordGens.epochs();
return rs -> {
int size = sizeGen.nextInt(rs);
if (size == 0)
return CommandStores.PreviouslyOwned.EMPTY;
long maxEpoch = 0;
long[] epochs = new long[size];
Ranges[] ranges = new Ranges[size];
for (int i = 0; i < size; i++)
{
epochs[i] = epochGen.nextLong(rs);
ranges[i] = rangesGen.next(rs);
maxEpoch = Math.max(maxEpoch, epochs[i]);
}
return new CommandStores.PreviouslyOwned(maxEpoch, epochs, ranges);
};
}
private void maybeUpdatePartitioner(CommandStores.RangesForEpoch expected) private void maybeUpdatePartitioner(CommandStores.RangesForEpoch expected)
{ {
if (expected.size() > 0) if (expected.size() > 0)