mirror of https://github.com/apache/cassandra
CEP-15: (Accord) When nodes are removed from a cluster, need to update topology tracking to avoid being blocked
patch by David Capwell; reviewed by Blake Eggleston for CASSANDRA-19719
This commit is contained in:
parent
ac58a62c51
commit
0d4c0961be
|
|
@ -1289,13 +1289,12 @@
|
|||
<condition property="maxMemory" value="8G" else="4G">
|
||||
<equals arg1="${test.classlistprefix}" arg2="distributed"/>
|
||||
</condition>
|
||||
<testmacrohelper inputdir="${test.dir}/${test.classlistprefix}" filelist="@{test.file.list}" exclude="**/*.java" timeout="${test.timeout}">
|
||||
<testmacrohelper inputdir="${test.dir}/${test.classlistprefix}" filelist="@{test.file.list}" exclude="**/*.java" timeout="${test.timeout}" maxmemory="${maxMemory}">
|
||||
<jvmarg value="-Dlegacy-sstable-root=${test.data}/legacy-sstables"/>
|
||||
<jvmarg value="-Dinvalid-legacy-sstable-root=${test.data}/invalid-legacy-sstables"/>
|
||||
<jvmarg value="-Dcassandra.ring_delay_ms=1000"/>
|
||||
<jvmarg value="-Dcassandra.tolerate_sstable_size=true"/>
|
||||
<jvmarg value="-Dcassandra.skip_sync=true" />
|
||||
<jvmarg value="-Xmx${maxMemory}"/>
|
||||
</testmacrohelper>
|
||||
</sequential>
|
||||
</macrodef>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f1f5ea5ccbd6e0a8abf579a4331fa84a1b3d9f95
|
||||
Subproject commit 694ae39e2e00075bdabd47632dced0db12a9981d
|
||||
|
|
@ -129,7 +129,7 @@ public class RepairCoordinator implements Runnable, ProgressEventNotifier, Repai
|
|||
{
|
||||
this.ctx = ctx;
|
||||
this.validationScheduler = Scheduler.build(DatabaseDescriptor.getConcurrentMerkleTreeRequests());
|
||||
this.state = new CoordinatorState(ctx.clock(), cmd, keyspace, options);
|
||||
this.state = new CoordinatorState(ctx, cmd, keyspace, options);
|
||||
this.tag = "repair:" + cmd;
|
||||
this.validColumnFamilies = validColumnFamilies;
|
||||
this.getLocalReplicas = getLocalReplicas;
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ public class RepairSession extends AsyncFuture<RepairSessionResult> implements I
|
|||
this.paxosOnly = paxosOnly;
|
||||
this.isConsensusMigration = isConsensusMigration;
|
||||
assert cfnames.length > 0 : "Repairing no column families seems pointless, doesn't it";
|
||||
this.state = new SessionState(ctx.clock(), parentRepairSession, keyspace, cfnames, commonRange);
|
||||
this.state = new SessionState(ctx, parentRepairSession, keyspace, cfnames, commonRange);
|
||||
this.parallelismDegree = parallelismDegree;
|
||||
this.isIncremental = isIncremental;
|
||||
this.previewKind = previewKind;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import org.apache.cassandra.streaming.StreamPlan;
|
|||
import org.apache.cassandra.utils.Clock;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.MBeanWrapper;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
|
||||
/**
|
||||
* Access methods to shared resources and services.
|
||||
|
|
@ -81,6 +82,10 @@ public interface SharedContext
|
|||
TableRepairManager repairManager(ColumnFamilyStore store);
|
||||
StreamExecutor streamExecutor();
|
||||
PaxosRepairState paxosRepairState();
|
||||
default Supplier<TimeUUID> timeUUID()
|
||||
{
|
||||
return TimeUUID.Generator::nextTimeUUID;
|
||||
}
|
||||
|
||||
class Global implements SharedContext
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,12 +30,10 @@ import org.apache.cassandra.db.ColumnFamilyStore;
|
|||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.repair.CommonRange;
|
||||
import org.apache.cassandra.repair.RepairCoordinator;
|
||||
import org.apache.cassandra.repair.SharedContext;
|
||||
import org.apache.cassandra.repair.messages.RepairOption;
|
||||
import org.apache.cassandra.utils.Clock;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
|
||||
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
|
||||
|
||||
public class CoordinatorState extends AbstractState<CoordinatorState.State, TimeUUID>
|
||||
{
|
||||
public enum State
|
||||
|
|
@ -56,9 +54,9 @@ public class CoordinatorState extends AbstractState<CoordinatorState.State, Time
|
|||
// API to split function calls for phase changes from getting the state
|
||||
public final Phase phase = new Phase();
|
||||
|
||||
public CoordinatorState(Clock clock, int cmd, String keyspace, RepairOption options)
|
||||
public CoordinatorState(SharedContext ctx, int cmd, String keyspace, RepairOption options)
|
||||
{
|
||||
super(clock, nextTimeUUID(), State.class);
|
||||
super(ctx.clock(), ctx.timeUUID().get(), State.class);
|
||||
this.cmd = cmd;
|
||||
this.keyspace = Objects.requireNonNull(keyspace);
|
||||
this.options = Objects.requireNonNull(options);
|
||||
|
|
|
|||
|
|
@ -26,11 +26,9 @@ import java.util.stream.Collectors;
|
|||
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.repair.CommonRange;
|
||||
import org.apache.cassandra.utils.Clock;
|
||||
import org.apache.cassandra.repair.SharedContext;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
|
||||
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
|
||||
|
||||
public class SessionState extends AbstractState<SessionState.State, TimeUUID>
|
||||
{
|
||||
public enum State
|
||||
|
|
@ -46,9 +44,9 @@ public class SessionState extends AbstractState<SessionState.State, TimeUUID>
|
|||
|
||||
public final Phase phase = new Phase();
|
||||
|
||||
public SessionState(Clock clock, TimeUUID parentRepairSession, String keyspace, String[] cfnames, CommonRange commonRange)
|
||||
public SessionState(SharedContext ctx, TimeUUID parentRepairSession, String keyspace, String[] cfnames, CommonRange commonRange)
|
||||
{
|
||||
super(clock, nextTimeUUID(), State.class);
|
||||
super(ctx.clock(), ctx.timeUUID().get(), State.class);
|
||||
this.parentRepairSession = parentRepairSession;
|
||||
this.keyspace = keyspace;
|
||||
this.cfnames = cfnames;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import accord.topology.Topology;
|
|||
import accord.utils.Invariants;
|
||||
import accord.utils.async.AsyncResult;
|
||||
import accord.utils.async.AsyncResults;
|
||||
import org.agrona.collections.LongArrayList;
|
||||
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
|
||||
import org.apache.cassandra.concurrent.ScheduledExecutors;
|
||||
import org.apache.cassandra.concurrent.Shutdownable;
|
||||
import org.apache.cassandra.concurrent.Stage;
|
||||
|
|
@ -57,6 +59,7 @@ import static org.apache.cassandra.utils.Simulate.With.MONITORS;
|
|||
public class AccordConfigurationService extends AbstractConfigurationService<AccordConfigurationService.EpochState, AccordConfigurationService.EpochHistory> implements ChangeListener, AccordEndpointMapper, AccordSyncPropagator.Listener, Shutdownable
|
||||
{
|
||||
private final AccordSyncPropagator syncPropagator;
|
||||
private final DiskStateManager diskStateManager;
|
||||
|
||||
private EpochDiskState diskState = EpochDiskState.EMPTY;
|
||||
|
||||
|
|
@ -114,15 +117,88 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
}
|
||||
}
|
||||
|
||||
public AccordConfigurationService(Node.Id node, MessageDelivery messagingService, IFailureDetector failureDetector)
|
||||
@VisibleForTesting
|
||||
interface DiskStateManager
|
||||
{
|
||||
EpochDiskState loadTopologies(AccordKeyspace.TopologyLoadConsumer consumer);
|
||||
EpochDiskState setNotifyingLocalSync(long epoch, Set<Node.Id> pending, EpochDiskState diskState);
|
||||
|
||||
EpochDiskState setCompletedLocalSync(long epoch, EpochDiskState diskState);
|
||||
|
||||
EpochDiskState markLocalSyncAck(Node.Id id, long epoch, EpochDiskState diskState);
|
||||
|
||||
EpochDiskState saveTopology(Topology topology, EpochDiskState diskState);
|
||||
|
||||
EpochDiskState markRemoteTopologySync(Node.Id node, long epoch, EpochDiskState diskState);
|
||||
|
||||
EpochDiskState markClosed(Ranges ranges, long epoch, EpochDiskState diskState);
|
||||
|
||||
EpochDiskState truncateTopologyUntil(long epoch, EpochDiskState diskState);
|
||||
}
|
||||
|
||||
enum SystemTableDiskStateManager implements DiskStateManager
|
||||
{
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public EpochDiskState loadTopologies(AccordKeyspace.TopologyLoadConsumer consumer)
|
||||
{
|
||||
return AccordKeyspace.loadTopologies(consumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpochDiskState setNotifyingLocalSync(long epoch, Set<Node.Id> notify, EpochDiskState diskState)
|
||||
{
|
||||
return AccordKeyspace.setNotifyingLocalSync(epoch, notify, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpochDiskState setCompletedLocalSync(long epoch, EpochDiskState diskState)
|
||||
{
|
||||
return AccordKeyspace.setCompletedLocalSync(epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpochDiskState markLocalSyncAck(Node.Id id, long epoch, EpochDiskState diskState)
|
||||
{
|
||||
return AccordKeyspace.markLocalSyncAck(id, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpochDiskState saveTopology(Topology topology, EpochDiskState diskState)
|
||||
{
|
||||
return AccordKeyspace.saveTopology(topology, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpochDiskState markRemoteTopologySync(Node.Id node, long epoch, EpochDiskState diskState)
|
||||
{
|
||||
return AccordKeyspace.markRemoteTopologySync(node, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpochDiskState markClosed(Ranges ranges, long epoch, EpochDiskState diskState)
|
||||
{
|
||||
return AccordKeyspace.markClosed(ranges, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpochDiskState truncateTopologyUntil(long epoch, EpochDiskState diskState)
|
||||
{
|
||||
return AccordKeyspace.truncateTopologyUntil(epoch, diskState);
|
||||
}
|
||||
}
|
||||
|
||||
public AccordConfigurationService(Node.Id node, MessageDelivery messagingService, IFailureDetector failureDetector, DiskStateManager diskStateManager, ScheduledExecutorPlus scheduledTasks)
|
||||
{
|
||||
super(node);
|
||||
this.syncPropagator = new AccordSyncPropagator(localId, this, messagingService, failureDetector, ScheduledExecutors.scheduledTasks, this);
|
||||
this.syncPropagator = new AccordSyncPropagator(localId, this, messagingService, failureDetector, scheduledTasks, this);
|
||||
this.diskStateManager = diskStateManager;
|
||||
}
|
||||
|
||||
public AccordConfigurationService(Node.Id node)
|
||||
{
|
||||
this(node, MessagingService.instance(), FailureDetector.instance);
|
||||
this(node, MessagingService.instance(), FailureDetector.instance, SystemTableDiskStateManager.instance, ScheduledExecutors.scheduledTasks);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -137,7 +213,7 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
state = State.LOADING;
|
||||
updateMapping(ClusterMetadata.current());
|
||||
EndpointMapping snapshot = mapping;
|
||||
diskState = AccordKeyspace.loadTopologies(((epoch, topology, syncStatus, pendingSyncNotify, remoteSyncComplete, closed, redundant) -> {
|
||||
diskState = diskStateManager.loadTopologies(((epoch, topology, syncStatus, pendingSyncNotify, remoteSyncComplete, closed, redundant) -> {
|
||||
if (topology != null)
|
||||
reportTopology(topology, syncStatus == SyncStatus.NOT_STARTED);
|
||||
|
||||
|
|
@ -221,12 +297,41 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
synchronized (AccordConfigurationService.this)
|
||||
{
|
||||
updateMapping(metadata);
|
||||
reportTopology(AccordTopology.createAccordTopology(metadata));
|
||||
Topology topology = AccordTopology.createAccordTopology(metadata);
|
||||
Topology current = isEmpty() ? Topology.EMPTY : currentTopology();
|
||||
reportTopology(topology);
|
||||
Sets.SetView<Node.Id> removedNodes = Sets.difference(current.nodes(), topology.nodes());
|
||||
if (!removedNodes.isEmpty())
|
||||
onNodesRemoved(topology.epoch(), removedNodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void maybeReportMetadata(ClusterMetadata metadata)
|
||||
private synchronized void onNodesRemoved(long epoch, Set<Node.Id> removed)
|
||||
{
|
||||
syncPropagator.onNodesRemoved(removed);
|
||||
for (long oldEpoch : nonCompletedEpochsBefore(epoch))
|
||||
{
|
||||
for (Node.Id node : removed)
|
||||
receiveRemoteSyncComplete(node, oldEpoch);
|
||||
}
|
||||
listeners.forEach(l -> l.onRemoveNodes(epoch, removed));
|
||||
}
|
||||
|
||||
private long[] nonCompletedEpochsBefore(long max)
|
||||
{
|
||||
LongArrayList notComplete = new LongArrayList();
|
||||
for (long epoch = epochs.minEpoch(); epoch <= max && epoch <= epochs.maxEpoch(); epoch++)
|
||||
{
|
||||
EpochSnapshot snapshot = getEpochSnapshot(epoch);
|
||||
if (snapshot.syncStatus != SyncStatus.COMPLETED)
|
||||
notComplete.add(epoch);
|
||||
}
|
||||
return notComplete.toLongArray();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void maybeReportMetadata(ClusterMetadata metadata)
|
||||
{
|
||||
// don't report metadata until the previous one has been acknowledged
|
||||
synchronized (this)
|
||||
|
|
@ -265,7 +370,7 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
return;
|
||||
|
||||
Set<Node.Id> notify = topology.nodes().stream().filter(i -> !localId.equals(i)).collect(Collectors.toSet());
|
||||
diskState = AccordKeyspace.setNotifyingLocalSync(epoch, notify, diskState);
|
||||
diskState = diskStateManager.setNotifyingLocalSync(epoch, notify, diskState);
|
||||
epochState.setSyncStatus(SyncStatus.NOTIFYING);
|
||||
syncPropagator.reportSyncComplete(epoch, notify, localId);
|
||||
}
|
||||
|
|
@ -276,7 +381,7 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
EpochState epochState = getOrCreateEpochState(epoch);
|
||||
if (epochState.syncStatus != SyncStatus.NOTIFYING)
|
||||
return;
|
||||
diskState = AccordKeyspace.markLocalSyncAck(id, epoch, diskState);
|
||||
diskState = diskStateManager.markLocalSyncAck(id, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -284,21 +389,21 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
{
|
||||
EpochState epochState = getOrCreateEpochState(epoch);
|
||||
epochState.setSyncStatus(SyncStatus.COMPLETED);
|
||||
diskState = AccordKeyspace.setCompletedLocalSync(epoch, diskState);
|
||||
diskState = diskStateManager.setCompletedLocalSync(epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void topologyUpdatePreListenerNotify(Topology topology)
|
||||
{
|
||||
if (state == State.STARTED)
|
||||
diskState = AccordKeyspace.saveTopology(topology, diskState);
|
||||
diskState = diskStateManager.saveTopology(topology, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void receiveRemoteSyncCompletePreListenerNotify(Node.Id node, long epoch)
|
||||
{
|
||||
if (state == State.STARTED)
|
||||
diskState = AccordKeyspace.markRemoteTopologySync(node, epoch, diskState);
|
||||
diskState = diskStateManager.markRemoteTopologySync(node, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -309,6 +414,11 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
syncPropagator.reportClosed(epoch, topology.nodes(), ranges);
|
||||
}
|
||||
|
||||
public AccordSyncPropagator syncPropagator()
|
||||
{
|
||||
return syncPropagator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reportEpochRedundant(Ranges ranges, long epoch)
|
||||
{
|
||||
|
|
@ -321,14 +431,14 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
@Override
|
||||
public synchronized void receiveClosed(Ranges ranges, long epoch)
|
||||
{
|
||||
diskState = AccordKeyspace.markClosed(ranges, epoch, diskState);
|
||||
diskState = diskStateManager.markClosed(ranges, epoch, diskState);
|
||||
super.receiveClosed(ranges, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void receiveRedundant(Ranges ranges, long epoch)
|
||||
{
|
||||
diskState = AccordKeyspace.markClosed(ranges, epoch, diskState);
|
||||
diskState = diskStateManager.markClosed(ranges, epoch, diskState);
|
||||
super.receiveRedundant(ranges, epoch);
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +452,7 @@ public class AccordConfigurationService extends AbstractConfigurationService<Acc
|
|||
protected synchronized void truncateTopologiesPostListenerNotify(long epoch)
|
||||
{
|
||||
if (state == State.STARTED)
|
||||
diskState = AccordKeyspace.truncateTopologyUntil(epoch, diskState);
|
||||
diskState = diskStateManager.truncateTopologyUntil(epoch, diskState);
|
||||
}
|
||||
|
||||
private void checkStarted()
|
||||
|
|
|
|||
|
|
@ -1587,7 +1587,8 @@ public class AccordKeyspace
|
|||
return minEpoch == maxEpoch && maxEpoch == 0;
|
||||
}
|
||||
|
||||
private EpochDiskState withNewMaxEpoch(long epoch)
|
||||
@VisibleForTesting
|
||||
EpochDiskState withNewMaxEpoch(long epoch)
|
||||
{
|
||||
Invariants.checkArgument(epoch > maxEpoch, "Epoch %d <= %d (max)", epoch, maxEpoch);
|
||||
return EpochDiskState.create(Math.max(1, minEpoch), epoch);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
package org.apache.cassandra.service.accord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
|
@ -29,6 +30,9 @@ import java.util.concurrent.TimeUnit;
|
|||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import accord.local.Node;
|
||||
import accord.messages.SimpleReply;
|
||||
import accord.primitives.Ranges;
|
||||
|
|
@ -38,6 +42,7 @@ import org.agrona.collections.Long2ObjectHashMap;
|
|||
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
|
||||
import org.apache.cassandra.db.TypeSizes;
|
||||
import org.apache.cassandra.exceptions.RequestFailure;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.gms.IFailureDetector;
|
||||
import org.apache.cassandra.io.IVersionedSerializer;
|
||||
import org.apache.cassandra.io.util.DataInputPlus;
|
||||
|
|
@ -50,6 +55,7 @@ import org.apache.cassandra.net.RequestCallback;
|
|||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.service.accord.serializers.KeySerializers;
|
||||
import org.apache.cassandra.service.accord.serializers.TopologySerializers;
|
||||
import org.apache.cassandra.tcm.ClusterMetadata;
|
||||
import org.apache.cassandra.utils.CollectionSerializers;
|
||||
|
||||
import static org.apache.cassandra.utils.CollectionSerializers.newListSerializer;
|
||||
|
|
@ -59,6 +65,8 @@ import static org.apache.cassandra.utils.CollectionSerializers.newListSerializer
|
|||
*/
|
||||
public class AccordSyncPropagator
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(AccordSyncPropagator.class);
|
||||
|
||||
public static final IVerbHandler<List<Notification>> verbHandler = message -> {
|
||||
if (!AccordService.isSetup())
|
||||
return;
|
||||
|
|
@ -120,6 +128,11 @@ public class AccordSyncPropagator
|
|||
return new Notification(epoch, Collections.emptySet(), Ranges.EMPTY, addRedundant);
|
||||
}
|
||||
|
||||
boolean isEmpty()
|
||||
{
|
||||
return syncComplete.isEmpty() && closed.isEmpty() && redundant.isEmpty();
|
||||
}
|
||||
|
||||
boolean ack(Notification notification)
|
||||
{
|
||||
if (!notification.syncComplete.isEmpty())
|
||||
|
|
@ -201,6 +214,15 @@ public class AccordSyncPropagator
|
|||
return !pending.isEmpty();
|
||||
}
|
||||
|
||||
synchronized boolean hasPending(long epoch)
|
||||
{
|
||||
if (pending.isEmpty()) return false;
|
||||
return pending.values().stream().allMatch(n -> {
|
||||
PendingEpoch p = n.get(epoch);
|
||||
return p != null && !p.isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
@ -210,6 +232,28 @@ public class AccordSyncPropagator
|
|||
'}';
|
||||
}
|
||||
|
||||
public synchronized void onNodesRemoved(Set<Node.Id> removed)
|
||||
{
|
||||
for (Node.Id node : removed)
|
||||
{
|
||||
PendingEpochs pendingEpochs = pending.get(node.id);
|
||||
if (pendingEpochs == null) continue;
|
||||
long[] toComplete = new long[pendingEpochs.size()];
|
||||
Long2ObjectHashMap<PendingEpoch>.KeyIterator it = pendingEpochs.keySet().iterator();
|
||||
for (int i = 0; it.hasNext(); i++)
|
||||
toComplete[i] = it.nextLong();
|
||||
Arrays.sort(toComplete);
|
||||
for (long epoch : toComplete)
|
||||
listener.onEndpointAck(node, epoch);
|
||||
pending.remove(node.id);
|
||||
for (long epoch : toComplete)
|
||||
{
|
||||
if (hasSyncCompletedFor(epoch))
|
||||
listener.onComplete(epoch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reportSyncComplete(long epoch, Collection<Node.Id> notify, Node.Id syncCompleteId)
|
||||
{
|
||||
if (notify.isEmpty())
|
||||
|
|
@ -258,17 +302,13 @@ public class AccordSyncPropagator
|
|||
private boolean notify(Node.Id to, List<Notification> notifications)
|
||||
{
|
||||
InetAddressAndPort toEp = endpointMapper.mappedEndpoint(to);
|
||||
if (!failureDetector.isAlive(toEp))
|
||||
{
|
||||
scheduler.schedule(() -> notify(to, notifications), 1, TimeUnit.MINUTES);
|
||||
return false;
|
||||
}
|
||||
Message<List<Notification>> msg = Message.out(Verb.ACCORD_SYNC_NOTIFY_REQ, notifications);
|
||||
messagingService.sendWithCallback(msg, toEp, new RequestCallback<SimpleReply>(){
|
||||
RequestCallback<SimpleReply> cb = new RequestCallback<>()
|
||||
{
|
||||
@Override
|
||||
public void onResponse(Message<SimpleReply> msg)
|
||||
{
|
||||
Invariants.checkState(msg.payload == SimpleReply.Ok, "Unexpected message: %s", msg);
|
||||
Invariants.checkState(msg.payload == SimpleReply.Ok, "Unexpected message: %s", msg);
|
||||
Set<Long> completedEpochs = new HashSet<>();
|
||||
// TODO review is it a good idea to call the listener while not holding the `AccordSyncPropagator` lock?
|
||||
synchronized (AccordSyncPropagator.this)
|
||||
|
|
@ -304,7 +344,22 @@ public class AccordSyncPropagator
|
|||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
if (!failureDetector.isAlive(toEp))
|
||||
{
|
||||
// was the endpoint removed from membership?
|
||||
ClusterMetadata metadata = ClusterMetadata.current();
|
||||
if (Gossiper.instance.getEndpointStateForEndpoint(toEp) == null && !metadata.directory.allJoinedEndpoints().contains(toEp) && !metadata.fullCMSMembers().contains(toEp))
|
||||
{
|
||||
// endpoint no longer exists...
|
||||
cb.onResponse(msg.responseWith(SimpleReply.Ok));
|
||||
return true;
|
||||
}
|
||||
logger.warn("Node{} is not alive, unable to notify of {}", to, notifications);
|
||||
scheduler.schedule(() -> notify(to, notifications), 1, TimeUnit.MINUTES);
|
||||
return false;
|
||||
}
|
||||
messagingService.sendWithCallback(msg, toEp, cb);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -352,5 +407,16 @@ public class AccordSyncPropagator
|
|||
this.closed = closed;
|
||||
this.redundant = redundant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "Notification{" +
|
||||
"epoch=" + epoch +
|
||||
", syncComplete=" + syncComplete +
|
||||
", closed=" + closed +
|
||||
", redundant=" + redundant +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ public class SimulatedExecutorFactory implements ExecutorFactory, Clock
|
|||
|
||||
public SimulatedExecutorFactory(RandomSource rs, Consumer<Throwable> onError)
|
||||
{
|
||||
this(rs, toGen(Generators.TIMESTAMP_GEN.map(Timestamp::getTime)).mapToLong(TimeUnit.MILLISECONDS::toNanos).next(rs), onError);
|
||||
this(rs, toGen(Generators.TIMESTAMP_GEN.map(Timestamp::getTime)).mapToLong(TimeUnit.MILLISECONDS::toNanos).nextLong(rs), onError);
|
||||
}
|
||||
|
||||
public SimulatedExecutorFactory(RandomSource rs)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import org.apache.cassandra.locator.InetAddressAndPort;
|
|||
import org.apache.cassandra.repair.CommonRange;
|
||||
import org.apache.cassandra.repair.RepairJobDesc;
|
||||
import org.apache.cassandra.repair.RepairCoordinator;
|
||||
import org.apache.cassandra.repair.SharedContext;
|
||||
import org.apache.cassandra.repair.messages.PrepareMessage;
|
||||
import org.apache.cassandra.repair.messages.RepairOption;
|
||||
import org.apache.cassandra.repair.state.Completable;
|
||||
|
|
@ -291,7 +292,7 @@ public class LocalRepairTablesTest extends CQLTester
|
|||
private static CoordinatorState coordinator()
|
||||
{
|
||||
RepairOption options = RepairOption.parse(Collections.emptyMap(), DatabaseDescriptor.getPartitioner());
|
||||
CoordinatorState state = new CoordinatorState(Clock.Global.clock(), 0, "test", options);
|
||||
CoordinatorState state = new CoordinatorState(SharedContext.Global.instance, 0, "test", options);
|
||||
ActiveRepairService.instance().register(state);
|
||||
return state;
|
||||
}
|
||||
|
|
@ -299,7 +300,7 @@ public class LocalRepairTablesTest extends CQLTester
|
|||
private static SessionState session()
|
||||
{
|
||||
CoordinatorState parent = coordinator();
|
||||
SessionState state = new SessionState(Clock.Global.clock(), parent.id, REPAIR_KS, new String[]{ REPAIR_TABLE }, COMMON_RANGE);
|
||||
SessionState state = new SessionState(SharedContext.Global.instance, parent.id, REPAIR_KS, new String[]{ REPAIR_TABLE }, COMMON_RANGE);
|
||||
parent.register(state);
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,11 +257,11 @@ public class AccordIndexStressTest extends CQLTester
|
|||
{
|
||||
case Key:
|
||||
{
|
||||
store = rs.pick(storeToTableToRoutingKeysToTxns.keySet());
|
||||
store = rs.pickUnorderedSet(storeToTableToRoutingKeysToTxns.keySet());
|
||||
var actual = this.storeToTableToRoutingKeysToTxns.get(store);
|
||||
var tableToTokens = store2Table2Tokens.get(store);
|
||||
|
||||
table = rs.pick(actual.keySet());
|
||||
table = rs.pickUnorderedSet(actual.keySet());
|
||||
var tokens = tableToTokens.get(table);
|
||||
|
||||
var offset = rs.nextInt(0, tokens.length);
|
||||
|
|
@ -274,11 +274,11 @@ public class AccordIndexStressTest extends CQLTester
|
|||
break;
|
||||
case Range:
|
||||
{
|
||||
store = rs.pick(storeToTableToRangesToTxns.keySet());
|
||||
store = rs.pickUnorderedSet(storeToTableToRangesToTxns.keySet());
|
||||
var tableToRangesToTxns = storeToTableToRangesToTxns.get(store);
|
||||
var tableToRanges = store2Table2Ranges.get(store);
|
||||
|
||||
table = rs.pick(tableToRangesToTxns.keySet());
|
||||
table = rs.pickUnorderedSet(tableToRangesToTxns.keySet());
|
||||
var wrapper = tableToRanges.get(table);
|
||||
var ranges = wrapper.ranges;
|
||||
var tree = wrapper.tree;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
|
@ -125,7 +126,7 @@ public class RouteIndexTest extends CQLTester.InMemory
|
|||
@Override
|
||||
public Gen<Command<State, ColumnFamilyStore, ?>> commands(State state)
|
||||
{
|
||||
Map<Gen<Command<State, ColumnFamilyStore, ?>>, Integer> possible = new HashMap<>();
|
||||
Map<Gen<Command<State, ColumnFamilyStore, ?>>, Integer> possible = new LinkedHashMap<>();
|
||||
possible.put(ignore -> FLUSH, 1);
|
||||
possible.put(ignore -> COMPACT, 1);
|
||||
possible.put(rs -> {
|
||||
|
|
@ -139,9 +140,9 @@ public class RouteIndexTest extends CQLTester.InMemory
|
|||
if (!state.storeToTableToRangesToTxns.isEmpty())
|
||||
{
|
||||
possible.put(rs -> {
|
||||
int storeId = rs.pick(state.storeToTableToRangesToTxns.keySet());
|
||||
int storeId = rs.pickUnorderedSet(state.storeToTableToRangesToTxns.keySet());
|
||||
var tables = state.storeToTableToRangesToTxns.get(storeId);
|
||||
TableId tableId = rs.pick(tables.keySet());
|
||||
TableId tableId = rs.pickUnorderedSet(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;
|
||||
|
|
@ -154,14 +155,14 @@ public class RouteIndexTest extends CQLTester.InMemory
|
|||
switch (rs.nextInt(0, 2))
|
||||
{
|
||||
case 0: // perfect match
|
||||
range = rs.pick(distinctRanges);
|
||||
range = rs.pickOrderedSet(distinctRanges);
|
||||
break;
|
||||
case 1: // mutli-match
|
||||
{
|
||||
TokenRange a = rs.pick(distinctRanges);
|
||||
TokenRange b = rs.pick(distinctRanges);
|
||||
TokenRange a = rs.pickOrderedSet(distinctRanges);
|
||||
TokenRange b = rs.pickOrderedSet(distinctRanges);
|
||||
while (a.equals(b))
|
||||
b = rs.pick(distinctRanges);
|
||||
b = rs.pickOrderedSet(distinctRanges);
|
||||
if (b.compareTo(a) < 0)
|
||||
{
|
||||
TokenRange tmp = a;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import javax.annotation.Nullable;
|
|||
|
||||
import accord.utilsfork.Gens;
|
||||
import accord.utilsfork.RandomSource;
|
||||
|
||||
import org.apache.cassandra.exceptions.RequestFailure;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.utils.concurrent.AsyncPromise;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import java.io.IOException;
|
|||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
|
@ -44,7 +44,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
|
@ -85,7 +84,6 @@ import org.apache.cassandra.dht.Murmur3Partitioner;
|
|||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper;
|
||||
import org.apache.cassandra.exceptions.RequestFailure;
|
||||
import org.apache.cassandra.gms.ApplicationState;
|
||||
import org.apache.cassandra.gms.EndpointState;
|
||||
import org.apache.cassandra.gms.HeartBeatState;
|
||||
|
|
@ -99,12 +97,12 @@ import org.apache.cassandra.locator.InetAddressAndPort;
|
|||
import org.apache.cassandra.locator.LocalStrategy;
|
||||
import org.apache.cassandra.locator.Locator;
|
||||
import org.apache.cassandra.locator.RangesAtEndpoint;
|
||||
import org.apache.cassandra.net.ConnectionType;
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessageDelivery;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.RequestCallback;
|
||||
import org.apache.cassandra.net.SimulatedMessageDelivery;
|
||||
import org.apache.cassandra.net.SimulatedMessageDelivery.SimulatedMessageReceiver;
|
||||
import org.apache.cassandra.repair.messages.RepairMessage;
|
||||
import org.apache.cassandra.repair.messages.RepairOption;
|
||||
import org.apache.cassandra.repair.messages.ValidationResponse;
|
||||
|
|
@ -151,8 +149,6 @@ import org.apache.cassandra.utils.MerkleTree;
|
|||
import org.apache.cassandra.utils.MerkleTrees;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
import org.apache.cassandra.utils.concurrent.AsyncPromise;
|
||||
import org.apache.cassandra.utils.concurrent.Future;
|
||||
import org.apache.cassandra.utils.concurrent.ImmediateFuture;
|
||||
import org.apache.cassandra.utils.progress.ProgressEventType;
|
||||
import org.assertj.core.api.Assertions;
|
||||
|
|
@ -166,10 +162,10 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE
|
|||
public abstract class FuzzTestBase extends CQLTester.InMemory
|
||||
{
|
||||
private static final int MISMATCH_NUM_PARTITIONS = 1;
|
||||
private static final Gen<String> IDENTIFIER_GEN = fromQT(Generators.IDENTIFIER_GEN);
|
||||
private static final Gen<String> KEYSPACE_NAME_GEN = fromQT(CassandraGenerators.KEYSPACE_NAME_GEN);
|
||||
private static final Gen<TableId> TABLE_ID_GEN = fromQT(CassandraGenerators.TABLE_ID_GEN);
|
||||
private static final Gen<InetAddressAndPort> ADDRESS_W_PORT = fromQT(CassandraGenerators.INET_ADDRESS_AND_PORT_GEN);
|
||||
private static final Gen<String> IDENTIFIER_GEN = Generators.toGen(Generators.IDENTIFIER_GEN);
|
||||
private static final Gen<String> KEYSPACE_NAME_GEN = Generators.toGen(CassandraGenerators.KEYSPACE_NAME_GEN);
|
||||
private static final Gen<TableId> TABLE_ID_GEN = Generators.toGen(CassandraGenerators.TABLE_ID_GEN);
|
||||
private static final Gen<InetAddressAndPort> ADDRESS_W_PORT = Generators.toGen(CassandraGenerators.INET_ADDRESS_AND_PORT_GEN);
|
||||
|
||||
private static boolean SETUP_SCHEMA = false;
|
||||
static String KEYSPACE;
|
||||
|
|
@ -494,7 +490,7 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
Set<Token> allTokens = new HashSet<>();
|
||||
for (Range<Token> range : validator.desc.ranges)
|
||||
{
|
||||
Gen<Token> gen = fromQT(CassandraGenerators.tokensInRange(range));
|
||||
Gen<Token> gen = Generators.toGen(CassandraGenerators.tokensInRange(range));
|
||||
Set<Token> tokens = new LinkedHashSet<>();
|
||||
for (int i = 0, size = rs.nextInt(1, 10); i < size; i++)
|
||||
{
|
||||
|
|
@ -691,15 +687,13 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
private final List<MessageListener> listeners = new ArrayList<>();
|
||||
private final RandomSource rs;
|
||||
private BiFunction<Node, Message<?>, Set<Faults>> allowedMessageFaults = (a, b) -> Collections.emptySet();
|
||||
|
||||
private final Map<Connection, LongSupplier> networkLatencies = new HashMap<>();
|
||||
private final Map<Connection, Supplier<Boolean>> networkDrops = new HashMap<>();
|
||||
|
||||
Cluster(RandomSource rs)
|
||||
{
|
||||
ClockAccess.includeThreadAsOwner();
|
||||
this.rs = rs;
|
||||
globalExecutor = new SimulatedExecutorFactory(rs, fromQT(Generators.TIMESTAMP_GEN.map(Timestamp::getTime)).mapToLong(TimeUnit.MILLISECONDS::toNanos).next(rs));
|
||||
globalExecutor = new SimulatedExecutorFactory(rs);
|
||||
orderedExecutor = globalExecutor.configureSequential("ignore").build();
|
||||
unorderedScheduled = globalExecutor.scheduled("ignored");
|
||||
|
||||
|
|
@ -720,8 +714,8 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
int numNodes = rs.nextInt(3, 10);
|
||||
List<String> dcs = Gens.lists(IDENTIFIER_GEN).unique().ofSizeBetween(1, Math.min(10, numNodes)).next(rs);
|
||||
Map<InetAddressAndPort, Node> nodes = Maps.newHashMapWithExpectedSize(numNodes);
|
||||
Gen<Token> tokenGen = fromQT(CassandraGenerators.token(DatabaseDescriptor.getPartitioner()));
|
||||
Gen<UUID> hostIdGen = fromQT(Generators.UUID_RANDOM_GEN);
|
||||
Gen<Token> tokenGen = Generators.toGen(CassandraGenerators.token(DatabaseDescriptor.getPartitioner()));
|
||||
Gen<UUID> hostIdGen = Generators.toGen(Generators.UUID_RANDOM_GEN);
|
||||
Set<Token> tokens = new HashSet<>();
|
||||
Set<UUID> hostIds = new HashSet<>();
|
||||
for (int i = 0; i < numNodes; i++)
|
||||
|
|
@ -813,214 +807,43 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
}
|
||||
}
|
||||
|
||||
private class CallbackContext
|
||||
private SimulatedMessageDelivery.Action action(InetAddressAndPort self, Message<?> msg, InetAddressAndPort to)
|
||||
{
|
||||
final RequestCallback callback;
|
||||
|
||||
private CallbackContext(RequestCallback callback)
|
||||
{
|
||||
this.callback = Objects.requireNonNull(callback);
|
||||
}
|
||||
|
||||
public void onResponse(Message msg)
|
||||
{
|
||||
callback.onResponse(msg);
|
||||
}
|
||||
|
||||
public void onFailure(InetAddressAndPort from, RequestFailure failure)
|
||||
{
|
||||
if (callback.invokeOnFailure()) callback.onFailure(from, failure);
|
||||
}
|
||||
boolean toSelf = self.equals(to);
|
||||
Node node = nodes.get(to);
|
||||
Set<Faults> allowedFaults = allowedMessageFaults.apply(node, msg);
|
||||
if (allowedFaults.contains(Faults.DROP) && !toSelf && networkDrops(self, to)) return SimulatedMessageDelivery.Action.DROP_PARTITIONED;
|
||||
return SimulatedMessageDelivery.Action.DELIVER;
|
||||
}
|
||||
|
||||
private static class CallbackKey
|
||||
private boolean networkDrops(InetAddressAndPort self, InetAddressAndPort to)
|
||||
{
|
||||
private final long id;
|
||||
private final InetAddressAndPort peer;
|
||||
|
||||
private CallbackKey(long id, InetAddressAndPort peer)
|
||||
{
|
||||
this.id = id;
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CallbackKey that = (CallbackKey) o;
|
||||
return id == that.id && peer.equals(that.peer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(id, peer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "CallbackKey{" +
|
||||
"id=" + id +
|
||||
", peer=" + peer +
|
||||
'}';
|
||||
}
|
||||
return networkDrops.computeIfAbsent(new Connection(self, to), ignore -> Gens.bools().biasedRepeatingRuns(rs.nextInt(1, 11) / 100.0D, rs.nextInt(3, 15)).asSupplier(rs)).get();
|
||||
}
|
||||
|
||||
private class Messaging implements MessageDelivery
|
||||
private class Messaging extends SimulatedMessageDelivery
|
||||
{
|
||||
final InetAddressAndPort broadcastAddressAndPort;
|
||||
final Map<CallbackKey, CallbackContext> callbacks = new HashMap<>();
|
||||
|
||||
private Messaging(InetAddressAndPort broadcastAddressAndPort)
|
||||
{
|
||||
this.broadcastAddressAndPort = broadcastAddressAndPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <REQ> void send(Message<REQ> message, InetAddressAndPort to)
|
||||
{
|
||||
message = message.withFrom(broadcastAddressAndPort);
|
||||
maybeEnqueue(message, to, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <REQ, RSP> void sendWithCallback(Message<REQ> message, InetAddressAndPort to, RequestCallback<RSP> cb)
|
||||
{
|
||||
message = message.withFrom(broadcastAddressAndPort);
|
||||
maybeEnqueue(message, to, cb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <REQ, RSP> void sendWithCallback(Message<REQ> message, InetAddressAndPort to, RequestCallback<RSP> cb, ConnectionType specifyConnection)
|
||||
{
|
||||
message = message.withFrom(broadcastAddressAndPort);
|
||||
maybeEnqueue(message, to, cb);
|
||||
}
|
||||
|
||||
private <REQ, RSP> void maybeEnqueue(Message<REQ> message, InetAddressAndPort to, @Nullable RequestCallback<RSP> callback)
|
||||
{
|
||||
CallbackContext cb;
|
||||
if (callback != null)
|
||||
{
|
||||
CallbackKey key = new CallbackKey(message.id(), to);
|
||||
if (callbacks.containsKey(key))
|
||||
throw new AssertionError("Message id " + message.id() + " to " + to + " already has a callback");
|
||||
cb = new CallbackContext(callback);
|
||||
callbacks.put(key, cb);
|
||||
}
|
||||
else
|
||||
{
|
||||
cb = null;
|
||||
}
|
||||
boolean toSelf = this.broadcastAddressAndPort.equals(to);
|
||||
Node node = nodes.get(to);
|
||||
Set<Faults> allowedFaults = allowedMessageFaults.apply(node, message);
|
||||
if (allowedFaults.isEmpty())
|
||||
{
|
||||
// enqueue so stack overflow doesn't happen with the inlining
|
||||
unorderedScheduled.submit(() -> node.handle(message));
|
||||
}
|
||||
else
|
||||
{
|
||||
Runnable enqueue = () -> {
|
||||
if (!allowedFaults.contains(Faults.DELAY))
|
||||
{
|
||||
unorderedScheduled.submit(() -> node.handle(message));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (toSelf) unorderedScheduled.submit(() -> node.handle(message));
|
||||
else
|
||||
unorderedScheduled.schedule(() -> node.handle(message), networkJitterNanos(to), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
};
|
||||
|
||||
if (!allowedFaults.contains(Faults.DROP)) enqueue.run();
|
||||
else
|
||||
{
|
||||
if (!toSelf && networkDrops(to))
|
||||
{
|
||||
// logger.warn("Dropped message {}", message);
|
||||
// drop
|
||||
}
|
||||
else
|
||||
{
|
||||
enqueue.run();
|
||||
}
|
||||
}
|
||||
|
||||
if (cb != null)
|
||||
{
|
||||
unorderedScheduled.schedule(() -> {
|
||||
CallbackContext ctx = callbacks.remove(new CallbackKey(message.id(), to));
|
||||
if (ctx != null)
|
||||
{
|
||||
assert ctx == cb;
|
||||
try
|
||||
{
|
||||
ctx.onFailure(to, RequestFailure.TIMEOUT);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
failures.add(t);
|
||||
}
|
||||
}
|
||||
}, message.verb().expiresAfterNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long networkJitterNanos(InetAddressAndPort to)
|
||||
{
|
||||
return networkLatencies.computeIfAbsent(new Connection(broadcastAddressAndPort, to), ignore -> {
|
||||
long min = TimeUnit.MICROSECONDS.toNanos(500);
|
||||
long maxSmall = TimeUnit.MILLISECONDS.toNanos(5);
|
||||
long max = TimeUnit.SECONDS.toNanos(5);
|
||||
LongSupplier small = () -> rs.nextLong(min, maxSmall);
|
||||
LongSupplier large = () -> rs.nextLong(maxSmall, max);
|
||||
return Gens.bools().biasedRepeatingRuns(rs.nextInt(1, 11) / 100.0D, rs.nextInt(3, 15)).mapToLong(b -> b ? large.getAsLong() : small.getAsLong()).asLongSupplier(rs);
|
||||
}).getAsLong();
|
||||
}
|
||||
|
||||
private boolean networkDrops(InetAddressAndPort to)
|
||||
{
|
||||
return networkDrops.computeIfAbsent(new Connection(broadcastAddressAndPort, to), ignore -> Gens.bools().biasedRepeatingRuns(rs.nextInt(1, 11) / 100.0D, rs.nextInt(3, 15)).asSupplier(rs)).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <REQ, RSP> Future<Message<RSP>> sendWithResult(Message<REQ> message, InetAddressAndPort to)
|
||||
{
|
||||
AsyncPromise<Message<RSP>> promise = new AsyncPromise<>();
|
||||
sendWithCallback(message, to, new RequestCallback<RSP>()
|
||||
{
|
||||
@Override
|
||||
public void onResponse(Message<RSP> msg)
|
||||
{
|
||||
promise.trySuccess(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(InetAddressAndPort from, RequestFailure failure)
|
||||
{
|
||||
promise.tryFailure(new MessagingService.FailureResponseException(from, failure));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invokeOnFailure()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> void respond(V response, Message<?> message)
|
||||
{
|
||||
send(message.responseWith(response), message.respondTo());
|
||||
super(broadcastAddressAndPort,
|
||||
Cluster.this::action,
|
||||
new NetworkDelaySupplier()
|
||||
{
|
||||
private final NetworkDelaySupplier delegate = SimulatedMessageDelivery.randomDelay(rs);
|
||||
@Nullable
|
||||
@Override
|
||||
public Duration jitter(Message<?> msg, InetAddressAndPort to)
|
||||
{
|
||||
Set<Faults> allowedFaults = allowedMessageFaults.apply(nodes.get(to), msg);
|
||||
if (!allowedFaults.contains(Faults.DELAY) || broadcastAddressAndPort.equals(to))
|
||||
return null;
|
||||
return delegate.jitter(msg, to);
|
||||
}
|
||||
},
|
||||
(to, msg) -> unorderedScheduled.submit(() -> nodes.get(to).handle(msg)),
|
||||
(action, to, msg) -> logger.warn("{} message {}", action, msg),
|
||||
unorderedScheduled::schedule,
|
||||
failures::add);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1070,7 +893,7 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
final InetAddressAndPort addressAndPort;
|
||||
final Collection<Token> tokens;
|
||||
final ActiveRepairService activeRepairService;
|
||||
final IVerbHandler verbHandler;
|
||||
final SimulatedMessageReceiver receiver;
|
||||
final Messaging messaging;
|
||||
final IValidationManager validationManager;
|
||||
private FailingBiConsumer<ColumnFamilyStore, Validator> doValidation = DEFAULT_VALIDATION;
|
||||
|
|
@ -1104,7 +927,7 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
validator.fail(e);
|
||||
}
|
||||
});
|
||||
this.verbHandler = new IVerbHandler<>()
|
||||
this.receiver = messaging.receiver(new IVerbHandler<>()
|
||||
{
|
||||
private final RepairMessageVerbHandler repairVerbHandler = new RepairMessageVerbHandler(Node.this);
|
||||
private final IVerbHandler<PaxosStartPrepareCleanup.Request> paxosStartPrepareCleanup = PaxosStartPrepareCleanup.createVerbHandler(Node.this);
|
||||
|
|
@ -1136,7 +959,7 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
repairVerbHandler.doVerb(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
activeRepairService.start();
|
||||
}
|
||||
|
|
@ -1176,38 +999,7 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
}
|
||||
for (MessageListener l : listeners)
|
||||
l.preHandle(this, msg);
|
||||
if (msg.verb().isResponse())
|
||||
{
|
||||
// handle callbacks
|
||||
CallbackKey key = new CallbackKey(msg.id(), msg.from());
|
||||
if (messaging.callbacks.containsKey(key))
|
||||
{
|
||||
CallbackContext callback = messaging.callbacks.remove(key);
|
||||
if (callback == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (msg.isFailureResponse())
|
||||
callback.onFailure(msg.from(), (RequestFailure) msg.payload);
|
||||
else callback.onResponse(msg);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
failures.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
verbHandler.doVerb(msg);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
failures.add(e);
|
||||
}
|
||||
}
|
||||
receiver.recieve(msg);
|
||||
}
|
||||
|
||||
public UUID hostId()
|
||||
|
|
@ -1362,6 +1154,12 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
return paxosRepairState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<TimeUUID> timeUUID()
|
||||
{
|
||||
return Generators.toGen(Generators.timeUUID()).asSupplier(rs);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return "Node{" +
|
||||
|
|
@ -1388,14 +1186,6 @@ public abstract class FuzzTestBase extends CQLTester.InMemory
|
|||
}
|
||||
}
|
||||
|
||||
private static <T> Gen<T> fromQT(org.quicktheories.core.Gen<T> qt)
|
||||
{
|
||||
return rs -> {
|
||||
JavaRandom r = new JavaRandom(rs.asJdkRandom());
|
||||
return qt.generate(r);
|
||||
};
|
||||
}
|
||||
|
||||
public static class HackStrat extends LocalStrategy
|
||||
{
|
||||
public HackStrat(String keyspaceName, Map<String, String> configOptions)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import accord.topology.Shard;
|
|||
import accord.topology.Topology;
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.ServerTestUtils;
|
||||
import org.apache.cassandra.concurrent.ScheduledExecutors;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
|
|
@ -176,7 +177,7 @@ public class AccordConfigurationServiceTest
|
|||
@Test
|
||||
public void initialEpochTest() throws Throwable
|
||||
{
|
||||
AccordConfigurationService service = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector());
|
||||
AccordConfigurationService service = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector(), AccordConfigurationService.SystemTableDiskStateManager.instance, ScheduledExecutors.scheduledTasks);
|
||||
Assert.assertEquals(null, AccordKeyspace.loadEpochDiskState());
|
||||
service.start();
|
||||
Assert.assertEquals(null, AccordKeyspace.loadEpochDiskState());
|
||||
|
|
@ -201,7 +202,7 @@ public class AccordConfigurationServiceTest
|
|||
@Test
|
||||
public void loadTest() throws Throwable
|
||||
{
|
||||
AccordConfigurationService service = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector());
|
||||
AccordConfigurationService service = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector(), AccordConfigurationService.SystemTableDiskStateManager.instance, ScheduledExecutors.scheduledTasks);
|
||||
service.start();
|
||||
|
||||
Topology topology1 = new Topology(1, new Shard(AccordTopology.fullRange(TBL1), ID_LIST, ID_SET));
|
||||
|
|
@ -221,7 +222,7 @@ public class AccordConfigurationServiceTest
|
|||
service.reportTopology(topology3);
|
||||
service.acknowledgeEpoch(EpochReady.done(3), true);
|
||||
|
||||
AccordConfigurationService loaded = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector());
|
||||
AccordConfigurationService loaded = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector(), AccordConfigurationService.SystemTableDiskStateManager.instance, ScheduledExecutors.scheduledTasks);
|
||||
loaded.updateMapping(mappingForEpoch(ClusterMetadata.current().epoch.getEpoch() + 1));
|
||||
AbstractConfigurationServiceTest.TestListener listener = new AbstractConfigurationServiceTest.TestListener(loaded, true);
|
||||
loaded.registerListener(listener);
|
||||
|
|
@ -240,7 +241,7 @@ public class AccordConfigurationServiceTest
|
|||
@Test
|
||||
public void truncateTest()
|
||||
{
|
||||
AccordConfigurationService service = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector());
|
||||
AccordConfigurationService service = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector(), AccordConfigurationService.SystemTableDiskStateManager.instance, ScheduledExecutors.scheduledTasks);
|
||||
TestListener serviceListener = new TestListener(service, true);
|
||||
service.registerListener(serviceListener);
|
||||
service.start();
|
||||
|
|
@ -258,7 +259,7 @@ public class AccordConfigurationServiceTest
|
|||
Assert.assertEquals(EpochDiskState.create(3), service.diskState());
|
||||
serviceListener.assertTruncates(3L);
|
||||
|
||||
AccordConfigurationService loaded = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector());
|
||||
AccordConfigurationService loaded = new AccordConfigurationService(ID1, new Messaging(), new MockFailureDetector(), AccordConfigurationService.SystemTableDiskStateManager.instance, ScheduledExecutors.scheduledTasks);
|
||||
loaded.updateMapping(mappingForEpoch(ClusterMetadata.current().epoch.getEpoch() + 1));
|
||||
TestListener loadListener = new TestListener(loaded, true);
|
||||
loaded.registerListener(loadListener);
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ public class AccordKeyspaceTest extends CQLTester.InMemory
|
|||
// else this will loop forever...
|
||||
for (int attempt = 0; attempt < 10; attempt++)
|
||||
{
|
||||
TableId tableId = rs.pick(tables.keySet());
|
||||
TableId tableId = rs.pickOrderedSet(tables.navigableKeySet());
|
||||
IPartitioner partitioner = tables.get(tableId);
|
||||
ByteBuffer data = !(partitioner instanceof LocalPartitioner) ? Int32Type.instance.decompose(rs.nextInt())
|
||||
: fromQT(getTypeSupport(partitioner.getTokenValidator()).bytesGen()).next(rs);
|
||||
|
|
@ -258,7 +258,7 @@ public class AccordKeyspaceTest extends CQLTester.InMemory
|
|||
|
||||
for (int i = 0, queries = rs.nextInt(1, 5); i < queries; i++)
|
||||
{
|
||||
int store = rs.pick(storesToKeys.keySet());
|
||||
int store = rs.pickOrderedSet(storesToKeys.navigableKeySet());
|
||||
var keysForStore = new ArrayList<>(storesToKeys.get(store));
|
||||
|
||||
int offset;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
|
|||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
import org.apache.cassandra.exceptions.RequestFailure;
|
||||
import org.apache.cassandra.gms.EndpointState;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.gms.HeartBeatState;
|
||||
import org.apache.cassandra.gms.IFailureDetectionEventListener;
|
||||
import org.apache.cassandra.gms.IFailureDetector;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
|
|
@ -94,6 +97,10 @@ public class AccordSyncPropagatorTest
|
|||
Gen<Ranges> rangesGen = AccordGenerators.ranges().filter(r -> !r.isEmpty());
|
||||
Gen<List<Node.Id>> nodesGen = Gens.lists(AccordGens.nodes()).unique().ofSizeBetween(1, 40);
|
||||
qt().withExamples(100).check(rs -> {
|
||||
// when gossip and cluster metadata don't know an endpoint, retries are avoided (node removed)
|
||||
// so when instances are created here they are added to gossip to trick the membership check...
|
||||
Gossiper.instance.clearUnsafe();
|
||||
|
||||
List<Node.Id> nodes = nodesGen.next(rs);
|
||||
Set<Node.Id> nodesAsSet = ImmutableSet.copyOf(nodes);
|
||||
|
||||
|
|
@ -214,6 +221,7 @@ public class AccordSyncPropagatorTest
|
|||
Sink sink = new Sink(id);
|
||||
IFailureDetector fd = new FailureDetector(address);
|
||||
instances.put(id, new Instace(id, address, cs, sink, fd, cs, new AccordSyncPropagator(id, Cluster.this, sink, fd, scheduler, cs)));
|
||||
Gossiper.instance.endpointStateMap.put(address, new EndpointState(HeartBeatState.empty()));
|
||||
}
|
||||
this.nodeToAddress = nodeToAddress.build();
|
||||
this.instances = instances.build();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,754 @@
|
|||
/*
|
||||
* 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.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import accord.api.ConfigurationService;
|
||||
import accord.api.ConfigurationService.EpochReady;
|
||||
import accord.impl.SizeOfIntersectionSorter;
|
||||
import accord.local.Node;
|
||||
import accord.primitives.Ranges;
|
||||
import accord.topology.Topology;
|
||||
import accord.topology.TopologyManager;
|
||||
import accord.utils.Gen;
|
||||
import accord.utils.Gens;
|
||||
import accord.utils.Invariants;
|
||||
import accord.utils.Property.Command;
|
||||
import accord.utils.Property.Commands;
|
||||
import accord.utils.Property.UnitCommand;
|
||||
import accord.utils.RandomSource;
|
||||
import accord.utils.async.AsyncChain;
|
||||
import accord.utils.async.AsyncChains;
|
||||
import accord.utils.async.AsyncResult;
|
||||
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
|
||||
import org.apache.cassandra.concurrent.SimulatedExecutorFactory;
|
||||
import org.apache.cassandra.concurrent.Stage;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
|
||||
import org.apache.cassandra.gms.IFailureDetectionEventListener;
|
||||
import org.apache.cassandra.gms.IFailureDetector;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.locator.Replica;
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.SimulatedMessageDelivery;
|
||||
import org.apache.cassandra.net.SimulatedMessageDelivery.Action;
|
||||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.schema.DistributedMetadataLogKeyspace;
|
||||
import org.apache.cassandra.schema.DistributedSchema;
|
||||
import org.apache.cassandra.schema.KeyspaceMetadata;
|
||||
import org.apache.cassandra.schema.KeyspaceParams;
|
||||
import org.apache.cassandra.schema.Keyspaces;
|
||||
import org.apache.cassandra.schema.ReplicationParams;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.schema.TableParams;
|
||||
import org.apache.cassandra.schema.Tables;
|
||||
import org.apache.cassandra.service.accord.AccordConfigurationService.EpochSnapshot;
|
||||
import org.apache.cassandra.service.consensus.TransactionalMode;
|
||||
import org.apache.cassandra.tcm.ClusterMetadata;
|
||||
import org.apache.cassandra.tcm.ClusterMetadataService;
|
||||
import org.apache.cassandra.tcm.Epoch;
|
||||
import org.apache.cassandra.tcm.StubClusterMetadataService;
|
||||
import org.apache.cassandra.tcm.membership.Directory;
|
||||
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.utils.ByteArrayUtil;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
import org.assertj.core.api.Assertions;
|
||||
|
||||
import static accord.utils.Property.stateful;
|
||||
|
||||
public class EpochSyncTest
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(EpochSyncTest.class);
|
||||
|
||||
static
|
||||
{
|
||||
DatabaseDescriptor.clientInitialization();
|
||||
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
|
||||
|
||||
ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test()
|
||||
{
|
||||
stateful().withExamples(50).check(new Commands<Cluster, Void>()
|
||||
{
|
||||
@Override
|
||||
public Gen<Cluster> genInitialState()
|
||||
{
|
||||
return Cluster::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void createSut(Cluster Cluster)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Gen<Command<Cluster, Void, ?>> commands(Cluster cluster)
|
||||
{
|
||||
List<Node.Id> alive = cluster.alive();
|
||||
Map<Gen<Command<Cluster, Void, ?>>, Integer> possible = new LinkedHashMap<>();
|
||||
if (alive.size() < cluster.maxNodes)
|
||||
{
|
||||
// add node
|
||||
possible.put(rs -> {
|
||||
Node.Id id = new Node.Id(++cluster.nodeCounter);
|
||||
long token = cluster.tokenGen.nextLong(rs);
|
||||
while (cluster.tokens.contains(token))
|
||||
token = cluster.tokenGen.nextLong(rs);
|
||||
long epoch = cluster.current.epoch.getEpoch() + 1;
|
||||
long finalToken = token;
|
||||
return new SimpleCommand("Add Node " + id + "; token=" + token + ", epoch=" + epoch,
|
||||
c -> c.addNode(id, finalToken));
|
||||
}, 5);
|
||||
}
|
||||
if (alive.size() > cluster.minNodes)
|
||||
{
|
||||
possible.put(rs -> {
|
||||
Node.Id pick = rs.pick(alive);
|
||||
long token = cluster.instances.get(pick).token;
|
||||
long epoch = cluster.current.epoch.getEpoch() + 1;
|
||||
return new SimpleCommand("Remove Node " + pick + "; token=" + token + "; epoch=" + epoch, c -> c.removeNode(pick));
|
||||
}, 3);
|
||||
}
|
||||
if (cluster.hasWork())
|
||||
{
|
||||
possible.put(rs -> new SimpleCommand("Process Some",
|
||||
c -> {//noinspection StatementWithEmptyBody
|
||||
for (int i = 0, attempts = rs.nextInt(1, 100); i < attempts && c.processOne(); i++)
|
||||
{
|
||||
}
|
||||
}), 10);
|
||||
}
|
||||
|
||||
possible.put(rs -> new SimpleCommand("Validate",
|
||||
c -> c.validate(false)), 1);
|
||||
possible.put(rs -> new SimpleCommand("Bump Epoch " + (cluster.current.epoch.getEpoch() + 1),
|
||||
Cluster::bumpEpoch), 10);
|
||||
return Gens.oneOf(possible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyState(Cluster cluster)
|
||||
{
|
||||
cluster.processAll();
|
||||
cluster.validate(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class SimpleCommand implements UnitCommand<Cluster, Void>
|
||||
{
|
||||
private final String name;
|
||||
private final Consumer<Cluster> fn;
|
||||
|
||||
private SimpleCommand(String name, Consumer<Cluster> fn)
|
||||
{
|
||||
this.name = name;
|
||||
this.fn = fn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String detailed(Cluster Cluster)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyUnit(Cluster Cluster)
|
||||
{
|
||||
fn.accept(Cluster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runUnit(Void Void)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static class Cluster
|
||||
{
|
||||
private static final int rf = 2;
|
||||
private static final ReplicationParams replication_params = ReplicationParams.simple(rf);
|
||||
private static final ReplicationParams meta = ReplicationParams.simpleMeta(1, Collections.singleton("dc1"));
|
||||
|
||||
private final RandomSource rs;
|
||||
private final int minNodes, maxNodes;
|
||||
private final Gen.LongGen tokenGen;
|
||||
private final SortedSet<Long> tokens = new TreeSet<>();
|
||||
private final Map<Node.Id, Instance> instances = new HashMap<>();
|
||||
private final Set<Node.Id> removed = new HashSet<>();
|
||||
private final List<Throwable> failures = new ArrayList<>();
|
||||
private final SimulatedExecutorFactory globalExecutor;
|
||||
private final ScheduledExecutorPlus scheduler;
|
||||
private int nodeCounter = 0;
|
||||
private ClusterMetadata current = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY,
|
||||
new DistributedSchema(Keyspaces.of(
|
||||
DistributedMetadataLogKeyspace.initialMetadata(Collections.singleton("dc1")),
|
||||
KeyspaceMetadata.create("test", KeyspaceParams.simple(rf), Tables.of(TableMetadata.minimal("test", "tb1").unbuild().params(TableParams.builder().transactionalMode(TransactionalMode.full).build()).build())))));
|
||||
private final IFailureDetector fd = new IFailureDetector()
|
||||
{
|
||||
@Override
|
||||
public boolean isAlive(InetAddressAndPort ep)
|
||||
{
|
||||
return !removed.contains(nodeId(ep));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interpret(InetAddressAndPort ep)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void report(InetAddressAndPort ep)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(InetAddressAndPort ep)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceConviction(InetAddressAndPort ep)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerFailureDetectionEventListener(IFailureDetectionEventListener listener)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterFailureDetectionEventListener(IFailureDetectionEventListener listener)
|
||||
{
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private static InetAddressAndPort address(Node.Id id)
|
||||
{
|
||||
try
|
||||
{
|
||||
return InetAddressAndPort.getByAddress(ByteArrayUtil.bytes(id.id));
|
||||
}
|
||||
catch (UnknownHostException e)
|
||||
{
|
||||
throw new AssertionError("Unable to create address for id " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
public enum EpochTracker { topologyManager, accordSyncPropagator, configurationService}
|
||||
|
||||
Set<EpochTracker> globalSynced(long epoch)
|
||||
{
|
||||
return alive().stream()
|
||||
.filter(n -> instances.get(n).epoch.getEpoch() <= epoch)
|
||||
.map(n -> instances.get(n).synced(epoch))
|
||||
.reduce(EnumSet.allOf(EpochTracker.class), Sets::intersection);
|
||||
}
|
||||
|
||||
boolean allSynced(long epoch)
|
||||
{
|
||||
Set<EpochTracker> done = globalSynced(epoch);
|
||||
return done.contains(EpochTracker.topologyManager);
|
||||
}
|
||||
|
||||
private static Node.Id nodeId(InetAddressAndPort address)
|
||||
{
|
||||
return new Node.Id(ByteArrayUtil.getInt(address.addressBytes));
|
||||
}
|
||||
|
||||
public Cluster(RandomSource rs)
|
||||
{
|
||||
this.rs = rs;
|
||||
this.minNodes = 3;
|
||||
this.maxNodes = 10;
|
||||
this.tokenGen = rs2 -> rs2.nextLong(Long.MIN_VALUE + 1, Long.MAX_VALUE);
|
||||
|
||||
this.globalExecutor = new SimulatedExecutorFactory(accord.utilsfork.RandomSource.wrap(rs.asJdkRandom()), failures::add);
|
||||
this.scheduler = globalExecutor.scheduled("ignored");
|
||||
Stage.MISC.unsafeSetExecutor(scheduler);
|
||||
|
||||
scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (aliveCount() < 2) return;
|
||||
if (!partitions.isEmpty() && rs.nextBoolean())
|
||||
{
|
||||
// remove partition
|
||||
if (partitions.size() == 1)
|
||||
{
|
||||
partitions.clear();
|
||||
return;
|
||||
}
|
||||
partitions.remove(rs.pickOrderedSet(partitions));
|
||||
}
|
||||
else
|
||||
{
|
||||
// add partition
|
||||
List<Node.Id> alive = alive();
|
||||
InetAddressAndPort a = address(rs.pick(alive));
|
||||
InetAddressAndPort b = address(rs.pick(alive));
|
||||
while (a.equals(b))
|
||||
b = address(rs.pick(alive));
|
||||
partitions.add(new Connection(a, b));
|
||||
}
|
||||
}, 1, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
void validate(boolean isDone)
|
||||
{
|
||||
for (Node.Id id : alive())
|
||||
{
|
||||
Instance inst = instances.get(id);
|
||||
if (removed.contains(id)) continue; // ignore removed nodes
|
||||
AccordConfigurationService conf = inst.config;
|
||||
TopologyManager tm = inst.topology;
|
||||
for (long epoch = inst.epoch.getEpoch(); epoch <= current.epoch.getEpoch(); epoch++)
|
||||
{
|
||||
// validate config
|
||||
EpochSnapshot snapshot = conf.getEpochSnapshot(epoch);
|
||||
if (isDone)
|
||||
{
|
||||
Assertions.assertThat(snapshot).describedAs("node%s does not have epoch %d", id, epoch).isNotNull();
|
||||
Assertions.assertThat(snapshot.syncStatus).isEqualTo(AccordConfigurationService.SyncStatus.COMPLETED);
|
||||
|
||||
// validate topology manager
|
||||
Assertions.assertThat(tm.hasEpoch(epoch)).describedAs("node%s does not have epoch %d", id, epoch).isTrue();
|
||||
Ranges ranges = tm.globalForEpoch(epoch).ranges().mergeTouching();
|
||||
Ranges actual = tm.syncComplete(epoch).mergeTouching();
|
||||
Assertions.assertThat(actual).describedAs("node%s does not have all expected sync ranges for epoch %d; missing %s", id, epoch, ranges.subtract(actual)).isEqualTo(ranges);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (snapshot == null || snapshot.syncStatus != AccordConfigurationService.SyncStatus.COMPLETED) continue;
|
||||
|
||||
if (!allSynced(epoch))
|
||||
continue;
|
||||
|
||||
Assertions.assertThat(tm.hasEpoch(epoch)).describedAs("node%s does not have epoch %d", id, epoch).isTrue();
|
||||
Topology topology = tm.globalForEpoch(epoch);
|
||||
Ranges ranges = topology.ranges().mergeTouching();
|
||||
Ranges actual = tm.syncComplete(epoch).mergeTouching();
|
||||
// TopologyManager defines syncComplete for an epoch as (epoch - 1).syncComplete. This means that an epoch has reached quorum, but will still miss ranges as previous epochs have not
|
||||
if (!ranges.equals(actual) && tm.minEpoch() != epoch && !ranges.equals(tm.syncComplete(epoch - 1).mergeTouching()))
|
||||
continue;
|
||||
Assertions.assertThat(actual)
|
||||
.describedAs("node%s does not have all expected sync ranges for epoch %d; missing %s; peers=%s; previous epochs %s", id, epoch, ranges.subtract(actual), topology.nodes(),
|
||||
LongStream.range(inst.epoch.getEpoch(), epoch + 1).mapToObj(e -> e + " -> " + conf.getEpochSnapshot(e).syncStatus + "(synced=" + globalSynced(e) + "): " + tm.syncComplete(e)).collect(Collectors.joining("\n")))
|
||||
.isEqualTo(ranges);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String displayTopology()
|
||||
{
|
||||
List<Node.Id> alive = alive();
|
||||
List<Pair<Node.Id, Long>> withToken = new ArrayList<>(alive.size());
|
||||
for (Node.Id n : alive)
|
||||
withToken.add(Pair.create(n, instances.get(n).token));
|
||||
withToken.sort(Comparator.comparing(a -> a.right));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (var p : withToken)
|
||||
sb.append(p.left).append('\t').append(p.right).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "Topology:\n" + displayTopology();
|
||||
}
|
||||
|
||||
boolean hasWork()
|
||||
{
|
||||
return globalExecutor.hasWork();
|
||||
}
|
||||
|
||||
boolean processOne()
|
||||
{
|
||||
boolean result = globalExecutor.processOne();
|
||||
checkFailures();
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
void processAll()
|
||||
{
|
||||
while (processOne())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
List<Node.Id> alive()
|
||||
{
|
||||
ArrayList<Node.Id> ids = new ArrayList<>(Sets.difference(instances.keySet(), removed));
|
||||
ids.sort(Comparator.naturalOrder());
|
||||
return ids;
|
||||
}
|
||||
|
||||
int aliveCount()
|
||||
{
|
||||
return instances.size() - removed.size();
|
||||
}
|
||||
|
||||
private final NavigableSet<Connection> partitions = new TreeSet<>();
|
||||
|
||||
private boolean partitioned(InetAddressAndPort self, InetAddressAndPort to)
|
||||
{
|
||||
return partitions.contains(new Connection(self, to));
|
||||
}
|
||||
|
||||
private SimulatedMessageDelivery createMessaging(Node.Id id)
|
||||
{
|
||||
InetAddressAndPort address = address(id);
|
||||
return new SimulatedMessageDelivery(address,
|
||||
(self, msg, to) -> {
|
||||
if (removed.contains(nodeId(self)) || removed.contains(nodeId(to)))
|
||||
return Action.DROP;
|
||||
if (!self.equals(to) && partitioned(self, to))
|
||||
return Action.DROP_PARTITIONED;
|
||||
if (rs.decide(.01))
|
||||
return rs.nextBoolean() ? Action.DELIVER_WITH_FAILURE : Action.FAILURE;
|
||||
return Action.DELIVER;
|
||||
},
|
||||
SimulatedMessageDelivery.randomDelay(accord.utilsfork.RandomSource.wrap(rs.asJdkRandom())),
|
||||
(to, msg) -> instances.get(nodeId(to)).reciver.recieve(msg),
|
||||
(action, to, msg) -> logger.warn("{} message {}", action, msg),
|
||||
scheduler::schedule,
|
||||
failures::add);
|
||||
}
|
||||
|
||||
void addNode(Node.Id id, long token)
|
||||
{
|
||||
Invariants.checkState(!tokens.contains(token), "Attempted to add token %d for node %s but token is already taken", token, id);
|
||||
Epoch epoch = Epoch.create(current.epoch.getEpoch() + 1);
|
||||
|
||||
Instance instance = new Instance(id, token, epoch, createMessaging(id), fd);
|
||||
instances.put(id, instance);
|
||||
tokens.add(token);
|
||||
|
||||
current = current.forceEpoch(epoch)
|
||||
.withPlacements(DataPlacements.builder(2)
|
||||
.with(meta, DataPlacement.empty())
|
||||
.with(replication_params, rebuildPlacements(epoch))
|
||||
.build())
|
||||
.withDirectory(current.directory.with(new NodeAddresses(address(id)), new Location("dc1", "r1")));
|
||||
notify(current);
|
||||
}
|
||||
|
||||
void removeNode(Node.Id pick)
|
||||
{
|
||||
Instance inst = Objects.requireNonNull(instances.get(pick), "Unknown id " + pick);
|
||||
Invariants.checkState(!removed.contains(pick), "Can not remove node twice; node " + pick);
|
||||
tokens.remove(inst.token);
|
||||
removed.add(pick);
|
||||
inst.stop();
|
||||
current = current.forceEpoch(Epoch.create(current.epoch.getEpoch() + 1))
|
||||
.withDirectory(current.directory.without(new NodeId(pick.id)));
|
||||
|
||||
current = current.withPlacements(DataPlacements.builder(2)
|
||||
.with(meta, DataPlacement.empty())
|
||||
.with(replication_params, rebuildPlacements(current.epoch))
|
||||
.build());
|
||||
notify(current);
|
||||
}
|
||||
|
||||
private DataPlacement rebuildPlacements(Epoch epoch)
|
||||
{
|
||||
DataPlacement.Builder builder = DataPlacement.builder();
|
||||
for (Node.Id inst : alive())
|
||||
for (Replica replica : instances.get(inst).replica())
|
||||
builder.withReadReplica(epoch, replica).withWriteReplica(epoch, replica);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
void bumpEpoch()
|
||||
{
|
||||
current = current.forceEpoch(Epoch.create(current.epoch.getEpoch() + 1));
|
||||
notify(current);
|
||||
}
|
||||
|
||||
private void notify(ClusterMetadata current)
|
||||
{
|
||||
Ranges ranges = AccordTopology.createAccordTopology(current).ranges().mergeTouching();
|
||||
if (!current.directory.isEmpty())
|
||||
Assertions.assertThat(ranges).hasSize(1);
|
||||
((StubClusterMetadataService) ClusterMetadataService.instance()).setMetadata(current);
|
||||
for (Node.Id id : alive())
|
||||
{
|
||||
Instance inst = instances.get(id);
|
||||
inst.maybeStart();
|
||||
inst.config.maybeReportMetadata(current);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private <T> AsyncChain<T> schedule(long time, TimeUnit unit, Callable<T> task)
|
||||
{
|
||||
return new AsyncChains.Head<>()
|
||||
{
|
||||
@Override
|
||||
protected void start(BiConsumer<? super T, Throwable> callback)
|
||||
{
|
||||
scheduler.schedule(() -> {
|
||||
T value;
|
||||
try
|
||||
{
|
||||
value = task.call();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
callback.accept(null, t);
|
||||
return;
|
||||
}
|
||||
callback.accept(value, null);
|
||||
}, time, unit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private enum Status { Init, Started}
|
||||
private class Instance
|
||||
{
|
||||
private final Node.Id id;
|
||||
private final long token;
|
||||
private final AccordConfigurationService config;
|
||||
private final SimulatedMessageDelivery messaging;
|
||||
private final SimulatedMessageDelivery.SimulatedMessageReceiver reciver;
|
||||
private final TopologyManager topology;
|
||||
private final Epoch epoch;
|
||||
private Status status = Status.Init;
|
||||
|
||||
Instance(Node.Id node, long token, Epoch epoch, SimulatedMessageDelivery messagingService, IFailureDetector failureDetector)
|
||||
{
|
||||
this.id = node;
|
||||
this.token = token;
|
||||
this.epoch = epoch;
|
||||
this.topology = new TopologyManager(SizeOfIntersectionSorter.SUPPLIER, id);
|
||||
AccordConfigurationService.DiskStateManager instance = MockDiskStateManager.instance;
|
||||
config = new AccordConfigurationService(node, messagingService, failureDetector, instance, scheduler);
|
||||
config.registerListener(new ConfigurationService.Listener()
|
||||
{
|
||||
@Override
|
||||
public AsyncResult<Void> onTopologyUpdate(Topology topology, boolean startSync)
|
||||
{
|
||||
// EpochReady ready = EpochReady.done(topology.epoch());
|
||||
AsyncResult<Void> metadata = schedule(rs.nextInt(1, 10), TimeUnit.SECONDS, (Callable<Void>) () -> null).beginAsResult();
|
||||
AsyncResult<Void> coordination = metadata.flatMap(ignore -> schedule(rs.nextInt(1, 10), TimeUnit.SECONDS, (Callable<Void>) () -> null)).beginAsResult();
|
||||
AsyncResult<Void> data = coordination.flatMap(ignore -> schedule(rs.nextInt(1, 10), TimeUnit.SECONDS, (Callable<Void>) () -> null)).beginAsResult();
|
||||
AsyncResult<Void> reads = data.flatMap(ignore -> schedule(rs.nextInt(1, 10), TimeUnit.SECONDS, (Callable<Void>) () -> null)).beginAsResult();
|
||||
EpochReady ready = new EpochReady(topology.epoch(), metadata, coordination, data, reads);
|
||||
|
||||
topology().onTopologyUpdate(topology, () -> ready);
|
||||
ready.coordination.addCallback(() -> topology().onEpochSyncComplete(id, topology.epoch()));
|
||||
if (topology().minEpoch() == topology.epoch() && topology().epoch() != topology.epoch())
|
||||
return ready.coordination;
|
||||
config.acknowledgeEpoch(ready, startSync);
|
||||
return ready.coordination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteSyncComplete(Node.Id node, long epoch)
|
||||
{
|
||||
topology.onEpochSyncComplete(node, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveNodes(long epoch, Collection<Node.Id> removed)
|
||||
{
|
||||
topology.onRemoveNodes(epoch, removed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void truncateTopologyUntil(long epoch)
|
||||
{
|
||||
topology.truncateTopologyUntil(epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEpochClosed(Ranges ranges, long epoch)
|
||||
{
|
||||
topology.onEpochClosed(ranges, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEpochRedundant(Ranges ranges, long epoch)
|
||||
{
|
||||
topology.onEpochRedundant(ranges, epoch);
|
||||
}
|
||||
});
|
||||
|
||||
Map<Verb, IVerbHandler<?>> handlers = new EnumMap<>(Verb.class);
|
||||
//noinspection unchecked
|
||||
handlers.put(Verb.ACCORD_SYNC_NOTIFY_REQ, msg -> AccordService.receive(messagingService, config, (Message<List<AccordSyncPropagator.Notification>>) (Message<?>) msg));
|
||||
this.messaging = messagingService;
|
||||
this.reciver = messagingService.receiver(new SimulatedMessageDelivery.SimpleVerbHandler(handlers));
|
||||
}
|
||||
|
||||
void maybeStart()
|
||||
{
|
||||
if (status == Status.Init)
|
||||
{
|
||||
start();
|
||||
status = Status.Started;
|
||||
}
|
||||
}
|
||||
|
||||
private void start()
|
||||
{
|
||||
config.start();
|
||||
}
|
||||
|
||||
TopologyManager topology()
|
||||
{
|
||||
return topology;
|
||||
}
|
||||
|
||||
Collection<Replica> replica()
|
||||
{
|
||||
InetAddressAndPort address = Cluster.address(id);
|
||||
SortedSet<Long> lessThan = tokens.headSet(token);
|
||||
if (lessThan.isEmpty())
|
||||
{
|
||||
// wrap around
|
||||
return Arrays.asList(new Replica(address, new LongToken(Long.MIN_VALUE), new LongToken(token), true),
|
||||
new Replica(address, new LongToken(tokens.last()), new LongToken(Long.MIN_VALUE), true));
|
||||
}
|
||||
|
||||
return Collections.singletonList(new Replica(address, new LongToken(lessThan.last()), new LongToken(token), true));
|
||||
}
|
||||
|
||||
Set<EpochTracker> synced(long epoch)
|
||||
{
|
||||
if (epoch < this.epoch.getEpoch()) throw new IllegalArgumentException("Asked for epoch before this instance existed");
|
||||
EnumSet<EpochTracker> done = EnumSet.noneOf(EpochTracker.class);
|
||||
EpochSnapshot snapshot = config.getEpochSnapshot(epoch);
|
||||
if (snapshot != null && snapshot.syncStatus == AccordConfigurationService.SyncStatus.COMPLETED)
|
||||
done.add(EpochTracker.configurationService);
|
||||
if (topology.hasReachedQuorum(epoch))
|
||||
done.add(EpochTracker.topologyManager);
|
||||
if (!config.syncPropagator().hasPending(epoch))
|
||||
done.add(EpochTracker.accordSyncPropagator);
|
||||
return done;
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
messaging.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Connection implements Comparable<Connection>
|
||||
{
|
||||
final InetAddressAndPort from, to;
|
||||
|
||||
private Connection(InetAddressAndPort from, InetAddressAndPort to)
|
||||
{
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Connection that = (Connection) o;
|
||||
return from.equals(that.from) && to.equals(that.to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(from, to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "Connection{" + "from=" + from + ", to=" + to + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Connection o)
|
||||
{
|
||||
int rc = from.compareTo(o.from);
|
||||
if (rc == 0)
|
||||
rc = to.compareTo(o.to);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.service.accord;
|
||||
|
||||
import accord.local.Node;
|
||||
import accord.primitives.Ranges;
|
||||
import accord.topology.Topology;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* When trying to inspect the order in which disk state is modified, this class can aid by adding logging. This class
|
||||
* mostly exists for testing to aid in debugging.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@VisibleForTesting
|
||||
public class LoggingDiskStateManager implements AccordConfigurationService.DiskStateManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LoggingDiskStateManager.class);
|
||||
private final Node.Id self;
|
||||
private final AccordConfigurationService.DiskStateManager delegate;
|
||||
|
||||
public LoggingDiskStateManager(Node.Id self, AccordConfigurationService.DiskStateManager delegate) {
|
||||
this.self = self;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState loadTopologies(AccordKeyspace.TopologyLoadConsumer consumer) {
|
||||
logger.info("[node={}] Calling loadTopologies()", self);
|
||||
return delegate.loadTopologies(consumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState setNotifyingLocalSync(long epoch, Set<Node.Id> pending, AccordKeyspace.EpochDiskState diskState) {
|
||||
logger.info("[node={}] Calling setNotifyingLocalSync({}, {}, {})", self, epoch, pending, diskState);
|
||||
return delegate.setNotifyingLocalSync(epoch, pending, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState setCompletedLocalSync(long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
logger.info("[node={}] Calling setCompletedLocalSync({}, {})", self, epoch, diskState);
|
||||
return delegate.setCompletedLocalSync(epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState markLocalSyncAck(Node.Id id, long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
logger.info("[node={}] Calling markLocalSyncAck({}, {}, {})", self, id, epoch, diskState);
|
||||
return delegate.markLocalSyncAck(id, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState saveTopology(Topology topology, AccordKeyspace.EpochDiskState diskState) {
|
||||
logger.info("[node={}] Calling saveTopology({}, {})", self, topology.epoch(), diskState);
|
||||
return delegate.saveTopology(topology, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState markRemoteTopologySync(Node.Id id, long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
logger.info("[node={}] Calling markRemoteTopologySync({}, {}, {})", self, id, epoch, diskState);
|
||||
return delegate.markRemoteTopologySync(id, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState markClosed(Ranges ranges, long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
logger.info("[node={}] Calling markClosed({}, {}, {})", self, ranges, epoch, diskState);
|
||||
return delegate.markClosed(ranges, epoch, diskState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState truncateTopologyUntil(long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
logger.info("[node={}] Calling truncateTopologyUntil({}, {})", self, epoch, diskState);
|
||||
return delegate.truncateTopologyUntil(epoch, diskState);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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.Node;
|
||||
import accord.primitives.Ranges;
|
||||
import accord.topology.Topology;
|
||||
import accord.utils.Invariants;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public enum MockDiskStateManager implements AccordConfigurationService.DiskStateManager {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState loadTopologies(AccordKeyspace.TopologyLoadConsumer consumer) {
|
||||
return AccordKeyspace.EpochDiskState.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState setNotifyingLocalSync(long epoch, Set<Node.Id> pending, AccordKeyspace.EpochDiskState diskState) {
|
||||
return maybeUpdateMaxEpoch(diskState, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState setCompletedLocalSync(long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
return maybeUpdateMaxEpoch(diskState, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState markLocalSyncAck(Node.Id id, long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
return maybeUpdateMaxEpoch(diskState, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState saveTopology(Topology topology, AccordKeyspace.EpochDiskState diskState) {
|
||||
return maybeUpdateMaxEpoch(diskState, topology.epoch());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState markRemoteTopologySync(Node.Id node, long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
return maybeUpdateMaxEpoch(diskState, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState markClosed(Ranges ranges, long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
return maybeUpdateMaxEpoch(diskState, epoch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccordKeyspace.EpochDiskState truncateTopologyUntil(long epoch, AccordKeyspace.EpochDiskState diskState) {
|
||||
return maybeUpdateMaxEpoch(diskState, epoch);
|
||||
}
|
||||
|
||||
private static AccordKeyspace.EpochDiskState maybeUpdateMaxEpoch(AccordKeyspace.EpochDiskState diskState, long epoch) {
|
||||
if (diskState.isEmpty())
|
||||
return AccordKeyspace.EpochDiskState.create(epoch);
|
||||
Invariants.checkArgument(epoch >= diskState.minEpoch, "Epoch %d < %d (min)", epoch, diskState.minEpoch);
|
||||
if (epoch > diskState.maxEpoch)
|
||||
diskState = diskState.withNewMaxEpoch(epoch);
|
||||
return diskState;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ package org.apache.cassandra.utils;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
|
|
@ -99,7 +99,7 @@ public class StatefulRangeTreeTest
|
|||
@Override
|
||||
public Gen<Command<State, Sut, ?>> commands(State state)
|
||||
{
|
||||
Map<Gen<Command<State, Sut, ?>>, Integer> possible = new HashMap<>();
|
||||
Map<Gen<Command<State, Sut, ?>>, Integer> possible = new LinkedHashMap<>();
|
||||
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);
|
||||
|
|
@ -108,15 +108,15 @@ public class StatefulRangeTreeTest
|
|||
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 -> new Read(rs.pickOrderedSet(state.uniqRanges)), state.readWeight);
|
||||
possible.put(rs -> {
|
||||
Range range = rs.pick(state.uniqRanges);
|
||||
Range range = rs.pickOrderedSet(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);
|
||||
possible.put(rs -> new RangeRead(rs.pickOrderedSet(state.uniqRanges)), state.readWeight);
|
||||
possible.put(rs -> new Update(rs.pickOrderedSet(state.uniqRanges), SMALL_INT_GEN.nextInt(rs)), state.updateWeight);
|
||||
possible.put(rs -> new Delete(rs.pickOrderedSet(state.uniqRanges)), state.deleteWeight);
|
||||
}
|
||||
return Gens.oneOf(possible);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue