Implement coordinator log offset broadcasting

patch by Aleksey Yeschenko; reviewed by Blake Eggleston for CASSANDRA-20576
This commit is contained in:
Aleksey Yeschenko 2025-04-04 12:38:28 +01:00 committed by Blake Eggleston
parent c18ca7afc5
commit fa017c6334
19 changed files with 607 additions and 186 deletions

View File

@ -78,6 +78,7 @@ import org.apache.cassandra.repair.messages.SyncResponse;
import org.apache.cassandra.repair.messages.ValidationRequest;
import org.apache.cassandra.repair.messages.ValidationResponse;
import org.apache.cassandra.replication.ForwardedWrite;
import org.apache.cassandra.replication.ShardReplicatedOffsets;
import org.apache.cassandra.schema.SchemaMutationsSerializer;
import org.apache.cassandra.schema.SchemaPullVerbHandler;
import org.apache.cassandra.schema.SchemaPushVerbHandler;
@ -326,6 +327,7 @@ public enum Verb
READ_RECONCILE_RCV (902, P0, rpcTimeout, MUTATION, () -> ReadReconcileReceive.serializer, () -> ReadReconcileReceive.verbHandler ),
READ_RECONCILE_NOTIFY (903, P0, rpcTimeout, REQUEST_RESPONSE, () -> ReadReconcileNotify.serializer, () -> ReadReconcileNotify.verbHandler ),
FORWARDING_WRITE (904, P3, writeTimeout, MUTATION, () -> ForwardedWrite.Request.serializer, () -> ForwardedWrite.verbHandler),
BROADCAST_LOG_OFFSETS (905, P1, rpcTimeout, MISC, () -> ShardReplicatedOffsets.serializer, () -> ShardReplicatedOffsets.verbHandler),
TRACKED_PARTITION_READ_RSP (906, P2, readTimeout, REQUEST_RESPONSE, () -> TrackedDataResponse.serializer, RESPONSE_HANDLER ),
TRACKED_PARTITION_READ_REQ (907, P3, readTimeout, READ, () -> TrackedRead.DataRequest.serializer, () -> TrackedRead.verbHandler, TRACKED_PARTITION_READ_RSP),

View File

@ -21,15 +21,17 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.google.common.base.Preconditions;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.schema.TableId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
@ -41,29 +43,26 @@ public abstract class CoordinatorLog
protected final CoordinatorLogId logId;
protected final Participants participants;
/**
* State machines and an Id <-> token index for unreconciled mutation ids that exist oh this host.
*/
private final LocalMutationStates unreconciledMutations;
protected final Offsets.Mutable[] witnessedOffsets;
protected final Offsets.Mutable reconciledOffsets;
protected final Offsets.Mutable[] witnessedIds;
protected final Offsets.Mutable reconciledIds;
protected final ReadWriteLock lock;
abstract UnreconciledMutations unreconciledMutations();
CoordinatorLog(int localHostId, CoordinatorLogId logId, Participants participants)
{
this.localHostId = localHostId;
this.logId = logId;
this.participants = participants;
this.unreconciledMutations = new LocalMutationStates();
this.lock = new ReentrantReadWriteLock();
Offsets.Mutable[] ids = new Offsets.Mutable[participants.size()];
for (int i = 0; i < participants.size(); i++)
ids[i] = new Offsets.Mutable(logId);
witnessedIds = ids;
reconciledIds = new Offsets.Mutable(logId);
witnessedOffsets = ids;
reconciledOffsets = new Offsets.Mutable(logId);
}
static CoordinatorLog create(int localHostId, CoordinatorLogId id, Participants participants)
@ -72,7 +71,7 @@ public abstract class CoordinatorLog
: new CoordinatorLogReplica(localHostId, id, participants);
}
void witnessedRemoteMutation(MutationId mutationId, int onHostId)
void receivedWriteResponse(MutationId mutationId, int onHostId)
{
Preconditions.checkArgument(!mutationId.isNone());
logger.trace("witnessed remote mutation {} from {}", mutationId, onHostId);
@ -80,26 +79,17 @@ public abstract class CoordinatorLog
try
{
if (!get(onHostId).add(mutationId.offset()))
return; // already witnessed
return; // already witnessed; very uncommon but possible path
if (!getLocal().contains(mutationId.offset()))
return; // local host hasn't witnessed -> no cleanup needed
return; // local host hasn't witnessed yet -> no cleanup needed
// see if any other replicas haven't witnessed the id yet
boolean allOtherReplicasWitnessed = true;
for (int i = 0; i < participants.size() && allOtherReplicasWitnessed; i++)
{
int hostId = participants.get(i);
if (hostId != onHostId && hostId != localHostId && !get(hostId).contains(mutationId.offset()))
allOtherReplicasWitnessed = false;
}
if (allOtherReplicasWitnessed)
if (remoteReplicasWitnessed(mutationId.offset()))
{
logger.trace("marking mutation {} as fully reconciled", mutationId);
// if all replicas have now witnessed the id, remove it from the index
unreconciledMutations.remove(mutationId.offset());
reconciledIds.add(mutationId.offset());
unreconciledMutations().remove(mutationId.offset());
reconciledOffsets.add(mutationId.offset());
}
}
finally
@ -108,6 +98,45 @@ public abstract class CoordinatorLog
}
}
void updateReplicatedOffsets(Offsets offsets, int onHostId)
{
lock.writeLock().lock();
try
{
get(onHostId).addAll(offsets, (ignore, start, end) ->
{
for (int offset = start; offset <= end; ++offset)
{
// TODO (desired): use the fact that Offsets are ordered to optimise this look up
if (othersWitnessed(offset, onHostId))
{
reconciledOffsets.add(offset);
unreconciledMutations().remove(offset);
}
}
});
}
finally
{
lock.writeLock().unlock();
}
}
@Nullable
Offsets.Immutable collectReplicatedOffsets()
{
lock.readLock().lock();
try
{
Offsets offsets = witnessedOffsets[participants.indexOf(localHostId)];
return offsets.isEmpty() ? null : Offsets.Immutable.copy(offsets);
}
finally
{
lock.readLock().unlock();
}
}
void startWriting(Mutation mutation)
{
lock.writeLock().lock();
@ -116,7 +145,7 @@ public abstract class CoordinatorLog
if (getLocal().contains(mutation.id().offset()))
return; // already witnessed; shouldn't get to this path often (duplicate mutation)
unreconciledMutations.startWriting(mutation);
unreconciledMutations().startWriting(mutation);
}
finally
{
@ -127,27 +156,21 @@ public abstract class CoordinatorLog
void finishWriting(Mutation mutation)
{
logger.trace("witnessed local mutation {}", mutation.id());
lock.writeLock().lock();
try
{
if (!getLocal().add(mutation.id().offset()))
throw new IllegalStateException("finishWriting() called on a reconciled mutation");
int offset = mutation.id().offset();
if (!getLocal().add(offset))
throw new IllegalStateException("finishWriting() called on a locally witnessed mutation " + mutation.id());
// see if any other replicas haven't witnessed the id yet
boolean allOtherReplicasWitnessed = true;
for (int i = 0; i < participants.size() && allOtherReplicasWitnessed; i++)
unreconciledMutations().finishWriting(mutation);
if (remoteReplicasWitnessed(offset))
{
int hostId = participants.get(i);
if (hostId != localHostId && !get(hostId).contains(mutation.id().offset()))
allOtherReplicasWitnessed = false;
reconciledOffsets.add(offset);
unreconciledMutations().remove(offset);
}
// if some replicas also haven't witnessed the mutation yet, we should update local mutation state;
// otherwise we are the last node to witness this mutation, and can clean it up
if (allOtherReplicasWitnessed)
reconciledIds.add(mutation.id().offset());
else
unreconciledMutations.finishWriting(mutation);
}
finally
{
@ -155,6 +178,22 @@ public abstract class CoordinatorLog
}
}
private boolean othersWitnessed(int offset, int exceptHostId)
{
for (int i = 0; i < participants.size(); ++i)
{
int hostId = participants.get(i);
if (hostId != exceptHostId && !get(hostId).contains(offset))
return false;
}
return true;
}
private boolean remoteReplicasWitnessed(int offset)
{
return othersWitnessed(offset, localHostId);
}
/**
* Look up unreconciled sequence ids of mutations witnessed by this host in this coordinataor log.
* Adds the ids to the supplied collection, so it can be reused to aggregate lookups for multiple logs.
@ -164,8 +203,8 @@ public abstract class CoordinatorLog
lock.readLock().lock();
try
{
reconciledInto.addAll(reconciledIds);
return unreconciledMutations.collect(token, tableId, includePending, unreconciledInto);
reconciledInto.addAll(reconciledOffsets);
return unreconciledMutations().collect(token, tableId, includePending, unreconciledInto);
}
finally
{
@ -182,8 +221,8 @@ public abstract class CoordinatorLog
lock.readLock().lock();
try
{
reconciledInto.addAll(reconciledIds);
return unreconciledMutations.collect(range, tableId, includePending, unreconciledInto);
reconciledInto.addAll(reconciledOffsets);
return unreconciledMutations().collect(range, tableId, includePending, unreconciledInto);
}
finally
{
@ -193,21 +232,29 @@ public abstract class CoordinatorLog
protected Offsets.Mutable get(int hostId)
{
return witnessedIds[participants.indexOf(hostId)];
return witnessedOffsets[participants.indexOf(hostId)];
}
protected Offsets.Mutable getLocal()
{
return witnessedIds[participants.indexOf(localHostId)];
return witnessedOffsets[participants.indexOf(localHostId)];
}
public static class CoordinatorLogPrimary extends CoordinatorLog
{
AtomicLong sequenceId = new AtomicLong(-1);
private final AtomicLong sequenceId = new AtomicLong(-1);
private final UnreconciledMutationsReplica unreconciledMutations;
CoordinatorLogPrimary(int localHostId, CoordinatorLogId logId, Participants participants)
{
super(localHostId, logId, participants);
unreconciledMutations = new UnreconciledMutationsReplica();
}
@Override
UnreconciledMutationsReplica unreconciledMutations()
{
return unreconciledMutations;
}
MutationId nextId()
@ -235,9 +282,18 @@ public abstract class CoordinatorLog
public static class CoordinatorLogReplica extends CoordinatorLog
{
private final UnreconciledMutationsReplica unreconciledMutations;
CoordinatorLogReplica(int localHostId, CoordinatorLogId logId, Participants participants)
{
super(localHostId, logId, participants);
this.unreconciledMutations = new UnreconciledMutationsReplica();
}
@Override
UnreconciledMutationsReplica unreconciledMutations()
{
return unreconciledMutations;
}
}
}

View File

@ -118,7 +118,7 @@ public class CoordinatorLogId implements Serializable
public static final Comparator<CoordinatorLogId> comparator = (l, r) -> Long.compareUnsigned(l.asLong(), r.asLong());
public static final IVersionedSerializer<CoordinatorLogId> serializer = new IVersionedSerializer<>()
static final class Serializer implements IVersionedSerializer<CoordinatorLogId>
{
@Override
public void serialize(CoordinatorLogId logId, DataOutputPlus out, int version) throws IOException
@ -127,6 +127,12 @@ public class CoordinatorLogId implements Serializable
out.writeInt(logId.hostLogId);
}
public void serialize(long logId, DataOutputPlus out, int version) throws IOException
{
out.writeInt(hostId(logId));
out.writeInt(hostLogId(logId));
}
@Override
public CoordinatorLogId deserialize(DataInputPlus in, int version) throws IOException
{
@ -142,5 +148,12 @@ public class CoordinatorLogId implements Serializable
{
return TypeSizes.sizeof(logId.hostId) + TypeSizes.sizeof(logId.hostLogId);
}
};
public long serializedSize(long logId, int version)
{
return TypeSizes.sizeof(logId);
}
}
static final Serializer serializer = new Serializer();
}

View File

@ -402,7 +402,7 @@ public class ForwardedWrite
{
// Local mutations are witnessed from Keyspace.applyInternalTracked
if (msg != null)
MutationTrackingService.instance.witnessedRemoteMutation(keyspace, token, id, msg.from());
MutationTrackingService.instance.receivedWriteResponse(keyspace, token, id, msg.from());
// Local write needs to be ack'd to coordinator
if (msg == null && ackTo != null)

View File

@ -255,6 +255,14 @@ public class MutationSummary
return count;
}
public int reconciledIds()
{
int count = 0;
for (CoordinatorSummary summary : summaries)
count += summary.reconciled.offsetCount();
return count;
}
public int size()
{
return summaries.size();

View File

@ -18,30 +18,44 @@
package org.apache.cassandra.replication;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.IntSupplier;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.agrona.collections.IntArrayList;
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
import org.apache.cassandra.concurrent.Shutdownable;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.reads.tracked.TrackedLocalReads;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.utils.FBUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
import static org.apache.cassandra.concurrent.ExecutorFactory.SimulatorSemantics.NORMAL;
// TODO (expected): persistence (handle restarts)
// TODO (expected): handle topology changes
public class MutationTrackingService
@ -50,6 +64,7 @@ public class MutationTrackingService
public static final MutationTrackingService instance = new MutationTrackingService();
private final TrackedLocalReads localReads = new TrackedLocalReads();
private final ReplicatedOffsetsBroadcaster broadcaster = new ReplicatedOffsetsBroadcaster();
private final ConcurrentHashMap<String, KeyspaceShards> shards = new ConcurrentHashMap<>();
private volatile boolean started = false;
@ -67,6 +82,9 @@ public class MutationTrackingService
for (KeyspaceMetadata keyspace : metadata.schema.getKeyspaces())
if (keyspace.useMutationTracking())
shards.put(keyspace.name, KeyspaceShards.make(keyspace, metadata, this::nextHostLogId));
broadcaster.start();
started = true;
}
@ -78,6 +96,8 @@ public class MutationTrackingService
public void shutdownBlocking() throws InterruptedException
{
localReads.shutdownBlocking();
broadcaster.shutdown();
broadcaster.awaitTermination(1, TimeUnit.MINUTES);
}
public TrackedLocalReads localReads()
@ -92,10 +112,15 @@ public class MutationTrackingService
return id;
}
public void witnessedRemoteMutation(String keyspace, Token token, MutationId mutationId, InetAddressAndPort onHost)
public void receivedWriteResponse(String keyspace, Token token, MutationId mutationId, InetAddressAndPort onHost)
{
Preconditions.checkArgument(!mutationId.isNone());
getOrCreate(keyspace).witnessedRemoteMutation(token, mutationId, onHost);
getOrCreate(keyspace).receivedWriteResponse(token, mutationId, onHost);
}
public void updateReplicatedOffsets(String keyspace, Range<Token> range, List<? extends Offsets> offsets, InetAddressAndPort onHost)
{
getOrCreate(keyspace).updateReplicatedOffsets(range, offsets, onHost);
}
public void startWriting(Mutation mutation)
@ -125,6 +150,12 @@ public class MutationTrackingService
return createSummaryForRange(Range.makeRowRange(range), tableId, includePending);
}
void forEachKeyspace(Consumer<KeyspaceShards> consumer)
{
for (KeyspaceShards keyspaceShards : shards.values())
consumer.accept(keyspaceShards);
}
private KeyspaceShards getOrCreate(TableId tableId)
{
//noinspection DataFlowIssue
@ -161,11 +192,13 @@ public class MutationTrackingService
Preconditions.checkArgument(keyspace.params.replicationType.isTracked());
Map<Range<Token>, Shard> shards = new HashMap<>();
cluster.placements.get(keyspace.params.replication).writes.forEach((tokenRange, forRange) -> {
IntArrayList participants = new IntArrayList(forRange.size(), IntArrayList.DEFAULT_NULL_VALUE);
for (InetAddressAndPort endpoint : forRange.endpoints())
participants.add(cluster.directory.peerId(endpoint).id());
Shard shard = new Shard(keyspace.name, tokenRange, cluster.myNodeId().id(), new Participants(participants), forRange.lastModified(), logIdProvider);
shards.put(tokenRange, shard);
if (!forRange.endpoints().contains(FBUtilities.getBroadcastAddressAndPort()))
return;
IntArrayList participants = new IntArrayList(forRange.size(), IntArrayList.DEFAULT_NULL_VALUE);
for (InetAddressAndPort endpoint : forRange.endpoints())
participants.add(cluster.directory.peerId(endpoint).id());
Shard shard = new Shard(keyspace.name, tokenRange, cluster.myNodeId().id(), new Participants(participants), forRange.lastModified(), logIdProvider);
shards.put(tokenRange, shard);
});
return new KeyspaceShards(keyspace.name, shards);
}
@ -184,9 +217,14 @@ public class MutationTrackingService
return lookUp(token).nextId();
}
void witnessedRemoteMutation(Token token, MutationId mutationId, InetAddressAndPort onHost)
void receivedWriteResponse(Token token, MutationId mutationId, InetAddressAndPort onHost)
{
lookUp(token).witnessedRemoteMutation(mutationId, onHost);
lookUp(token).receivedWriteResponse(mutationId, onHost);
}
void updateReplicatedOffsets(Range<Token> range, List<? extends Offsets> offsets, InetAddressAndPort onHost)
{
shards.get(range).updateReplicatedOffsets(offsets, onHost);
}
void startWriting(Mutation mutation)
@ -224,6 +262,12 @@ public class MutationTrackingService
});
}
void forEachShard(Consumer<Shard> consumer)
{
for (Shard shard : shards.values())
consumer.accept(shard);
}
Shard lookUp(Mutation mutation)
{
return lookUp(mutation.key());
@ -242,4 +286,73 @@ public class MutationTrackingService
return shards.get(range);
}
}
// TODO (later): a more intelligent heuristic for offsets included in broadcasts
private static class ReplicatedOffsetsBroadcaster implements Runnable, Shutdownable
{
private static final ScheduledExecutorPlus executor =
executorFactory().scheduled("Replicated-Offsets-Broadcaster", NORMAL);
// TODO (later): a more intelligent heuristic for scheduling broadcasts
private static final long BROADCAST_INTERVAL_MILLIS = 200;
void start()
{
executor.scheduleWithFixedDelay(this, BROADCAST_INTERVAL_MILLIS, BROADCAST_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
}
@Override
public boolean isTerminated()
{
return executor.isTerminated();
}
@Override
public void shutdown()
{
executor.shutdown();
}
@Override
public Object shutdownNow()
{
return executor.shutdownNow();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
{
return executor.awaitTermination(timeout, units);
}
@Override
public void run()
{
MutationTrackingService.instance.forEachKeyspace(this::run);
}
private void run(KeyspaceShards shards)
{
shards.forEachShard(this::run);
}
private void run(Shard shard)
{
ShardReplicatedOffsets replicatedOffsets = shard.collectReplicatedOffsets();
if (replicatedOffsets.isEmpty())
return;
Message<ShardReplicatedOffsets> message = Message.out(Verb.BROADCAST_LOG_OFFSETS, replicatedOffsets);
for (InetAddressAndPort target : shard.remoteReplicas())
if (FailureDetector.instance.isAlive(target))
MessagingService.instance().send(message, target);
}
}
@VisibleForTesting
public void broadcastOffsetsForTesting()
{
broadcaster.run();
}
}

View File

@ -351,6 +351,7 @@ public abstract class Offsets implements Iterable<ShortMutationId>
if (size == 0)
{
append(start, end);
onAdded.consume(logId, start, end);
return true;
}
@ -529,24 +530,6 @@ public abstract class Offsets implements Iterable<ShortMutationId>
bounds[size++] = start;
bounds[size++] = end;
}
public void append(int offset)
{
if (size == 0)
{
append(offset, offset);
return;
}
int tail = bounds[size - 1];
if (offset <= tail)
throw new IllegalArgumentException("Can't append " + offset + " to " + tail);
if (offset == tail + 1)
bounds[size-1] = offset;
else
append(offset, offset);
}
}
public static class Mutable extends AbstractMutable<Mutable>
@ -1014,7 +997,7 @@ public abstract class Offsets implements Iterable<ShortMutationId>
case VALID:
state = computeNext();
if (!state.isFinished())
break;;
break;
case FINISHED:
return false;
default:

View File

@ -46,10 +46,21 @@ public class Participants
return idx;
}
boolean contains(int hostId)
{
return indexOf(hostId) >= 0;
}
int get(int idx)
{
if (idx < 0 || idx >= hosts.length)
throw new IllegalArgumentException("Out of bounds host idx " + idx);
return hosts[idx];
}
@Override
public String toString()
{
return Arrays.toString(hosts);
}
}

View File

@ -17,6 +17,8 @@
*/
package org.apache.cassandra.replication;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntSupplier;
import com.google.common.base.Preconditions;
@ -30,6 +32,7 @@ import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.replication.CoordinatorLog.CoordinatorLogPrimary;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.membership.NodeId;
import org.jctools.maps.NonBlockingHashMapLong;
public class Shard
@ -45,6 +48,8 @@ public class Shard
Shard(String keyspace, Range<Token> tokenRange, int localHostId, Participants participants, Epoch sinceEpoch, IntSupplier logIdProvider)
{
Preconditions.checkArgument(participants.contains(localHostId));
this.keyspace = keyspace;
this.tokenRange = tokenRange;
this.localHostId = localHostId;
@ -62,20 +67,27 @@ public class Shard
return currentLocalLog.nextId();
}
void witnessedRemoteMutation(MutationId mutationId, InetAddressAndPort onHost)
void receivedWriteResponse(MutationId mutationId, InetAddressAndPort onHost)
{
int onHostId = ClusterMetadata.current().directory.peerId(onHost).id();
get(mutationId).witnessedRemoteMutation(mutationId, onHostId);
getOrCreate(mutationId).receivedWriteResponse(mutationId, onHostId);
}
void updateReplicatedOffsets(List<? extends Offsets> offsets, InetAddressAndPort onHost)
{
int onHostId = ClusterMetadata.current().directory.peerId(onHost).id();
for (Offsets logOffsets : offsets)
getOrCreate(logOffsets.logId()).updateReplicatedOffsets(logOffsets, onHostId);
}
void startWriting(Mutation mutation)
{
get(mutation.id()).startWriting(mutation);
getOrCreate(mutation.id()).startWriting(mutation);
}
void finishWriting(Mutation mutation)
{
get(mutation.id()).finishWriting(mutation);
getOrCreate(mutation.id()).finishWriting(mutation);
}
void addSummaryForKey(Token token, boolean includePending, MutationSummary.Builder builder)
@ -94,6 +106,34 @@ public class Shard
});
}
List<InetAddressAndPort> remoteReplicas()
{
List<InetAddressAndPort> replicas = new ArrayList<>(participants.size() - 1);
for (int i = 0, size = participants.size(); i < size; ++i)
{
int hostId = participants.get(i);
if (hostId != localHostId)
replicas.add(ClusterMetadata.current().directory.endpoint(new NodeId(hostId)));
}
return replicas;
}
/**
* Collects replicated offsets for the logs owned by this coordinator on this shard.
*/
ShardReplicatedOffsets collectReplicatedOffsets()
{
List<Offsets.Immutable> offsets = new ArrayList<>();
for (CoordinatorLog log : logs.values())
{
Offsets.Immutable logOffsets = log.collectReplicatedOffsets();
if (logOffsets != null)
offsets.add(logOffsets);
}
return new ShardReplicatedOffsets(keyspace, tokenRange, offsets);
}
/**
* Creates a new coordinator log for this host. Primarily on Shard init (node startup or topology change).
* Also on keyspace creation.
@ -104,13 +144,18 @@ public class Shard
return new CoordinatorLog.CoordinatorLogPrimary(localHostId, logId, participants);
}
private CoordinatorLog get(MutationId mutationId)
private CoordinatorLog getOrCreate(MutationId mutationId)
{
Preconditions.checkArgument(!mutationId.isNone());
return get(mutationId.logId());
return getOrCreate(mutationId.logId());
}
private CoordinatorLog get(long logId)
private CoordinatorLog getOrCreate(CoordinatorLogId logId)
{
return getOrCreate(logId.asLong());
}
private CoordinatorLog getOrCreate(long logId)
{
CoordinatorLog log = logs.get(logId);
return log != null

View File

@ -0,0 +1,108 @@
/*
* 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.replication;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.IVerbHandler;
public class ShardReplicatedOffsets
{
private static final Logger logger = LoggerFactory.getLogger(ShardReplicatedOffsets.class);
private final String keyspace;
private final Range<Token> range;
private final List<Offsets.Immutable> replicatedOffsets;
public ShardReplicatedOffsets(String keyspace, Range<Token> range, List<Offsets.Immutable> offsets)
{
this.keyspace = keyspace;
this.range = range;
this.replicatedOffsets = offsets;
}
boolean isEmpty()
{
return replicatedOffsets.isEmpty();
}
@Override
public String toString()
{
return "ShardReplicatedOffsets{" + keyspace + ", " + range + ", " + replicatedOffsets + '}';
}
public static final IVerbHandler<ShardReplicatedOffsets> verbHandler = message -> {
ShardReplicatedOffsets replicatedOffsets = message.payload;
logger.trace("Received replicated offsets {} from {}", replicatedOffsets, message.from());
MutationTrackingService.instance.updateReplicatedOffsets(replicatedOffsets.keyspace,
replicatedOffsets.range,
replicatedOffsets.replicatedOffsets,
message.from());
};
public static final IVersionedSerializer<ShardReplicatedOffsets> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(ShardReplicatedOffsets status, DataOutputPlus out, int version) throws IOException
{
out.writeUTF(status.keyspace);
AbstractBounds.tokenSerializer.serialize(status.range, out, version);
out.writeInt(status.replicatedOffsets.size());
for (Offsets.Immutable logOffsets : status.replicatedOffsets)
Offsets.serializer.serialize(logOffsets, out, version);
}
@Override
public ShardReplicatedOffsets deserialize(DataInputPlus in, int version) throws IOException
{
String keyspace = in.readUTF();
Range<Token> range = (Range<Token>) AbstractBounds.tokenSerializer.deserialize(in, IPartitioner.global(), version);
int count = in.readInt();
List<Offsets.Immutable> replicatedOffsets = new ArrayList<>(count);
for (int i = 0; i < count; ++i)
replicatedOffsets.add(Offsets.serializer.deserialize(in, version));
return new ShardReplicatedOffsets(keyspace, range, replicatedOffsets);
}
@Override
public long serializedSize(ShardReplicatedOffsets replicatedOffsets, int version)
{
long size = 0;
size += TypeSizes.sizeof(replicatedOffsets.keyspace);
size += AbstractBounds.tokenSerializer.serializedSize(replicatedOffsets.range, version);
size += TypeSizes.sizeof(replicatedOffsets.replicatedOffsets.size());
for (Offsets.Immutable logOffsets : replicatedOffsets.replicatedOffsets)
size += Offsets.serializer.serializedSize(logOffsets, version);
return size;
}
};
}

View File

@ -168,10 +168,10 @@ public class TrackedWriteRequest
if (remoteDCReplicas == null)
remoteDCReplicas = new HashMap<>();
List<Replica> messages = remoteDCReplicas.get(dc);
if (messages == null)
messages = remoteDCReplicas.computeIfAbsent(dc, ignore -> new ArrayList<>(3)); // most DCs will have <= 3 replicas
messages.add(destination);
List<Replica> replicas = remoteDCReplicas.get(dc);
if (replicas == null)
replicas = remoteDCReplicas.computeIfAbsent(dc, ignore -> new ArrayList<>(3)); // most DCs will have <= 3 replicas
replicas.add(destination);
}
}
@ -179,8 +179,8 @@ public class TrackedWriteRequest
applyMutationLocally(mutation, handler);
if (localDCReplicas != null)
for (Replica destination : localDCReplicas)
MessagingService.instance().sendWriteWithCallback(message, destination, handler);
for (Replica replica : localDCReplicas)
MessagingService.instance().sendWriteWithCallback(message, replica, handler);
if (remoteDCReplicas != null)
{

View File

@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.replication;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.schema.TableId;
/**
* Tracks unreconciled local mutations - the subset of all unreconciled mutations
* that have been witnessed, or are currently being written to, on the local node.
*/
interface UnreconciledMutations
{
void startWriting(Mutation mutation);
void finishWriting(Mutation mutation);
void remove(int offset);
boolean collect(Token token, TableId tableId, boolean includePending, Offsets.OffsetReciever into);
boolean collect(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending, Offsets.OffsetReciever into);
}

View File

@ -37,7 +37,7 @@ import org.apache.cassandra.schema.TableId;
* Tracks unreconciled local mutations - the subset of all unreconciled mutations
* that have been witnessed, or are currently being written to, on the local node.
*/
class LocalMutationStates
class UnreconciledMutationsReplica implements UnreconciledMutations
{
private final Int2ObjectHashMap<Entry> statesMap = new Int2ObjectHashMap<>();
private final SortedSet<Entry> statesSet = new TreeSet<>(Entry.comparator);
@ -115,14 +115,16 @@ class LocalMutationStates
}
}
void startWriting(Mutation mutation)
@Override
public void startWriting(Mutation mutation)
{
Entry entry = Entry.create(mutation);
statesMap.put(entry.offset, entry);
statesSet.add(entry);
}
void finishWriting(Mutation mutation)
@Override
public void finishWriting(Mutation mutation)
{
Preconditions.checkArgument(!mutation.id().isNone());
Entry entry = statesMap.get(mutation.id().offset());
@ -130,20 +132,23 @@ class LocalMutationStates
entry.visibility = Visibility.VISIBLE;
}
void remove(int offset)
@Override
public void remove(int offset)
{
Entry state = statesMap.remove(offset);
Preconditions.checkNotNull(state);
statesSet.remove(state);
}
boolean collect(Token token, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
@Override
public boolean collect(Token token, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
{
SortedSet<Entry> subset = statesSet.subSet(Entry.start(token, true), Entry.end(token, true));
return collect(subset, tableId, includePending, into);
}
boolean collect(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
@Override
public boolean collect(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
{
Entry start = Entry.start(range.left.getToken(), range.left.kind() != PartitionPosition.Kind.MAX_BOUND);
Entry end = Entry.end(range.right.getToken(), range.right.kind() != PartitionPosition.Kind.MIN_BOUND);

View File

@ -56,7 +56,7 @@ public class TrackedWriteResponseHandler extends AbstractWriteResponseHandler<No
{
// Local mutations are witnessed from Keyspace.applyInternalTracked
if (msg != null)
MutationTrackingService.instance.witnessedRemoteMutation(keyspace, token, mutationId, msg.from());
MutationTrackingService.instance.receivedWriteResponse(keyspace, token, mutationId, msg.from());
wrapped.onResponse(msg);
}

View File

@ -68,7 +68,7 @@ public class ReadReconcileNotify
}
};
public static final IVersionedSerializer<ReadReconcileNotify> serializer = new IVersionedSerializer<ReadReconcileNotify>()
public static final IVersionedSerializer<ReadReconcileNotify> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(ReadReconcileNotify notify, DataOutputPlus out, int version) throws IOException

View File

@ -15,14 +15,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.test.tracking;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
@ -31,7 +28,6 @@ import org.apache.cassandra.dht.Token;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.IInstance;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.shared.NetworkTopology;
import org.apache.cassandra.distributed.test.TestBaseImpl;
@ -39,7 +35,6 @@ import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.assertj.core.api.Assertions;
import static java.lang.String.format;
import static org.apache.cassandra.distributed.shared.NetworkTopology.dcAndRack;
@ -64,7 +59,7 @@ public class MutationTrackingWriteForwardingTest extends TestBaseImpl
public void testBasicWriteForwarding() throws Throwable
{
// 2 DCs, 1 replica in each, to test forwarding to instances in remote DCs and local DCs
Map<Integer, NetworkTopology.DcAndRack> topology = networkTopology(5, (nodeid) -> nodeid % 2 == 1 ? dcAndRack("dc1", "rack1") : dcAndRack("dc2", "rack2"));
Map<Integer, NetworkTopology.DcAndRack> topology = networkTopology(NODES, (nodeid) -> nodeid % 2 == 1 ? dcAndRack("dc1", "rack1") : dcAndRack("dc2", "rack2"));
// TODO: disable background reconciliation so we can test that writes are reconciling immediately
try (Cluster cluster = Cluster.build(NODES)
@ -82,56 +77,44 @@ public class MutationTrackingWriteForwardingTest extends TestBaseImpl
"AND replication_type='tracked';", keyspaceName));
cluster.schemaChange(format("CREATE TABLE %s.%s (k int, c int, v int, primary key (k, c));", keyspaceName, tableName));
Map<IInstance, Integer> instanceUnreconciled = new HashMap<>();
int ROWS = 100;
for (int inserted = 0; inserted < ROWS; inserted++)
{
// Writes should be completed for the client, regardless of whether they are forwarded or not
cluster.coordinator(inst(inserted)).execute(format("INSERT INTO %s.%s (k, c, v) VALUES (?, ?, ?)", keyspaceName, tableName), ConsistencyLevel.ALL, inserted, inserted, inserted);
// Writes should be ack'd in the journal too, but these could lag behind client acks, so could be
// permissive here. Each write should be reconciled on 1 leader, unreconciled on TOTAL_RF-1
// replicas (until background reconciliation broadcast is implemented), and ignored on others.
Set<IInvokableInstance> leaderOrNonReplica = new HashSet<>(2);
Set<IInvokableInstance> replicas = new HashSet<>();
for (IInvokableInstance instance : cluster)
{
int unreconciled = instance.callOnInstance(() -> {
Token token = DatabaseDescriptor.getPartitioner().getMinimumToken();
Range<Token> fullRange = new Range<>(token, token);
TableId tableId = Schema.instance.getTableMetadata(keyspaceName, tableName).id;
MutationSummary summary = MutationTrackingService.instance.createSummaryForRange(fullRange, tableId, true);
return summary.unreconciledIds();
});
int lastUnreconciled = instanceUnreconciled.getOrDefault(instance, 0);
int newUnreconciled = unreconciled - lastUnreconciled;
if (newUnreconciled == 0)
{
// instance already reconciled (as leader) or did not receive new mutation ID (non-replica)
leaderOrNonReplica.add(instance);
}
else if (newUnreconciled == 1)
{
// instance has not reconciled, so it's a replica (until reconciliation broadcast is implemented)
replicas.add(instance);
}
else
{
Assertions.fail("Should not have more than one new unreconciled mutation");
}
instanceUnreconciled.put(instance, unreconciled);
}
Assertions.assertThat(leaderOrNonReplica).hasSize(2);
Assertions.assertThat(replicas).hasSize(TOTAL_RF - 1);
}
Assertions.assertThat(instanceUnreconciled).matches(map -> {
int sum = 0;
for (Integer value : map.values())
sum += value;
// Each write is reconciled on the leader, unreconciled on all other replicas
return sum == (ROWS * (TOTAL_RF - 1));
});
Thread.sleep(1000); // allow time for all offsets to be broadcasted
int allReconciled = 0;
int allUnreconciled = 0;
// Writes should be ack'd in the journal too, but these could lag behind client acks, so could be
// permissive here. Each write should be reconciled everywhere.
for (IInvokableInstance instance : cluster)
{
int reconciled = instance.callOnInstance(() -> {
Token token = DatabaseDescriptor.getPartitioner().getMinimumToken();
Range<Token> fullRange = new Range<>(token, token);
TableId tableId = Schema.instance.getTableMetadata(keyspaceName, tableName).id;
MutationSummary summary = MutationTrackingService.instance.createSummaryForRange(fullRange, tableId, true);
return summary.reconciledIds();
});
int unreconciled = instance.callOnInstance(() -> {
Token token = DatabaseDescriptor.getPartitioner().getMinimumToken();
Range<Token> fullRange = new Range<>(token, token);
TableId tableId = Schema.instance.getTableMetadata(keyspaceName, tableName).id;
MutationSummary summary = MutationTrackingService.instance.createSummaryForRange(fullRange, tableId, true);
return summary.unreconciledIds();
});
allReconciled += reconciled;
allUnreconciled += unreconciled;
}
Assert.assertEquals(0, allUnreconciled);
Assert.assertEquals(ROWS * TOTAL_RF, allReconciled);
}
}
}

View File

@ -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.distributed.test.tracking;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils.getOnlyLogId;
public class OffsetBroadcastTest extends TestBaseImpl
{
private static final Logger logger = LoggerFactory.getLogger(OffsetBroadcastTest.class);
@Test
public void testBroadcastOffsets() throws Throwable
{
try (Cluster cluster = Cluster.build(3)
.withConfig(cfg -> cfg.with(Feature.NETWORK)
.with(Feature.GOSSIP)
.set("mutation_tracking_enabled", "true"))
.start())
{
cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = " +
"{'class': 'SimpleStrategy', 'replication_factor': 3} " +
"AND replication_type='tracked';"));
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k int primary key, v int);"));
String keyspaceName = KEYSPACE;
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.tbl (k, v) VALUES (1, 1)"), ConsistencyLevel.QUORUM);
for (int i = 1; i <= cluster.size(); ++i)
cluster.get(i).runOnInstance(() -> MutationTrackingService.instance.broadcastOffsetsForTesting());
for (int i = 1; i <= cluster.size(); ++i)
{
cluster.get(i).runOnInstance(() -> {
TableMetadata table = Schema.instance.getTableMetadata(keyspaceName, "tbl");
DecoratedKey dk = Murmur3Partitioner.instance.decorateKey(ByteBufferUtil.bytes(1));
MutationSummary summary = MutationTrackingService.instance.createSummaryForKey(dk, table.id, false);
MutationSummary.CoordinatorSummary coordinatorSummary = summary.get(getOnlyLogId(summary));
Assert.assertEquals(1, coordinatorSummary.reconciled.offsetCount());
Assert.assertEquals(0, coordinatorSummary.unreconciled.offsetCount());
});
}
}
}
}

View File

@ -68,7 +68,7 @@ public class CoordinatorLogTest
{
Offsets.Mutable list = new Offsets.Mutable(LOG_ID);
for (MutationId id : ids)
list.append(id.offset());
list.add(id.offset());
return list;
}
@ -119,10 +119,10 @@ public class CoordinatorLogTest
// the call to finishWriting will have made the ids visible without the includePending flag
assertUnreconciled(tk, tableId, log, false, reconciled, ids);
log.witnessedRemoteMutation(ids[0], PARTICIPANTS.get(1));
log.receivedWriteResponse(ids[0], PARTICIPANTS.get(1));
assertUnreconciled(tk, tableId, log, false, reconciled, ids);
log.witnessedRemoteMutation(ids[0], PARTICIPANTS.get(2));
log.receivedWriteResponse(ids[0], PARTICIPANTS.get(2));
reconciled.add(ids[0].offset());
assertUnreconciled(tk, tableId, log, false, reconciled, ids[1], ids[2]);
}

View File

@ -507,46 +507,20 @@ public class OffsetsTest
}
@Test
public void appendTest()
public void addTest()
{
Offsets.Mutable ids = new Offsets.Mutable(LOG_ID);
ids.append(5);
ids.add(5);
assertEquals(1, ids.rangeCount());
assertEquals(1, ids.offsetCount());
ids.append(6);
ids.add(6);
assertEquals(1, ids.rangeCount());
assertEquals(2, ids.offsetCount());
ids.append(8);
ids.add(8);
assertEquals(2, ids.rangeCount());
assertEquals(3, ids.offsetCount());
// insert before tail
try
{
ids.append(8);
Assert.fail();
}
catch (IllegalArgumentException e)
{
// expected
assertEquals(2, ids.rangeCount());
assertEquals(3, ids.offsetCount());
}
// insert before tail
try
{
ids.append(7);
Assert.fail();
}
catch (IllegalArgumentException e)
{
// expected
assertEquals(2, ids.rangeCount());
assertEquals(3, ids.offsetCount());
}
}
private static void testUnion(Offsets expected, Offsets a, Offsets b)