Implement Shard and CoordinatorLog metadata durability

patch by Aleksey Yeschenko; reviewed by Abe Ratnofsky for CASSANDRA-20882
This commit is contained in:
Aleksey Yeschenko 2025-07-17 13:00:33 +01:00 committed by Blake Eggleston
parent 9dbafa812d
commit 9d856f2c08
19 changed files with 1102 additions and 341 deletions

View File

@ -190,7 +190,10 @@ public final class SystemKeyspace
public static final String REPAIRS = "repairs"; public static final String REPAIRS = "repairs";
public static final String TOP_PARTITIONS = "top_partitions"; public static final String TOP_PARTITIONS = "top_partitions";
public static final String METADATA_LOG = "local_metadata_log"; public static final String METADATA_LOG = "local_metadata_log";
public static final String SNAPSHOT_TABLE_NAME = "metadata_snapshots"; public static final String METADATA_SNAPSHOTS = "metadata_snapshots";
public static final String HOST_LOG_ID = "host_log_id";
public static final String SHARDS = "shards";
public static final String COORDINATOR_LOGS = "coordinator_logs";
/** /**
* By default the system keyspace tables should be stored in a single data directory to allow the server * By default the system keyspace tables should be stored in a single data directory to allow the server
@ -225,14 +228,16 @@ public final class SystemKeyspace
TABLE_ESTIMATES_TYPE_LOCAL_PRIMARY, AVAILABLE_RANGES_V2, TRANSFERRED_RANGES_V2, VIEW_BUILDS_IN_PROGRESS, TABLE_ESTIMATES_TYPE_LOCAL_PRIMARY, AVAILABLE_RANGES_V2, TRANSFERRED_RANGES_V2, VIEW_BUILDS_IN_PROGRESS,
BUILT_VIEWS, PREPARED_STATEMENTS, REPAIRS, TOP_PARTITIONS, LEGACY_PEERS, LEGACY_PEER_EVENTS, BUILT_VIEWS, PREPARED_STATEMENTS, REPAIRS, TOP_PARTITIONS, LEGACY_PEERS, LEGACY_PEER_EVENTS,
LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY, LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY,
METADATA_LOG, SNAPSHOT_TABLE_NAME, CONSENSUS_MIGRATION_STATE); METADATA_LOG, METADATA_SNAPSHOTS, CONSENSUS_MIGRATION_STATE,
HOST_LOG_ID, SHARDS, COORDINATOR_LOGS);
public static final Set<String> TABLE_NAMES = ImmutableSet.of( public static final Set<String> TABLE_NAMES = ImmutableSet.of(
BATCHES, PAXOS, PAXOS_REPAIR_HISTORY, BUILT_INDEXES, LOCAL, PEERS_V2, PEER_EVENTS_V2, BATCHES, PAXOS, PAXOS_REPAIR_HISTORY, BUILT_INDEXES, LOCAL, PEERS_V2, PEER_EVENTS_V2,
COMPACTION_HISTORY, SSTABLE_ACTIVITY_V2, TABLE_ESTIMATES, AVAILABLE_RANGES_V2, TRANSFERRED_RANGES_V2, VIEW_BUILDS_IN_PROGRESS, COMPACTION_HISTORY, SSTABLE_ACTIVITY_V2, TABLE_ESTIMATES, AVAILABLE_RANGES_V2, TRANSFERRED_RANGES_V2, VIEW_BUILDS_IN_PROGRESS,
BUILT_VIEWS, PREPARED_STATEMENTS, REPAIRS, TOP_PARTITIONS, LEGACY_PEERS, LEGACY_PEER_EVENTS, BUILT_VIEWS, PREPARED_STATEMENTS, REPAIRS, TOP_PARTITIONS, LEGACY_PEERS, LEGACY_PEER_EVENTS,
LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY, LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY,
METADATA_LOG, SNAPSHOT_TABLE_NAME, CONSENSUS_MIGRATION_STATE); METADATA_LOG, METADATA_SNAPSHOTS, CONSENSUS_MIGRATION_STATE,
HOST_LOG_ID, SHARDS, COORDINATOR_LOGS);
public static final TableMetadata Batches = public static final TableMetadata Batches =
parse(BATCHES, parse(BATCHES,
@ -518,7 +523,7 @@ public final class SystemKeyspace
+ "cfids set<uuid>, " + "cfids set<uuid>, "
+ "PRIMARY KEY (parent_id))").build(); + "PRIMARY KEY (parent_id))").build();
public static final TableMetadata LocalMetadataLog = private static final TableMetadata LocalMetadataLog =
parse(METADATA_LOG, parse(METADATA_LOG,
"Local Metadata Log", "Local Metadata Log",
"CREATE TABLE %s (" "CREATE TABLE %s ("
@ -532,13 +537,49 @@ public final class SystemKeyspace
"compaction_window_size","1"))) "compaction_window_size","1")))
.build(); .build();
public static final TableMetadata Snapshots = parse(SNAPSHOT_TABLE_NAME, private static final TableMetadata Snapshots =
"ClusterMetadata snapshots", parse(METADATA_SNAPSHOTS,
"CREATE TABLE IF NOT EXISTS %s (" + "ClusterMetadata snapshots",
"epoch bigint PRIMARY KEY," + "CREATE TABLE IF NOT EXISTS %s (" +
"snapshot blob)") "epoch bigint PRIMARY KEY," +
.partitioner(MetaStrategy.partitioner) "snapshot blob)")
.build(); .partitioner(MetaStrategy.partitioner)
.build();
private static final TableMetadata HostLogId =
parse(HOST_LOG_ID,
"mutation tracking last used host log id",
"CREATE TABLE %s ("
+ "key text,"
+ "host_log_id int,"
+ "PRIMARY KEY ((key)))")
.build();
private static final TableMetadata Shards =
parse(SHARDS,
"mutation tracking shards",
"CREATE TABLE %s ("
+ "keyspace_name text,"
+ "range_start text,"
+ "range_end text,"
+ "participants frozen<set<int>>,"
+ "PRIMARY KEY ((keyspace_name, range_start, range_end)))")
.build();
private static final TableMetadata CoordinatorLogs =
parse(COORDINATOR_LOGS,
"mutation tracking coordinator logs",
"CREATE TABLE %s ("
+ "keyspace_name text,"
+ "range_start text,"
+ "range_end text,"
+ "host_id int,"
+ "host_log_id int,"
+ "participants frozen<set<int>>,"
+ "witnessed_offsets map<int, frozen<list<int>>>,"
+ "persisted_offsets map<int, frozen<list<int>>>,"
+ "PRIMARY KEY ((keyspace_name, range_start, range_end), host_id, host_log_id))")
.build();
@Deprecated(since = "4.0") @Deprecated(since = "4.0")
private static final TableMetadata LegacyPeers = private static final TableMetadata LegacyPeers =
@ -633,7 +674,10 @@ public final class SystemKeyspace
TopPartitions, TopPartitions,
LocalMetadataLog, LocalMetadataLog,
Snapshots, Snapshots,
ConsensusMigrationState); ConsensusMigrationState,
HostLogId,
Shards,
CoordinatorLogs);
} }
private static volatile Map<TableId, Pair<CommitLogPosition, Long>> truncationRecords; private static volatile Map<TableId, Pair<CommitLogPosition, Long>> truncationRecords;
@ -2129,16 +2173,16 @@ public final class SystemKeyspace
{ {
Preconditions.checkArgument(epoch.isAfter(Epoch.FIRST), "Cannot store a snapshot for an epoch less than " + Epoch.FIRST.getEpoch()); Preconditions.checkArgument(epoch.isAfter(Epoch.FIRST), "Cannot store a snapshot for an epoch less than " + Epoch.FIRST.getEpoch());
logger.info("Storing snapshot of cluster metadata at epoch {}", epoch); logger.info("Storing snapshot of cluster metadata at epoch {}", epoch);
String query = String.format("INSERT INTO %s.%s (epoch, snapshot) VALUES (?, ?)", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); String query = String.format("INSERT INTO %s.%s (epoch, snapshot) VALUES (?, ?)", SchemaConstants.SYSTEM_KEYSPACE_NAME, METADATA_SNAPSHOTS);
executeInternal(query, epoch.getEpoch(), snapshot); executeInternal(query, epoch.getEpoch(), snapshot);
forceBlockingFlush(SNAPSHOT_TABLE_NAME); forceBlockingFlush(METADATA_SNAPSHOTS);
} }
public static ByteBuffer getSnapshot(Epoch epoch) public static ByteBuffer getSnapshot(Epoch epoch)
{ {
Preconditions.checkArgument(epoch.isAfter(Epoch.FIRST), "Cannot retrieve a snapshot for an epoch less than " + Epoch.FIRST.getEpoch()); Preconditions.checkArgument(epoch.isAfter(Epoch.FIRST), "Cannot retrieve a snapshot for an epoch less than " + Epoch.FIRST.getEpoch());
logger.info("Getting snapshot of epoch = {}", epoch); logger.info("Getting snapshot of epoch = {}", epoch);
String query = String.format("SELECT snapshot FROM %s.%s WHERE epoch = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); String query = String.format("SELECT snapshot FROM %s.%s WHERE epoch = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, METADATA_SNAPSHOTS);
UntypedResultSet res = executeInternal(query, epoch.getEpoch()); UntypedResultSet res = executeInternal(query, epoch.getEpoch());
if (res == null || res.isEmpty()) if (res == null || res.isEmpty())
return null; return null;
@ -2167,7 +2211,7 @@ public final class SystemKeyspace
{ {
// during gossip upgrade we have epoch = Long.MIN_VALUE + 1 (and the reverse partitioner doesn't support negative keys) // during gossip upgrade we have epoch = Long.MIN_VALUE + 1 (and the reverse partitioner doesn't support negative keys)
search = search.isBefore(Epoch.EMPTY) ? Epoch.EMPTY : search; search = search.isBefore(Epoch.EMPTY) ? Epoch.EMPTY : search;
String query = String.format("SELECT snapshot FROM %s.%s WHERE token(epoch) >= token(?) LIMIT 1", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); String query = String.format("SELECT snapshot FROM %s.%s WHERE token(epoch) >= token(?) LIMIT 1", SchemaConstants.SYSTEM_KEYSPACE_NAME, METADATA_SNAPSHOTS);
UntypedResultSet res = executeInternal(query, search.getEpoch()); UntypedResultSet res = executeInternal(query, search.getEpoch());
if (res != null && !res.isEmpty()) if (res != null && !res.isEmpty())
return res.one().getBytes("snapshot").duplicate(); return res.one().getBytes("snapshot").duplicate();
@ -2178,7 +2222,7 @@ public final class SystemKeyspace
{ {
// during gossip upgrade we have epoch = Long.MIN_VALUE + 1 (and the reverse partitioner doesn't support negative keys) // during gossip upgrade we have epoch = Long.MIN_VALUE + 1 (and the reverse partitioner doesn't support negative keys)
search = search.isBefore(Epoch.EMPTY) ? Epoch.EMPTY : search; search = search.isBefore(Epoch.EMPTY) ? Epoch.EMPTY : search;
String query = String.format("SELECT epoch FROM %s.%s WHERE token(epoch) < token(?)", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); String query = String.format("SELECT epoch FROM %s.%s WHERE token(epoch) < token(?)", SchemaConstants.SYSTEM_KEYSPACE_NAME, METADATA_SNAPSHOTS);
UntypedResultSet res = executeInternal(query, search.getEpoch()); UntypedResultSet res = executeInternal(query, search.getEpoch());
if (res == null) if (res == null)
return Collections.emptyList(); return Collections.emptyList();
@ -2191,7 +2235,7 @@ public final class SystemKeyspace
*/ */
public static ByteBuffer findLastSnapshot() public static ByteBuffer findLastSnapshot()
{ {
String query = String.format("SELECT snapshot FROM %s.%s LIMIT 1", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); String query = String.format("SELECT snapshot FROM %s.%s LIMIT 1", SchemaConstants.SYSTEM_KEYSPACE_NAME, METADATA_SNAPSHOTS);
UntypedResultSet res = executeInternal(query); UntypedResultSet res = executeInternal(query);
if (res != null && !res.isEmpty()) if (res != null && !res.isEmpty())
return res.one().getBytes("snapshot").duplicate(); return res.one().getBytes("snapshot").duplicate();

View File

@ -357,7 +357,7 @@ public abstract class SSTableWriter extends SSTable implements Transactional
if (metadata().replicationType().isTracked() && repairedAt == ActiveRepairService.UNREPAIRED_SSTABLE) if (metadata().replicationType().isTracked() && repairedAt == ActiveRepairService.UNREPAIRED_SSTABLE)
{ {
Preconditions.checkState(Objects.equals(pendingRepair, ActiveRepairService.NO_PENDING_REPAIR)); Preconditions.checkState(Objects.equals(pendingRepair, ActiveRepairService.NO_PENDING_REPAIR));
if (MutationTrackingService.instance.isDurablyReconciled(getKeyspaceName(), coordinatorLogOffsets)) if (MutationTrackingService.instance.isDurablyReconciled(coordinatorLogOffsets))
{ {
repairedAt = Clock.Global.currentTimeMillis(); repairedAt = Clock.Global.currentTimeMillis();
logger.debug("Marking SSTable {} as reconciled with repairedAt {}", descriptor, repairedAt); logger.debug("Marking SSTable {} as reconciled with repairedAt {}", descriptor, repairedAt);

View File

@ -41,12 +41,14 @@ public class BroadcastLogOffsets
private final String keyspace; private final String keyspace;
private final Range<Token> range; private final Range<Token> range;
private final List<Offsets.Immutable> replicatedOffsets; private final List<Offsets.Immutable> replicatedOffsets;
private final boolean durable;
public BroadcastLogOffsets(String keyspace, Range<Token> range, List<Offsets.Immutable> offsets) public BroadcastLogOffsets(String keyspace, Range<Token> range, List<Offsets.Immutable> offsets, boolean durable)
{ {
this.keyspace = keyspace; this.keyspace = keyspace;
this.range = range; this.range = range;
this.replicatedOffsets = offsets; this.replicatedOffsets = offsets;
this.durable = durable;
} }
boolean isEmpty() boolean isEmpty()
@ -57,7 +59,7 @@ public class BroadcastLogOffsets
@Override @Override
public String toString() public String toString()
{ {
return "ShardReplicatedOffsets{" + keyspace + ", " + range + ", " + replicatedOffsets + '}'; return "ShardReplicatedOffsets{" + keyspace + ", " + range + ", " + replicatedOffsets + ", " + durable + '}';
} }
public static final IVerbHandler<BroadcastLogOffsets> verbHandler = message -> { public static final IVerbHandler<BroadcastLogOffsets> verbHandler = message -> {
@ -66,6 +68,7 @@ public class BroadcastLogOffsets
MutationTrackingService.instance.updateReplicatedOffsets(replicatedOffsets.keyspace, MutationTrackingService.instance.updateReplicatedOffsets(replicatedOffsets.keyspace,
replicatedOffsets.range, replicatedOffsets.range,
replicatedOffsets.replicatedOffsets, replicatedOffsets.replicatedOffsets,
replicatedOffsets.durable,
message.from()); message.from());
}; };
@ -79,6 +82,7 @@ public class BroadcastLogOffsets
out.writeInt(status.replicatedOffsets.size()); out.writeInt(status.replicatedOffsets.size());
for (Offsets.Immutable logOffsets : status.replicatedOffsets) for (Offsets.Immutable logOffsets : status.replicatedOffsets)
Offsets.serializer.serialize(logOffsets, out, version); Offsets.serializer.serialize(logOffsets, out, version);
out.writeBoolean(status.durable);
} }
@Override @Override
@ -90,7 +94,8 @@ public class BroadcastLogOffsets
List<Offsets.Immutable> replicatedOffsets = new ArrayList<>(count); List<Offsets.Immutable> replicatedOffsets = new ArrayList<>(count);
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
replicatedOffsets.add(Offsets.serializer.deserialize(in, version)); replicatedOffsets.add(Offsets.serializer.deserialize(in, version));
return new BroadcastLogOffsets(keyspace, range, replicatedOffsets); boolean durable = in.readBoolean();
return new BroadcastLogOffsets(keyspace, range, replicatedOffsets, durable);
} }
@Override @Override
@ -102,6 +107,7 @@ public class BroadcastLogOffsets
size += TypeSizes.sizeof(replicatedOffsets.replicatedOffsets.size()); size += TypeSizes.sizeof(replicatedOffsets.replicatedOffsets.size());
for (Offsets.Immutable logOffsets : replicatedOffsets.replicatedOffsets) for (Offsets.Immutable logOffsets : replicatedOffsets.replicatedOffsets)
size += Offsets.serializer.serializedSize(logOffsets, version); size += Offsets.serializer.serializedSize(logOffsets, version);
size += TypeSizes.sizeof(replicatedOffsets.durable);
return size; return size;
} }
}; };

View File

@ -17,7 +17,11 @@
*/ */
package org.apache.cassandra.replication; package org.apache.cassandra.replication;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@ -27,74 +31,101 @@ import javax.annotation.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.agrona.collections.Int2ObjectHashMap;
import org.agrona.collections.IntArrayList; import org.agrona.collections.IntArrayList;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token; import org.apache.cassandra.dht.Token;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ClusterMetadata;
import static java.lang.String.format;
import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
import static org.apache.cassandra.replication.Node2OffsetsMap.forParticipants;
import static org.apache.cassandra.replication.Node2OffsetsMap.fromPrimitiveMap;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
public abstract class CoordinatorLog public abstract class CoordinatorLog
{ {
private static final Logger logger = LoggerFactory.getLogger(CoordinatorLog.class); private static final Logger logger = LoggerFactory.getLogger(CoordinatorLog.class);
protected final int localHostId; protected final int localNodeId;
protected final String keyspace;
protected final Range<Token> range;
protected final CoordinatorLogId logId; protected final CoordinatorLogId logId;
protected final Participants participants; protected final Participants participants;
protected final Node2OffsetsMap witnessedOffsets;
protected final Node2OffsetsMap persistedOffsets;
protected final UnreconciledMutations unreconciledMutations; protected final UnreconciledMutations unreconciledMutations;
protected final Offsets.Mutable[] witnessedOffsets;
protected final Offsets.Mutable reconciledOffsets; protected final Offsets.Mutable reconciledOffsets;
protected final Offsets.Mutable reconciledPersistedOffsets;
protected final ReadWriteLock lock; protected final ReadWriteLock lock;
abstract void receivedWriteResponse(ShortMutationId mutationId, int fromHostId); abstract void receivedWriteResponse(ShortMutationId mutationId, int fromNodeId);
CoordinatorLog(int localHostId, CoordinatorLogId logId, Participants participants) CoordinatorLog(String keyspace,
Range<Token> range,
int localNodeId,
CoordinatorLogId logId,
Participants participants,
Node2OffsetsMap witnessedOffsets,
Node2OffsetsMap persistedOffsets,
UnreconciledMutations unreconciledMutations)
{ {
this.localHostId = localHostId; this.localNodeId = localNodeId;
this.keyspace = keyspace;
this.range = range;
this.logId = logId; this.logId = logId;
this.participants = participants; this.participants = participants;
this.unreconciledMutations = unreconciledMutations;
this.unreconciledMutations = new UnreconciledMutations(); this.witnessedOffsets = witnessedOffsets;
this.reconciledOffsets = witnessedOffsets.intersection();
this.persistedOffsets = persistedOffsets;
this.reconciledPersistedOffsets = persistedOffsets.intersection();
this.lock = new ReentrantReadWriteLock(); 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);
witnessedOffsets = ids;
reconciledOffsets = new Offsets.Mutable(logId);
} }
static CoordinatorLog create(int localHostId, CoordinatorLogId id, Participants participants) CoordinatorLog(String keyspace, Range<Token> range, int localNodeId, CoordinatorLogId logId, Participants participants)
{ {
return id.hostId == localHostId ? new CoordinatorLogPrimary(localHostId, id, participants) this(keyspace, range, localNodeId, logId, participants, forParticipants(logId, participants), forParticipants(logId, participants), new UnreconciledMutations());
: new CoordinatorLogReplica(localHostId, id, participants);
} }
void updateReplicatedOffsets(Offsets offsets, int onHostId) static CoordinatorLog create(String keyspace, Range<Token> range, int localNodeId, CoordinatorLogId id, Participants participants)
{
return id.hostId == localNodeId ? new CoordinatorLogPrimary(keyspace, range, localNodeId, id, participants)
: new CoordinatorLogReplica(keyspace, range, localNodeId, id, participants);
}
static CoordinatorLog recreate(
String keyspace, Range<Token> range, int localNodeId, CoordinatorLogId id, Participants participants,
Node2OffsetsMap witnessedOffsets, Node2OffsetsMap persistedOffsets, UnreconciledMutations unreconciledMutations)
{
return id.hostId == localNodeId ? new CoordinatorLogPrimary(keyspace, range, localNodeId, id, participants, witnessedOffsets, persistedOffsets, unreconciledMutations)
: new CoordinatorLogReplica(keyspace, range, localNodeId, id, participants, witnessedOffsets, persistedOffsets, unreconciledMutations);
}
void updateReplicatedOffsets(Offsets offsets, boolean persisted, int onNodeId)
{ {
lock.writeLock().lock(); lock.writeLock().lock();
try try
{ {
get(onHostId).addAll(offsets, (ignore, start, end) -> if (persisted)
{ updatePersistedReplicatedOffsets(offsets, onNodeId);
for (int offset = start; offset <= end; ++offset) else
{ updateWitnessedReplicatedOffsets(offsets, onNodeId);
// 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 finally
{ {
@ -102,13 +133,37 @@ public abstract class CoordinatorLog
} }
} }
private void updateWitnessedReplicatedOffsets(Offsets offsets, int onNodeId)
{
witnessedOffsets.get(onNodeId).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, onNodeId))
{
reconciledOffsets.add(offset);
unreconciledMutations.remove(offset);
}
}
});
}
private void updatePersistedReplicatedOffsets(Offsets offsets, int onNodeId)
{
persistedOffsets.get(onNodeId).addAll(offsets);
reconciledPersistedOffsets.addAll(persistedOffsets.intersection());
}
@Nullable @Nullable
Offsets.Immutable collectReplicatedOffsets() Offsets.Immutable collectReplicatedOffsets(boolean persisted)
{ {
lock.readLock().lock(); lock.readLock().lock();
try try
{ {
Offsets offsets = witnessedOffsets[participants.indexOf(localHostId)]; Offsets offsets = persisted
? persistedOffsets.get(localNodeId)
: witnessedOffsets.get(localNodeId);
return offsets.isEmpty() ? null : Offsets.Immutable.copy(offsets); return offsets.isEmpty() ? null : Offsets.Immutable.copy(offsets);
} }
finally finally
@ -122,7 +177,7 @@ public abstract class CoordinatorLog
lock.writeLock().lock(); lock.writeLock().lock();
try try
{ {
if (getLocal().contains(mutation.id().offset())) if (witnessedOffsets.get(localNodeId).contains(mutation.id().offset()))
return false; // already witnessed; shouldn't get to this path often (duplicate mutation) return false; // already witnessed; shouldn't get to this path often (duplicate mutation)
unreconciledMutations.startWriting(mutation); unreconciledMutations.startWriting(mutation);
@ -143,7 +198,7 @@ public abstract class CoordinatorLog
{ {
int offset = mutation.id().offset(); int offset = mutation.id().offset();
// we've raced with another write, no need to do anything else // we've raced with another write, no need to do anything else
if (!getLocal().add(offset)) if (!witnessedOffsets.get(localNodeId).add(offset))
return; return;
unreconciledMutations.finishWriting(mutation); unreconciledMutations.finishWriting(mutation);
@ -160,12 +215,12 @@ public abstract class CoordinatorLog
} }
} }
private boolean othersWitnessed(int offset, int exceptHostId) private boolean othersWitnessed(int offset, int exceptNodeId)
{ {
for (int i = 0; i < participants.size(); ++i) for (int i = 0; i < participants.size(); ++i)
{ {
int hostId = participants.get(i); int nodeId = participants.get(i);
if (hostId != exceptHostId && !get(hostId).contains(offset)) if (nodeId != exceptNodeId && !witnessedOffsets.get(nodeId).contains(offset))
return false; return false;
} }
return true; return true;
@ -173,7 +228,7 @@ public abstract class CoordinatorLog
protected boolean remoteReplicasWitnessed(int offset) protected boolean remoteReplicasWitnessed(int offset)
{ {
return othersWitnessed(offset, localHostId); return othersWitnessed(offset, localNodeId);
} }
/** /**
@ -220,7 +275,7 @@ public abstract class CoordinatorLog
lock.readLock().lock(); lock.readLock().lock();
try try
{ {
into.add(Offsets.Immutable.difference(remoteOffsets, getLocal())); into.add(Offsets.Immutable.difference(remoteOffsets, witnessedOffsets.get(localNodeId)));
} }
finally finally
{ {
@ -233,9 +288,11 @@ public abstract class CoordinatorLog
lock.readLock().lock(); lock.readLock().lock();
try try
{ {
remoteNodeIds.forEachInt(remoteNodeId -> { remoteNodeIds.forEachInt(remoteNodeId ->
Offsets missing = Offsets.Immutable.difference(get(remoteNodeId), localOffsets); {
if (!missing.isEmpty()) into.add(remoteNodeId, missing); Offsets missing = Offsets.Immutable.difference(witnessedOffsets.get(remoteNodeId), localOffsets);
if (!missing.isEmpty())
into.add(remoteNodeId, missing);
}); });
} }
finally finally
@ -244,18 +301,12 @@ public abstract class CoordinatorLog
} }
} }
protected Offsets.Mutable get(int hostId)
{
return witnessedOffsets[participants.indexOf(hostId)];
}
boolean isDurablyReconciled(CoordinatorLogOffsets<?> logOffsets) boolean isDurablyReconciled(CoordinatorLogOffsets<?> logOffsets)
{ {
lock.readLock().lock(); lock.readLock().lock();
try try
{ {
// TODO: reconciledOffsets not necessarily durable, update once durability is implemented Offsets.RangeIterator durablyReconciled = reconciledPersistedOffsets.rangeIterator();
Offsets.RangeIterator durablyReconciled = reconciledOffsets.rangeIterator();
Offsets.RangeIterator difference = Offsets.difference(logOffsets.offsets(logId.asLong()).rangeIterator(), durablyReconciled); Offsets.RangeIterator difference = Offsets.difference(logOffsets.offsets(logId.asLong()).rangeIterator(), durablyReconciled);
return !difference.tryAdvance(); return !difference.tryAdvance();
} }
@ -265,17 +316,12 @@ public abstract class CoordinatorLog
} }
} }
protected Offsets.Mutable getLocal()
{
return witnessedOffsets[participants.indexOf(localHostId)];
}
@Override @Override
public String toString() public String toString()
{ {
return "CoordinatorLog{" + return "CoordinatorLog{" +
"logId=" + logId + "logId=" + logId +
", localHostId=" + localHostId + ", localNodeId=" + localNodeId +
", participants=" + participants + ", participants=" + participants +
'}'; '}';
} }
@ -284,24 +330,31 @@ public abstract class CoordinatorLog
{ {
private final AtomicLong sequenceId = new AtomicLong(-1); private final AtomicLong sequenceId = new AtomicLong(-1);
CoordinatorLogPrimary(int localHostId, CoordinatorLogId logId, Participants participants) CoordinatorLogPrimary(
String keyspace, Range<Token> range, int localNodeId, CoordinatorLogId logId, Participants participants,
Node2OffsetsMap witnessedOffsets, Node2OffsetsMap persistedOffsets, UnreconciledMutations unreconciledMutations)
{ {
super(localHostId, logId, participants); super(keyspace, range, localNodeId, logId, participants, witnessedOffsets, persistedOffsets, unreconciledMutations);
}
CoordinatorLogPrimary(String keyspace, Range<Token> range, int localNodeId, CoordinatorLogId logId, Participants participants)
{
super(keyspace, range, localNodeId, logId, participants);
} }
@Override @Override
void receivedWriteResponse(ShortMutationId mutationId, int fromHostId) void receivedWriteResponse(ShortMutationId mutationId, int fromNodeId)
{ {
Preconditions.checkArgument(!mutationId.isNone()); Preconditions.checkArgument(!mutationId.isNone());
Preconditions.checkArgument(!Objects.equals(fromHostId, ClusterMetadata.current().myNodeId().id())); Preconditions.checkArgument(!Objects.equals(fromNodeId, ClusterMetadata.current().myNodeId().id()));
logger.trace("witnessed remote mutation {} from {}", mutationId, fromHostId); logger.trace("witnessed remote mutation {} from {}", mutationId, fromNodeId);
lock.writeLock().lock(); lock.writeLock().lock();
try try
{ {
if (!get(fromHostId).add(mutationId.offset())) if (!witnessedOffsets.get(fromNodeId).add(mutationId.offset()))
return; // already witnessed; very uncommon but possible path return; // already witnessed; very uncommon but possible path
if (!getLocal().contains(mutationId.offset())) if (!witnessedOffsets.get(localNodeId).contains(mutationId.offset()))
return; // local host hasn't witnessed yet -> no cleanup needed return; // local host hasn't witnessed yet -> no cleanup needed
if (remoteReplicasWitnessed(mutationId.offset())) if (remoteReplicasWitnessed(mutationId.offset()))
@ -318,9 +371,13 @@ public abstract class CoordinatorLog
} }
} }
@Nullable
MutationId nextId() MutationId nextId()
{ {
return new MutationId(logId.asLong(), nextSequenceId()); long nextSequenceId = nextSequenceId();
return nextSequenceId >= 0
? new MutationId(logId.asLong(), nextSequenceId)
: null;
} }
private long nextSequenceId() private long nextSequenceId()
@ -331,6 +388,10 @@ public abstract class CoordinatorLog
int prevOffset = MutationId.offset(prev); int prevOffset = MutationId.offset(prev);
int prevTimestamp = MutationId.timestamp(prev); int prevTimestamp = MutationId.timestamp(prev);
// int overflow
if (prevOffset == MAX_OFFSET)
return -1;
int nextOffset = prevOffset + 1; int nextOffset = prevOffset + 1;
int nextTimestamp = Math.max(prevTimestamp + 1, (int) (currentTimeMillis() / 1000L)); int nextTimestamp = Math.max(prevTimestamp + 1, (int) (currentTimeMillis() / 1000L));
long next = MutationId.sequenceId(nextOffset, nextTimestamp); long next = MutationId.sequenceId(nextOffset, nextTimestamp);
@ -343,16 +404,131 @@ public abstract class CoordinatorLog
static class CoordinatorLogReplica extends CoordinatorLog static class CoordinatorLogReplica extends CoordinatorLog
{ {
CoordinatorLogReplica(int localHostId, CoordinatorLogId logId, Participants participants) CoordinatorLogReplica(
String keyspace, Range<Token> range, int localNodeId, CoordinatorLogId logId, Participants participants,
Node2OffsetsMap witnessedOffsets, Node2OffsetsMap persistedOffsets, UnreconciledMutations unreconciledMutations)
{ {
super(localHostId, logId, participants); super(keyspace, range, localNodeId, logId, participants, witnessedOffsets, persistedOffsets, unreconciledMutations);
}
CoordinatorLogReplica(String keyspace, Range<Token> range, int localNodeId, CoordinatorLogId logId, Participants participants)
{
super(keyspace, range, localNodeId, logId, participants);
} }
@Override @Override
void receivedWriteResponse(ShortMutationId mutationId, int fromHostId) void receivedWriteResponse(ShortMutationId mutationId, int fromNodeId)
{ {
// no-op // no-op
} }
} }
/*
* Persist to / load from system table.
*/
private static final String INSERT_QUERY =
format("INSERT INTO %s.%s (keyspace_name, range_start, range_end, host_id, host_log_id, participants, witnessed_offsets, persisted_offsets) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.COORDINATOR_LOGS);
void persistToSystemTable()
{
Map<Integer, List<Integer>> witnessed = new Int2ObjectHashMap<>();
Map<Integer, List<Integer>> persisted = new Int2ObjectHashMap<>();
lock.readLock().lock();
try
{
witnessedOffsets.convertToPrimitiveMap(witnessed);
persistedOffsets.convertToPrimitiveMap(persisted);
}
finally
{
lock.readLock().unlock();
}
executeInternal(INSERT_QUERY, keyspace, range.left.toString(), range.right.toString(), logId.hostId,
logId.hostLogId, participants.asSet(), witnessed, persisted);
}
void updateLogsInSystemTable()
{
Offsets.Mutable localWitnessed;
Map<Integer, List<Integer>> witnessed = new Int2ObjectHashMap<>();
Map<Integer, List<Integer>> persisted = new Int2ObjectHashMap<>();
lock.readLock().lock();
try
{
localWitnessed = Offsets.Mutable.copy(witnessedOffsets.get(localNodeId));
witnessedOffsets.convertToPrimitiveMap(witnessed);
persistedOffsets.convertToPrimitiveMap(persisted);
persisted.put(localNodeId, witnessed.get(localNodeId));
}
finally
{
lock.readLock().unlock();
}
executeInternal(INSERT_QUERY, keyspace, range.left.toString(), range.right.toString(), logId.hostId,
logId.hostLogId, participants.asSet(), witnessed, persisted);
lock.writeLock().lock();
try
{
persistedOffsets.set(localNodeId, localWitnessed);
reconciledPersistedOffsets.addAll(persistedOffsets.intersection());
}
finally
{
lock.writeLock().unlock();
}
}
private static final String SELECT_QUERY =
format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND range_start = ? AND range_end = ?",
SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.COORDINATOR_LOGS);
static List<CoordinatorLog> loadFromSystemTable(String keyspace, Range<Token> range, int localNodeId)
{
ArrayList<CoordinatorLog> logs = new ArrayList<>();
for (UntypedResultSet.Row row : executeInternal(SELECT_QUERY, keyspace, range.left.toString(), range.right.toString()))
{
int nodeId = row.getInt("host_id");
int hostLogId = row.getInt("host_log_id");
CoordinatorLogId logId = new CoordinatorLogId(nodeId, hostLogId);
Set<Integer> participants = row.getFrozenSet("participants", Int32Type.instance);
Map<Integer, List<Integer>> witnessedOffsets =
row.getMap("witnessed_offsets", Int32Type.instance, ListType.getInstance(Int32Type.instance, false));
Map<Integer, List<Integer>> persistedOffsets =
row.getMap("persisted_offsets", Int32Type.instance, ListType.getInstance(Int32Type.instance, false));
Node2OffsetsMap witnessed = fromPrimitiveMap(logId, witnessedOffsets);
Node2OffsetsMap persisted = fromPrimitiveMap(logId, persistedOffsets);
UnreconciledMutations unreconciled = UnreconciledMutations.loadFromJournal(witnessed, localNodeId);
CoordinatorLog log =
CoordinatorLog.recreate(keyspace, range, localNodeId, logId, new Participants(participants), witnessed, persisted, unreconciled);
logs.add(log);
}
return logs;
}
private static final String DELETE_QUERY =
format("DELETE FROM %s.%s WHERE keyspace_name = ? AND range_start = ? AND range_end = ? AND host_id = ? AND host_log_id = ?",
SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.COORDINATOR_LOGS);
void deleteFromSystemTable()
{
executeInternal(DELETE_QUERY, keyspace, range.left.toString(), range.right.toString(), logId.hostId, logId.hostLogId);
}
@VisibleForTesting
static void overrideMaxOffsetForTesting(int nexMaxOffset)
{
MAX_OFFSET = nexMaxOffset;
}
// don't make volatile unless it genuinely is an issue for some test,
// otherwise it should be *fine* as is, and slight overkill to make volatile
private static int MAX_OFFSET = Integer.MAX_VALUE;
} }

View File

@ -17,6 +17,7 @@
*/ */
package org.apache.cassandra.replication; package org.apache.cassandra.replication;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -26,10 +27,9 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.IntSupplier; import java.util.function.LongSupplier;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -40,10 +40,11 @@ import com.google.common.base.Preconditions;
import org.agrona.collections.IntArrayList; import org.agrona.collections.IntArrayList;
import org.agrona.collections.IntHashSet; import org.agrona.collections.IntHashSet;
import org.apache.cassandra.concurrent.ScheduledExecutorPlus; import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
import org.apache.cassandra.concurrent.Shutdownable; import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Splitter; import org.apache.cassandra.dht.Splitter;
@ -55,10 +56,13 @@ import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.Verb; import org.apache.cassandra.net.Verb;
import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.ReplicationParams;
import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.reads.tracked.TrackedLocalReads; import org.apache.cassandra.service.reads.tracked.TrackedLocalReads;
import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.membership.NodeId;
import org.apache.cassandra.tcm.ownership.ReplicaGroups; import org.apache.cassandra.tcm.ownership.ReplicaGroups;
import org.apache.cassandra.tcm.ownership.VersionedEndpoints; import org.apache.cassandra.tcm.ownership.VersionedEndpoints;
import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.FBUtilities;
@ -66,14 +70,17 @@ import org.apache.cassandra.utils.FBUtilities;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.lang.String.format;
import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
import static org.apache.cassandra.concurrent.ExecutorFactory.SimulatorSemantics.NORMAL; import static org.apache.cassandra.concurrent.ExecutorFactory.SimulatorSemantics.NORMAL;
import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
// TODO (expected): persistence (handle restarts) // TODO (expected): persistence (handle restarts)
// TODO (expected): handle topology changes // TODO (expected): handle topology changes
public class MutationTrackingService public class MutationTrackingService
{ {
private static final ScheduledExecutorPlus executor = executorFactory().scheduled("Mutation-Tracking-Service", NORMAL);
/** /**
* Split ranges into this many shards. * Split ranges into this many shards.
* *
@ -89,6 +96,7 @@ public class MutationTrackingService
private final ConcurrentHashMap<CoordinatorLogId, Shard> log2ShardMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap<CoordinatorLogId, Shard> log2ShardMap = new ConcurrentHashMap<>();
private final ReplicatedOffsetsBroadcaster offsetsBroadcaster = new ReplicatedOffsetsBroadcaster(); private final ReplicatedOffsetsBroadcaster offsetsBroadcaster = new ReplicatedOffsetsBroadcaster();
private final LogStatePersister offsetsPersister = new LogStatePersister();
private final ActiveLogReconciler activeReconciler = new ActiveLogReconciler(); private final ActiveLogReconciler activeReconciler = new ActiveLogReconciler();
private final IncomingMutations incomingMutations = new IncomingMutations(); private final IncomingMutations incomingMutations = new IncomingMutations();
@ -104,13 +112,16 @@ public class MutationTrackingService
if (started) if (started)
return; return;
logger.info("Starting replication tracking service"); prevHostLogId = loadHostLogIdFromSystemTable();
for (KeyspaceMetadata keyspace : metadata.schema.getKeyspaces()) logger.info("Starting mutation tracking service. Previous host log id: {}", prevHostLogId);
if (keyspace.useMutationTracking())
keyspaceShards.put(keyspace.name, KeyspaceShards.make(keyspace, metadata, this::nextHostLogId, this::onNewLog)); if (metadata.myNodeId() != null)
for (KeyspaceShards ks : KeyspaceShards.loadFromSystemTables(metadata, this::nextLogId, this::onNewLog))
keyspaceShards.put(ks.keyspace, ks);
offsetsBroadcaster.start(); offsetsBroadcaster.start();
offsetsPersister.start();
ExpiredStatePurger.instance.register(incomingMutations); ExpiredStatePurger.instance.register(incomingMutations);
@ -124,9 +135,10 @@ public class MutationTrackingService
public void shutdownBlocking() throws InterruptedException public void shutdownBlocking() throws InterruptedException
{ {
offsetsBroadcaster.shutdown(); // TODO: FIXME
offsetsBroadcaster.awaitTermination(1, TimeUnit.MINUTES);
activeReconciler.shutdownBlocking(); activeReconciler.shutdownBlocking();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
ExpiredStatePurger.instance.shutdownBlocking(); ExpiredStatePurger.instance.shutdownBlocking();
} }
@ -165,9 +177,9 @@ public class MutationTrackingService
activeReconciler.schedule(mutationId, onHost, ActiveLogReconciler.Priority.REGULAR); activeReconciler.schedule(mutationId, onHost, ActiveLogReconciler.Priority.REGULAR);
} }
public void updateReplicatedOffsets(String keyspace, Range<Token> range, List<? extends Offsets> offsets, InetAddressAndPort onHost) public void updateReplicatedOffsets(String keyspace, Range<Token> range, List<? extends Offsets> offsets, boolean durable, InetAddressAndPort onHost)
{ {
getOrCreateShards(keyspace).updateReplicatedOffsets(range, offsets, onHost); getOrCreateShards(keyspace).updateReplicatedOffsets(range, offsets, durable, onHost);
} }
public boolean startWriting(Mutation mutation) public boolean startWriting(Mutation mutation)
@ -264,41 +276,41 @@ public class MutationTrackingService
ClusterMetadata csm = ClusterMetadata.current(); ClusterMetadata csm = ClusterMetadata.current();
KeyspaceMetadata ksm = csm.schema.getKeyspaceMetadata(keyspace); KeyspaceMetadata ksm = csm.schema.getKeyspaceMetadata(keyspace);
return keyspaceShards.computeIfAbsent(keyspace, ignore -> KeyspaceShards.make(ksm, csm, this::nextHostLogId, this::onNewLog)); return keyspaceShards.computeIfAbsent(keyspace, ignore -> KeyspaceShards.make(ksm, csm, this::nextLogId, this::onNewLog));
} }
// TODO (expected): durability private long nextLogId()
int nextHostLogId()
{ {
return nextHostLogId.incrementAndGet(); NodeId nodeId = ClusterMetadata.current().myNodeId();
Preconditions.checkNotNull(nodeId);
return CoordinatorLogId.asLong(nodeId.id(), nextHostLogId());
} }
private final AtomicInteger nextHostLogId = new AtomicInteger();
public boolean isDurablyReconciled(String keyspace, ImmutableCoordinatorLogOffsets logOffsets) /*
* Allocate and persist the next host log id.
* We only do this on startup and when rotating logs.
*/
private int nextHostLogId()
{
int nextHostLogId = ++prevHostLogId;
persistHostLogIdToSystemTable(nextHostLogId);
return nextHostLogId;
}
private int prevHostLogId;
public boolean isDurablyReconciled(ImmutableCoordinatorLogOffsets logOffsets)
{ {
// Could pass through SSTable bounds to exclude shards for non-overlapping ranges, but this will mostly be // Could pass through SSTable bounds to exclude shards for non-overlapping ranges, but this will mostly be
// called on flush for L0 SSTables with wide bounds. // called on flush for L0 SSTables with wide bounds.
KeyspaceShards shards = keyspaceShards.get(keyspace);
if (shards == null)
{
logger.debug("Could not find shards for keyspace {}", keyspace);
return false;
}
for (Long logId : logOffsets) for (Long logId : logOffsets)
{ {
CoordinatorLogId coordinatorLogId = new CoordinatorLogId(logId); Shard shard = getShardNullable(new CoordinatorLogId(logId));
CoordinatorLog log = shards.logs.get(coordinatorLogId); if (shard == null)
if (log == null) throw new IllegalStateException("Could not find shard for logId " + logId);
{
logger.warn("Could not determine lifecycle for unknown logId {}, not marking as durably reconciled", coordinatorLogId); if (!shard.isDurablyReconciled(logId, logOffsets))
return false;
}
if (!log.isDurablyReconciled(logOffsets))
return false; return false;
} }
return true; return true;
} }
@ -308,17 +320,15 @@ public class MutationTrackingService
log2ShardMap.put(log.logId, shard); log2ShardMap.put(log.logId, shard);
} }
private static class KeyspaceShards implements Shard.Subscriber private static class KeyspaceShards
{ {
private final String keyspace; private final String keyspace;
private final Map<Range<Token>, Shard> shards; private final Map<Range<Token>, Shard> shards;
private final ReplicaGroups groups; private final ReplicaGroups groups;
private final BiConsumer<Shard, CoordinatorLog> onNewLog;
private transient final Map<Range<PartitionPosition>, Shard> ppShards; private transient final Map<Range<PartitionPosition>, Shard> ppShards;
private transient final Map<CoordinatorLogId, CoordinatorLog> logs;
static KeyspaceShards make(KeyspaceMetadata keyspace, ClusterMetadata cluster, IntSupplier logIdProvider, BiConsumer<Shard, CoordinatorLog> onNewLog) static KeyspaceShards make(KeyspaceMetadata keyspace, ClusterMetadata cluster, LongSupplier logIdProvider, BiConsumer<Shard, CoordinatorLog> onNewLog)
{ {
Preconditions.checkArgument(keyspace.params.replicationType.isTracked()); Preconditions.checkArgument(keyspace.params.replicationType.isTracked());
@ -341,26 +351,23 @@ public class MutationTrackingService
for (Range<Token> tokenRange : ranges) for (Range<Token> tokenRange : ranges)
{ {
shards.put(tokenRange, new Shard(keyspace.name, tokenRange, cluster.myNodeId().id(), participants, forRange.lastModified(), logIdProvider, onNewLog)); shards.put(tokenRange, new Shard(cluster.myNodeId().id(), keyspace.name, tokenRange, participants, logIdProvider, onNewLog));
groups.put(tokenRange, forRange.map(original -> original.withRange(tokenRange))); groups.put(tokenRange, forRange.map(original -> original.withRange(tokenRange)));
} }
}); });
return new KeyspaceShards(keyspace.name, shards, new ReplicaGroups(groups), onNewLog); KeyspaceShards keyspaceShards = new KeyspaceShards(keyspace.name, shards, new ReplicaGroups(groups));
keyspaceShards.persistToSystemTables();
return keyspaceShards;
} }
KeyspaceShards(String keyspace, Map<Range<Token>, Shard> shards, ReplicaGroups groups, BiConsumer<Shard, CoordinatorLog> onNewLog) KeyspaceShards(String keyspace, Map<Range<Token>, Shard> shards, ReplicaGroups groups)
{ {
this.keyspace = keyspace; this.keyspace = keyspace;
this.shards = shards; this.shards = shards;
this.groups = groups; this.groups = groups;
this.onNewLog = onNewLog;
this.logs = new HashMap<>();
HashMap<Range<PartitionPosition>, Shard> ppShards = new HashMap<>(); HashMap<Range<PartitionPosition>, Shard> ppShards = new HashMap<>();
shards.forEach((range, shard) -> { shards.forEach((range, shard) -> ppShards.put(Range.makeRowRange(range), shard));
ppShards.put(Range.makeRowRange(range), shard);
shard.addSubscriber(this);
});
this.ppShards = ppShards; this.ppShards = ppShards;
} }
@ -369,9 +376,9 @@ public class MutationTrackingService
return lookUp(token).nextId(); return lookUp(token).nextId();
} }
void updateReplicatedOffsets(Range<Token> range, List<? extends Offsets> offsets, InetAddressAndPort onHost) void updateReplicatedOffsets(Range<Token> range, List<? extends Offsets> offsets, boolean durable, InetAddressAndPort onHost)
{ {
shards.get(range).updateReplicatedOffsets(offsets, onHost); shards.get(range).updateReplicatedOffsets(offsets, durable, onHost);
} }
boolean startWriting(Mutation mutation) boolean startWriting(Mutation mutation)
@ -430,66 +437,105 @@ public class MutationTrackingService
return shards.get(groups.forRange(token).range()); return shards.get(groups.forRange(token).range());
} }
@Override void persistToSystemTables()
public void onLogCreation(CoordinatorLog log)
{ {
logger.debug("Indexing created log {}", log); for (Shard shard : shards.values()) shard.persistToSystemTables();
logs.put(log.logId, log);
} }
@Override static List<KeyspaceShards> loadFromSystemTables(ClusterMetadata cluster, LongSupplier logIdProvider, BiConsumer<Shard, CoordinatorLog> onNewLog)
public void onSubscribe(CoordinatorLog currentLog)
{ {
logger.debug("Indexing current log {}", currentLog); Map<String, Map<Range<Token>, Shard>> groupedShards = new HashMap<>();
logs.put(currentLog.logId, currentLog); for (Shard shard : Shard.loadFromSystemTables(cluster.myNodeId().id(), logIdProvider, onNewLog))
groupedShards.computeIfAbsent(shard.keyspace, k -> new HashMap<>()).put(shard.range, shard);
List<KeyspaceShards> keyspaceShards = new ArrayList<>();
for (Map.Entry<String, Map<Range<Token>, Shard>> entry : groupedShards.entrySet())
{
ReplicationParams params = cluster.schema.getKeyspaceMetadata(entry.getKey()).params.replication;
ReplicaGroups originalGroups = cluster.placements.get(params).writes; // prior to splitting
Map<Range<Token>, VersionedEndpoints.ForRange> splitGroups = new HashMap<>();
for (Range<Token> splitRange : entry.getValue().keySet())
splitGroups.put(splitRange, originalGroups.matchRange(splitRange));
keyspaceShards.add(new KeyspaceShards(entry.getKey(), entry.getValue(), new ReplicaGroups(splitGroups)));
}
return keyspaceShards;
} }
} }
// TODO (later): a more intelligent heuristic for offsets included in broadcasts private static final String HOST_LOG_ID_KEY = "local";
private static class ReplicatedOffsetsBroadcaster implements Runnable, Shutdownable
{
private static final ScheduledExecutorPlus executor =
executorFactory().scheduled("Replicated-Offsets-Broadcaster", NORMAL);
private static final String INSERT_QUERY =
format("INSERT INTO %s.%s (key, host_log_id) VALUES (?, ?)",
SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.HOST_LOG_ID);
static void persistHostLogIdToSystemTable(int hostLogId)
{
executeInternal(INSERT_QUERY, HOST_LOG_ID_KEY, hostLogId);
}
private static final String SELECT_QUERY =
format("SELECT * FROM %s.%s WHERE key = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.HOST_LOG_ID);
static int loadHostLogIdFromSystemTable()
{
UntypedResultSet rows = executeInternal(SELECT_QUERY, HOST_LOG_ID_KEY);
if (rows.isEmpty())
return 0;
return rows.one().getInt("host_log_id");
}
// TODO (later): a more intelligent heuristic for offsets included in broadcasts
private static class ReplicatedOffsetsBroadcaster
{
// TODO (later): a more intelligent heuristic for scheduling broadcasts // TODO (later): a more intelligent heuristic for scheduling broadcasts
private static final long BROADCAST_INTERVAL_MILLIS = 200; private static final long TRANSIENT_BROADCAST_INTERVAL_MILLIS = 200;
private static final long DURABLE_BROADCAST_INTERVAL_MILLIS = 60_000;
void start() void start()
{ {
executor.scheduleWithFixedDelay(this, BROADCAST_INTERVAL_MILLIS, BROADCAST_INTERVAL_MILLIS, TimeUnit.MILLISECONDS); executor.scheduleWithFixedDelay(() -> run(false),
TRANSIENT_BROADCAST_INTERVAL_MILLIS,
TRANSIENT_BROADCAST_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS);
executor.scheduleWithFixedDelay(() -> run(true),
DURABLE_BROADCAST_INTERVAL_MILLIS,
DURABLE_BROADCAST_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS);
} }
@Override public void run(boolean durable)
public boolean isTerminated()
{ {
return executor.isTerminated(); MutationTrackingService.instance.forEachKeyspace(ks -> run(ks, durable));
} }
@Override private void run(KeyspaceShards shards, boolean durable)
public void shutdown()
{ {
executor.shutdown(); shards.forEachShard(sh -> run(sh, durable));
} }
@Override private void run(Shard shard, boolean durable)
public Object shutdownNow()
{ {
return executor.shutdownNow(); BroadcastLogOffsets replicatedOffsets = shard.collectReplicatedOffsets(durable);
} if (replicatedOffsets.isEmpty())
return;
@Override Message<BroadcastLogOffsets> message = Message.out(Verb.BROADCAST_LOG_OFFSETS, replicatedOffsets);
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
{
return executor.awaitTermination(timeout, units);
}
public void shutdownBlocking() throws InterruptedException for (InetAddressAndPort target : shard.remoteReplicas())
if (FailureDetector.instance.isAlive(target))
MessagingService.instance().send(message, target);
}
}
private static class LogStatePersister implements Runnable
{
// TODO (expected): consider a different interval
private static final long PERSIST_INTERVAL_MINUTES = 1;
void start()
{ {
if (!executor.isTerminated()) executor.scheduleWithFixedDelay(this, PERSIST_INTERVAL_MINUTES, PERSIST_INTERVAL_MINUTES, TimeUnit.MINUTES);
{
executor.shutdown();
executor.awaitTermination(1, MINUTES);
}
} }
@Override @Override
@ -505,22 +551,21 @@ public class MutationTrackingService
private void run(Shard shard) private void run(Shard shard)
{ {
BroadcastLogOffsets replicatedOffsets = shard.collectReplicatedOffsets(); shard.updateLogsInSystemTable();
if (replicatedOffsets.isEmpty())
return;
Message<BroadcastLogOffsets> 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 persistLogStateForTesting()
{
offsetsPersister.run();
}
@VisibleForTesting @VisibleForTesting
public void broadcastOffsetsForTesting() public void broadcastOffsetsForTesting()
{ {
offsetsBroadcaster.run(); offsetsBroadcaster.run(false);
offsetsBroadcaster.run(true);
} }
@VisibleForTesting @VisibleForTesting

View File

@ -17,6 +17,14 @@
*/ */
package org.apache.cassandra.replication; package org.apache.cassandra.replication;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import com.google.common.base.Preconditions;
import org.agrona.collections.Int2ObjectHashMap; import org.agrona.collections.Int2ObjectHashMap;
import org.agrona.collections.IntObjConsumer; import org.agrona.collections.IntObjConsumer;
@ -25,24 +33,93 @@ import org.agrona.collections.IntObjConsumer;
*/ */
public class Node2OffsetsMap public class Node2OffsetsMap
{ {
private final Int2ObjectHashMap<Offsets> map = new Int2ObjectHashMap<>(); private final Int2ObjectHashMap<Offsets.Mutable> offsetsMap;
public Node2OffsetsMap()
{
offsetsMap = new Int2ObjectHashMap<>(8, 0.65f, false);
}
public static Node2OffsetsMap forParticipants(CoordinatorLogId logId, Participants participants)
{
Node2OffsetsMap map = new Node2OffsetsMap();
for (int i = 0; i < participants.size(); i++)
map.set(participants.get(i), new Offsets.Mutable(logId));
return map;
}
void set(int node, Offsets.Mutable offsets)
{
offsetsMap.put(node, offsets);
}
@Nonnull
Offsets.Mutable get(int node)
{
return Preconditions.checkNotNull(offsetsMap.get(node));
}
Offsets.Mutable intersection()
{
Iterator<Offsets.Mutable> iter = offsetsMap.values().iterator();
Preconditions.checkArgument(iter.hasNext());
if (offsetsMap.size() == 1)
return Offsets.Mutable.copy(iter.next());
Offsets.Mutable intersection = iter.next();
while (iter.hasNext())
intersection = Offsets.Mutable.intersection(intersection, iter.next());
return intersection;
}
public void add(int node, Offsets offsets) public void add(int node, Offsets offsets)
{ {
Offsets.Mutable current = (Offsets.Mutable) map.get(node); Offsets.Mutable current = offsetsMap.get(node);
if (current != null) if (current != null)
current.addAll(offsets); current.addAll(offsets);
else else
map.put(node, Offsets.Mutable.copy(offsets)); offsetsMap.put(node, Offsets.Mutable.copy(offsets));
} }
public void forEach(IntObjConsumer<Offsets> consumer) public void forEach(IntObjConsumer<Offsets.Mutable> consumer)
{ {
map.forEachInt(consumer); offsetsMap.forEachInt(consumer);
} }
public void clear() public void clear()
{ {
map.clear(); offsetsMap.clear();
}
public int size()
{
return offsetsMap.size();
}
void convertToPrimitiveMap(Map<Integer, List<Integer>> into)
{
for (Int2ObjectHashMap<Offsets.Mutable>.EntryIterator iter = offsetsMap.entrySet().iterator(); iter.hasNext();)
{
iter.next();
into.put(iter.getIntKey(), iter.getValue().asList());
}
}
static Node2OffsetsMap fromPrimitiveMap(CoordinatorLogId logId, Map<Integer, List<Integer>> from)
{
Node2OffsetsMap map = new Node2OffsetsMap();
for (Map.Entry<Integer, List<Integer>> entry : from.entrySet())
map.set(entry.getKey(), Offsets.fromList(logId, entry.getValue()));
return map;
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Node2OffsetsMap))
return false;
Node2OffsetsMap that = (Node2OffsetsMap) o;
return this.offsetsMap.equals(that.offsetsMap);
} }
} }

View File

@ -21,6 +21,7 @@ import com.google.common.base.Function;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.agrona.collections.IntArrayList;
import org.apache.cassandra.db.Digest; import org.apache.cassandra.db.Digest;
import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.IVersionedSerializer;
@ -34,6 +35,7 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
public abstract class Offsets implements Iterable<ShortMutationId> public abstract class Offsets implements Iterable<ShortMutationId>
{ {
@ -200,6 +202,19 @@ public abstract class Offsets implements Iterable<ShortMutationId>
digest.updateWithInt(bounds[i]); digest.updateWithInt(bounds[i]);
} }
List<Integer> asList()
{
return new IntArrayList(Arrays.copyOf(bounds, size), size, IntArrayList.DEFAULT_NULL_VALUE);
}
static Mutable fromList(CoordinatorLogId logId, List<Integer> list)
{
int[] bounds = new int[list.size()];
for (int i = 0; i < list.size(); i++)
bounds[i] = list.get(i);
return new Mutable(logId, bounds, bounds.length);
}
static abstract class AbstractMutable<T extends AbstractMutable<T>> extends Offsets implements OffsetReciever static abstract class AbstractMutable<T extends AbstractMutable<T>> extends Offsets implements OffsetReciever
{ {
AbstractMutable(RangeIterator rangeIterator) AbstractMutable(RangeIterator rangeIterator)
@ -380,6 +395,11 @@ public abstract class Offsets implements Iterable<ShortMutationId>
// the offset is before all existing ranges, in-between two, or after all exsiting ranges // the offset is before all existing ranges, in-between two, or after all exsiting ranges
} }
public void add(int... offsets)
{
for (int offset : offsets) add(offset);
}
private enum AddAction private enum AddAction
{ {
INSERT, MOVE, INCLUDE; INSERT, MOVE, INCLUDE;
@ -673,6 +693,19 @@ public abstract class Offsets implements Iterable<ShortMutationId>
{ {
return Offsets.intersection(a, b, adaptor); return Offsets.intersection(a, b, adaptor);
} }
public static Mutable intersection(Offsets.Mutable... offsets)
{
Preconditions.checkArgument(offsets.length > 0);
if (offsets.length == 1)
return copy(offsets[0]);
Mutable intersection = offsets[0];
for (int i = 1; i < offsets.length; i++)
intersection = intersection(intersection, offsets[i]);
return intersection;
}
} }
public static class Immutable extends Offsets public static class Immutable extends Offsets

View File

@ -19,6 +19,9 @@ package org.apache.cassandra.replication;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Set;
import org.agrona.collections.IntHashSet;
public class Participants public class Participants
{ {
@ -63,4 +66,21 @@ public class Participants
{ {
return Arrays.toString(hosts); return Arrays.toString(hosts);
} }
@Override
public boolean equals(Object o)
{
if (!(o instanceof Participants))
return false;
Participants that = (Participants) o;
return Arrays.equals(this.hosts, that.hosts);
}
Set<Integer> asSet()
{
IntHashSet set = new IntHashSet(hosts.length);
for (int host : hosts)
set.add(host);
return set;
}
} }

View File

@ -18,72 +18,100 @@
package org.apache.cassandra.replication; package org.apache.cassandra.replication;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.IntSupplier; import java.util.function.LongSupplier;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.agrona.collections.IntArrayList; import org.agrona.collections.IntArrayList;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token; import org.apache.cassandra.dht.Token;
import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.replication.CoordinatorLog.CoordinatorLogPrimary;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.tcm.membership.NodeId;
import org.jctools.maps.NonBlockingHashMapLong; import org.jctools.maps.NonBlockingHashMapLong;
import static java.lang.String.format;
import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
public class Shard public class Shard
{ {
private final String keyspace; private static final Logger logger = LoggerFactory.getLogger(Shard.class);
private final Range<Token> tokenRange;
private final int localHostId; final int localNodeId;
private final Participants participants; final String keyspace;
private final Epoch sinceEpoch; final Range<Token> range;
final Participants participants;
private final LongSupplier logIdProvider;
private final BiConsumer<Shard, CoordinatorLog> onNewLog; private final BiConsumer<Shard, CoordinatorLog> onNewLog;
private final NonBlockingHashMapLong<CoordinatorLog> logs; private final NonBlockingHashMapLong<CoordinatorLog> logs;
// TODO (expected): add support for log rotation private volatile CoordinatorLogPrimary currentLocalLog;
private final CoordinatorLog.CoordinatorLogPrimary currentLocalLog;
private final List<Subscriber> subscribers = new ArrayList<>(); Shard(int localNodeId,
String keyspace,
public interface Subscriber Range<Token> range,
{
default void onLogCreation(CoordinatorLog log) {}
default void onSubscribe(CoordinatorLog currentLog) {}
}
Shard(String keyspace,
Range<Token> tokenRange,
int localHostId,
Participants participants, Participants participants,
Epoch sinceEpoch, List<CoordinatorLog> logs,
IntSupplier logIdProvider, LongSupplier logIdProvider,
BiConsumer<Shard, CoordinatorLog> onNewLog) BiConsumer<Shard, CoordinatorLog> onNewLog)
{ {
Preconditions.checkArgument(participants.contains(localHostId)); Preconditions.checkArgument(participants.contains(localNodeId));
this.localNodeId = localNodeId;
this.keyspace = keyspace; this.keyspace = keyspace;
this.tokenRange = tokenRange; this.range = range;
this.localHostId = localHostId;
this.participants = participants; this.participants = participants;
this.sinceEpoch = sinceEpoch; this.logIdProvider = logIdProvider;
this.logs = new NonBlockingHashMapLong<>(); this.logs = new NonBlockingHashMapLong<>();
this.onNewLog = onNewLog; this.onNewLog = onNewLog;
this.currentLocalLog = startNewLog(localHostId, logIdProvider.getAsInt(), participants); for (CoordinatorLog log : logs)
CoordinatorLogId logId = currentLocalLog.logId; {
Preconditions.checkArgument(!logId.isNone()); this.logs.put(log.logId.asLong(), log);
logs.put(logId.asLong(), currentLocalLog); onNewLog.accept(Shard.this, log);
}
this.currentLocalLog = createNewPrimaryLog();
}
Shard(int localNodeId, String keyspace, Range<Token> range, Participants participants, LongSupplier logIdProvider, BiConsumer<Shard, CoordinatorLog> onNewLog)
{
this(localNodeId, keyspace, range, participants, Collections.emptyList(), logIdProvider, onNewLog);
} }
MutationId nextId() MutationId nextId()
{ {
return currentLocalLog.nextId(); MutationId nextId = currentLocalLog.nextId();
if (nextId != null)
return nextId;
return maybeRotateLocalLogAndGetNextId();
}
// if ids overflow, we need to rotate the local log
synchronized private MutationId maybeRotateLocalLogAndGetNextId()
{
MutationId nextId = currentLocalLog.nextId();
if (nextId != null) // another thread got to rotate before us
return nextId;
CoordinatorLogId oldLogId = currentLocalLog.logId;
currentLocalLog = createNewPrimaryLog();
logger.info("Rotated primary log for {}/{} from {} to {}", keyspace, range, oldLogId, currentLocalLog.logId);
return nextId();
} }
void receivedWriteResponse(ShortMutationId mutationId, InetAddressAndPort fromHost) void receivedWriteResponse(ShortMutationId mutationId, InetAddressAndPort fromHost)
@ -92,11 +120,11 @@ public class Shard
getOrCreate(mutationId).receivedWriteResponse(mutationId, fromHostId); getOrCreate(mutationId).receivedWriteResponse(mutationId, fromHostId);
} }
void updateReplicatedOffsets(List<? extends Offsets> offsets, InetAddressAndPort onHost) void updateReplicatedOffsets(List<? extends Offsets> offsets, boolean durable, InetAddressAndPort onHost)
{ {
int onHostId = ClusterMetadata.current().directory.peerId(onHost).id(); int onHostId = ClusterMetadata.current().directory.peerId(onHost).id();
for (Offsets logOffsets : offsets) for (Offsets logOffsets : offsets)
getOrCreate(logOffsets.logId()).updateReplicatedOffsets(logOffsets, onHostId); getOrCreate(logOffsets.logId()).updateReplicatedOffsets(logOffsets, durable, onHostId);
} }
boolean startWriting(Mutation mutation) boolean startWriting(Mutation mutation)
@ -143,39 +171,31 @@ public class Shard
for (int i = 0, size = participants.size(); i < size; ++i) for (int i = 0, size = participants.size(); i < size; ++i)
{ {
int hostId = participants.get(i); int hostId = participants.get(i);
if (hostId != localHostId) if (hostId != localNodeId)
replicas.add(ClusterMetadata.current().directory.endpoint(new NodeId(hostId))); replicas.add(ClusterMetadata.current().directory.endpoint(new NodeId(hostId)));
} }
return replicas; return replicas;
} }
boolean isDurablyReconciled(long logId, CoordinatorLogOffsets<?> logOffsets)
{
return logs.get(logId).isDurablyReconciled(logOffsets);
}
/** /**
* Collects replicated offsets for the logs owned by this coordinator on this shard. * Collects replicated offsets for the logs owned by this coordinator on this shard.
*/ */
BroadcastLogOffsets collectReplicatedOffsets() BroadcastLogOffsets collectReplicatedOffsets(boolean durable)
{ {
List<Offsets.Immutable> offsets = new ArrayList<>(); List<Offsets.Immutable> offsets = new ArrayList<>();
for (CoordinatorLog log : logs.values()) for (CoordinatorLog log : logs.values())
{ {
Offsets.Immutable logOffsets = log.collectReplicatedOffsets(); Offsets.Immutable logOffsets = log.collectReplicatedOffsets(durable);
if (logOffsets != null) if (logOffsets != null)
offsets.add(logOffsets); offsets.add(logOffsets);
} }
return new BroadcastLogOffsets(keyspace, tokenRange, offsets); return new BroadcastLogOffsets(keyspace, range, offsets, durable);
}
/**
* Creates a new coordinator log for this host. Primarily on Shard init (node startup or topology change).
* Also on keyspace creation.
*/
private CoordinatorLog.CoordinatorLogPrimary startNewLog(int localHostId, int hostLogId, Participants participants)
{
CoordinatorLogId logId = new CoordinatorLogId(localHostId, hostLogId);
CoordinatorLog.CoordinatorLogPrimary log =
new CoordinatorLog.CoordinatorLogPrimary(localHostId, logId, participants);
onNewLog.accept(this, log);
return log;
} }
private CoordinatorLog getOrCreate(Mutation mutation) private CoordinatorLog getOrCreate(Mutation mutation)
@ -203,27 +223,63 @@ public class Shard
private CoordinatorLog getOrCreate(long logId) private CoordinatorLog getOrCreate(long logId)
{ {
CoordinatorLog log = logs.get(logId); CoordinatorLog log = logs.get(logId);
if (log != null) return log != null ? log : createNewLog(logId);
return log;
CoordinatorLog newLog = logs.computeIfAbsent(logId, ignore -> CoordinatorLog.create(localHostId, new CoordinatorLogId(logId), participants));
onNewLog.accept(this, newLog);
for (Subscriber subscriber : subscribers)
subscriber.onLogCreation(newLog);
return newLog;
} }
public void addSubscriber(Subscriber subscriber) /**
* Creates a new coordinator log for this host. Primarily on Shard init (node startup or topology change) and on keyspace creation.
*/
private CoordinatorLog createNewLog(long logId)
{ {
subscriber.onSubscribe(currentLocalLog); CoordinatorLog next = CoordinatorLog.create(keyspace, range, localNodeId, new CoordinatorLogId(logId), participants);
subscribers.add(subscriber); CoordinatorLog prev = logs.putIfAbsent(logId, next);
if (null == prev) onNewLog.accept(this, next);
return null != prev ? prev : next;
} }
private CoordinatorLog create(long logId) private CoordinatorLogPrimary createNewPrimaryLog()
{ {
CoordinatorLog log = CoordinatorLog.create(localHostId, new CoordinatorLogId(logId), participants); return (CoordinatorLogPrimary) createNewLog(logIdProvider.getAsLong());
onNewLog.accept(this, log); }
for (Subscriber subscriber : subscribers)
subscriber.onLogCreation(log); /*
return log; * Persist to / load from system table.
*/
private static final String INSERT_QUERY =
format("INSERT INTO %s.%s (keyspace_name, range_start, range_end, participants) VALUES (?, ?, ?, ?)",
SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.SHARDS);
void persistToSystemTables()
{
executeInternal(INSERT_QUERY, keyspace, range.left.toString(), range.right.toString(), participants.asSet());
for (CoordinatorLog log : logs.values())
log.persistToSystemTable();
}
void updateLogsInSystemTable()
{
for (CoordinatorLog log : logs.values())
log.updateLogsInSystemTable();
}
private static final String SELECT_QUERY =
format("SELECT * FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, SystemKeyspace.SHARDS);
static ArrayList<Shard> loadFromSystemTables(int localNodeId, LongSupplier logIdProvider, BiConsumer<Shard, CoordinatorLog> onNewLog)
{
Token.TokenFactory factory = ClusterMetadata.current().partitioner.getTokenFactory();
ArrayList<Shard> shards = new ArrayList<>();
for (UntypedResultSet.Row row : executeInternal(SELECT_QUERY))
{
String keyspace = row.getString("keyspace_name");
String rangeStart = row.getString("range_start");
String rangeEnd = row.getString("range_end");
Range<Token> range = new Range<>(factory.fromString(rangeStart), factory.fromString(rangeEnd));
Set<Integer> participants = row.getFrozenSet("participants", Int32Type.instance);
List<CoordinatorLog> logs = CoordinatorLog.loadFromSystemTable(keyspace, range, localNodeId);
shards.add(new Shard(localNodeId, keyspace, range, new Participants(participants), logs, logIdProvider, onNewLog));
}
return shards;
} }
} }

View File

@ -23,6 +23,7 @@ import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
@ -197,4 +198,41 @@ public class UnreconciledMutations
return collect(statesSet.subSet(start, end), tableId, includePending, into); return collect(statesSet.subSet(start, end), tableId, includePending, into);
} }
} }
@VisibleForTesting
boolean equalsForTesting(UnreconciledMutations other)
{
return this.statesMap.equals(other.statesMap) && this.statesSet.equals(other.statesSet);
}
@VisibleForTesting
void addDirectly(Mutation mutation)
{
Entry entry = Entry.create(mutation);
entry.visibility = Visibility.VISIBLE;
statesMap.put(entry.offset, entry);
statesSet.add(entry);
}
static UnreconciledMutations loadFromJournal(Node2OffsetsMap witnessedOffsets, int localNodeId)
{
UnreconciledMutations result = new UnreconciledMutations();
Offsets.Mutable witnessed = witnessedOffsets.get(localNodeId);
Offsets.Mutable reconciled = witnessedOffsets.intersection();
// difference between locally witnessed offsets and fully reconciled ones is all the ids
// that need to be loaded into UnreconciledMutations index
Offsets.RangeIterator iter = Offsets.difference(witnessed.rangeIterator(), reconciled.rangeIterator());
while (iter.tryAdvance())
{
for (int offset = iter.start(), end = iter.end(); offset <= end; offset++)
{
ShortMutationId id = new ShortMutationId(witnessed.logId, offset);
result.addDirectly(MutationJournal.instance.read(id));
}
}
return result;
}
} }

View File

@ -285,10 +285,10 @@ public class OfflineClusterMetadataDump implements Runnable
} }
// Find and import SSTables for metadata_snapshots // Find and import SSTables for metadata_snapshots
Set<String> snapshotTablePaths = findTablePaths(SystemKeyspace.SNAPSHOT_TABLE_NAME, SchemaConstants.SYSTEM_KEYSPACE_NAME); Set<String> snapshotTablePaths = findTablePaths(SystemKeyspace.METADATA_SNAPSHOTS, SchemaConstants.SYSTEM_KEYSPACE_NAME);
if (!snapshotTablePaths.isEmpty()) if (!snapshotTablePaths.isEmpty())
{ {
ColumnFamilyStore snapshotCfs = ks.getColumnFamilyStore(SystemKeyspace.SNAPSHOT_TABLE_NAME); ColumnFamilyStore snapshotCfs = ks.getColumnFamilyStore(SystemKeyspace.METADATA_SNAPSHOTS);
snapshotCfs.importNewSSTables(snapshotTablePaths, false, false, false, false, false, false, true); snapshotCfs.importNewSSTables(snapshotTablePaths, false, false, false, false, false, false, true);
if (verbose) if (verbose)
{ {

View File

@ -39,7 +39,7 @@ import org.apache.cassandra.tcm.log.LogState;
import org.apache.cassandra.tcm.transformations.CustomTransformation; import org.apache.cassandra.tcm.transformations.CustomTransformation;
import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.FBUtilities;
import static org.apache.cassandra.db.SystemKeyspace.SNAPSHOT_TABLE_NAME; import static org.apache.cassandra.db.SystemKeyspace.METADATA_SNAPSHOTS;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -184,7 +184,7 @@ public class SystemKeyspaceStorageTest extends CoordinatorPathTestBase
public static void deleteSnapshot(long epoch) public static void deleteSnapshot(long epoch)
{ {
String query = String.format("DELETE FROM %s.%s WHERE epoch = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); String query = String.format("DELETE FROM %s.%s WHERE epoch = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, METADATA_SNAPSHOTS);
QueryProcessor.executeInternal(query, epoch); QueryProcessor.executeInternal(query, epoch);
} }
} }

View File

@ -26,7 +26,7 @@ import org.apache.cassandra.utils.TimeUUID;
import static org.apache.cassandra.db.SystemKeyspace.BATCHES; import static org.apache.cassandra.db.SystemKeyspace.BATCHES;
import static org.apache.cassandra.db.SystemKeyspace.LOCAL; import static org.apache.cassandra.db.SystemKeyspace.LOCAL;
import static org.apache.cassandra.db.SystemKeyspace.METADATA_LOG; import static org.apache.cassandra.db.SystemKeyspace.METADATA_LOG;
import static org.apache.cassandra.db.SystemKeyspace.SNAPSHOT_TABLE_NAME; import static org.apache.cassandra.db.SystemKeyspace.METADATA_SNAPSHOTS;
import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME; import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME;
public class SystemKeyspaceQueryTest extends CQLTester public class SystemKeyspaceQueryTest extends CQLTester
@ -44,7 +44,7 @@ public class SystemKeyspaceQueryTest extends CQLTester
assertRowCountNet(executeNet(String.format("SELECT * FROM %s.%s WHERE epoch = 1", assertRowCountNet(executeNet(String.format("SELECT * FROM %s.%s WHERE epoch = 1",
SYSTEM_KEYSPACE_NAME, METADATA_LOG)), 0); SYSTEM_KEYSPACE_NAME, METADATA_LOG)), 0);
assertRowCountNet(executeNet(String.format("SELECT * FROM %s.%s WHERE epoch = 1", assertRowCountNet(executeNet(String.format("SELECT * FROM %s.%s WHERE epoch = 1",
SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME)), 0); SYSTEM_KEYSPACE_NAME, METADATA_SNAPSHOTS)), 0);
// system.batches table uses LocalPartitioner // system.batches table uses LocalPartitioner
assertRowCountNet(executeNet(String.format("SELECT * FROM %s.%s WHERE id = %s", assertRowCountNet(executeNet(String.format("SELECT * FROM %s.%s WHERE id = %s",
SYSTEM_KEYSPACE_NAME, BATCHES, SYSTEM_KEYSPACE_NAME, BATCHES,

View File

@ -141,14 +141,17 @@ public class CoordinatorLogOffsetsLifecycleTest
{ {
String ks = nextKeyspaceName(); String ks = nextKeyspaceName();
String tbl = "tbl"; String tbl = "tbl";
TableMetadata tableMetadata = TableMetadata.builder(ks, tbl) TableParams tableParams =
.addPartitionKeyColumn("k", Int32Type.instance) TableParams.builder()
.addRegularColumn("v", Int32Type.instance) .memtable(MemtableParams.get(memtableConfig))
.build(); .build();
TableParams tableParams = tableMetadata.params; TableMetadata tableMetadata =
tableParams = tableParams.unbuild().memtable(MemtableParams.get(memtableConfig)).build(); TableMetadata.builder(ks, tbl)
tableMetadata = tableMetadata.withSwapped(tableParams); .addPartitionKeyColumn("k", Int32Type.instance)
.addRegularColumn("v", Int32Type.instance)
.params(tableParams)
.build();
SchemaLoader.createKeyspace(ks, KeyspaceParams.simple(1, ReplicationType.tracked), tableMetadata); SchemaLoader.createKeyspace(ks, KeyspaceParams.simple(1, ReplicationType.tracked), tableMetadata);
@ -183,6 +186,7 @@ public class CoordinatorLogOffsetsLifecycleTest
// flush 1 // flush 1
{ {
MutationTrackingService.instance.persistLogStateForTesting();
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS); cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS);
View view = cfs.getTracker().getView(); View view = cfs.getTracker().getView();
@ -216,6 +220,7 @@ public class CoordinatorLogOffsetsLifecycleTest
// flush 2 // flush 2
{ {
MutationTrackingService.instance.persistLogStateForTesting();
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS); cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS);
View view = cfs.getTracker().getView(); View view = cfs.getTracker().getView();

View File

@ -20,8 +20,11 @@ package org.apache.cassandra.replication;
import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.commitlog.CommitLog;
import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.dht.Murmur3Partitioner; 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.distributed.test.log.ClusterMetadataTestHelper;
import org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils; import org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils;
import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.InetAddressAndPort;
@ -49,7 +52,10 @@ import org.apache.cassandra.net.MessagingService;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -263,6 +269,7 @@ public class CoordinatorLogOffsetsTest
ClusterMetadataTestHelper.createKeyspace(ks, KeyspaceParams.simple(3, ReplicationType.tracked)); ClusterMetadataTestHelper.createKeyspace(ks, KeyspaceParams.simple(3, ReplicationType.tracked));
ClusterMetadataTestHelper.commit(new AlterSchema(SchemaTransformations.addTable(tableMetadata, false))); ClusterMetadataTestHelper.commit(new AlterSchema(SchemaTransformations.addTable(tableMetadata, false)));
CommitLog.instance.start();
MutationTrackingService.instance.start(metadata); MutationTrackingService.instance.start(metadata);
// Eventually, will also run perturbations before checking isReconciled (like log truncation, durability, etc.) // Eventually, will also run perturbations before checking isReconciled (like log truncation, durability, etc.)
@ -276,11 +283,17 @@ public class CoordinatorLogOffsetsTest
MutationTrackingService.instance.finishWriting(mutation); MutationTrackingService.instance.finishWriting(mutation);
MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr2); MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr2);
MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr3); MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr3);
MutationTrackingService.instance.persistLogStateForTesting();
ImmutableCoordinatorLogOffsets logOffsets = new ImmutableCoordinatorLogOffsets.Builder() ImmutableCoordinatorLogOffsets logOffsets = new ImmutableCoordinatorLogOffsets.Builder()
.add(mutation.id()) .add(mutation.id())
.build(); .build();
Assertions.assertThat(MutationTrackingService.instance.isDurablyReconciled(ks, logOffsets)).isTrue(); Range<Token> range = getShardRange(mutation);
List<? extends Offsets> offsets = Collections.singletonList(logOffsets.offsets(mutation.id().logId()));
MutationTrackingService.instance.updateReplicatedOffsets(ks, range, offsets, true, addr2);
MutationTrackingService.instance.updateReplicatedOffsets(ks, range, offsets, true, addr3);
Assertions.assertThat(MutationTrackingService.instance.isDurablyReconciled(logOffsets)).isTrue();
} }
// Applied locally but not on remote replicas // Applied locally but not on remote replicas
@ -288,11 +301,12 @@ public class CoordinatorLogOffsetsTest
Mutation mutation = MutationTrackingUtils.createMutation(tableMetadata, 2, 2); Mutation mutation = MutationTrackingUtils.createMutation(tableMetadata, 2, 2);
MutationTrackingService.instance.startWriting(mutation); MutationTrackingService.instance.startWriting(mutation);
MutationTrackingService.instance.finishWriting(mutation); MutationTrackingService.instance.finishWriting(mutation);
MutationTrackingService.instance.persistLogStateForTesting();
ImmutableCoordinatorLogOffsets logOffsets = new ImmutableCoordinatorLogOffsets.Builder() ImmutableCoordinatorLogOffsets logOffsets = new ImmutableCoordinatorLogOffsets.Builder()
.add(mutation.id()) .add(mutation.id())
.build(); .build();
Assertions.assertThat(MutationTrackingService.instance.isDurablyReconciled(ks, logOffsets)).isFalse(); Assertions.assertThat(MutationTrackingService.instance.isDurablyReconciled(logOffsets)).isFalse();
} }
// Applied on remote replicas but not locally // Applied on remote replicas but not locally
@ -302,11 +316,18 @@ public class CoordinatorLogOffsetsTest
MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr2); MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr2);
MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr3); MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr3);
MutationTrackingService.instance.persistLogStateForTesting();
ImmutableCoordinatorLogOffsets logOffsets = new ImmutableCoordinatorLogOffsets.Builder() ImmutableCoordinatorLogOffsets logOffsets = new ImmutableCoordinatorLogOffsets.Builder()
.add(mutation.id()) .add(mutation.id())
.build(); .build();
Assertions.assertThat(MutationTrackingService.instance.isDurablyReconciled(ks, logOffsets)).isFalse();
Range<Token> range = getShardRange(mutation);
List<? extends Offsets> offsets = Collections.singletonList(logOffsets.offsets(mutation.id().logId()));
MutationTrackingService.instance.updateReplicatedOffsets(ks, range, offsets, true, addr2);
MutationTrackingService.instance.updateReplicatedOffsets(ks, range, offsets, true, addr3);
Assertions.assertThat(MutationTrackingService.instance.isDurablyReconciled(logOffsets)).isFalse();
} }
// If no replicas are aware of a log, it should be considered unreconciled out of caution // If no replicas are aware of a log, it should be considered unreconciled out of caution
@ -317,6 +338,7 @@ public class CoordinatorLogOffsetsTest
MutationTrackingService.instance.finishWriting(mutation); MutationTrackingService.instance.finishWriting(mutation);
MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr2); MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr2);
MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr3); MutationTrackingService.instance.receivedWriteResponse(mutation.id(), addr3);
MutationTrackingService.instance.persistLogStateForTesting();
MutationId fakeMutationId = new MutationId(CoordinatorLogId.asLong(111, 222), MutationId.sequenceId(333, 444)); MutationId fakeMutationId = new MutationId(CoordinatorLogId.asLong(111, 222), MutationId.sequenceId(333, 444));
Assertions.assertThat(metadata.directory.version(new NodeId(fakeMutationId.hostId()))).isNull(); Assertions.assertThat(metadata.directory.version(new NodeId(fakeMutationId.hostId()))).isNull();
@ -326,9 +348,22 @@ public class CoordinatorLogOffsetsTest
ImmutableCoordinatorLogOffsets.Builder logOffsetsBuilder = new ImmutableCoordinatorLogOffsets.Builder(); ImmutableCoordinatorLogOffsets.Builder logOffsetsBuilder = new ImmutableCoordinatorLogOffsets.Builder();
logOffsetsBuilder.add(fakeMutationId); logOffsetsBuilder.add(fakeMutationId);
Assertions.assertThat(MutationTrackingService.instance.isDurablyReconciled(ks, logOffsetsBuilder.build())).isFalse(); Assertions.assertThatThrownBy(() -> MutationTrackingService.instance.isDurablyReconciled(logOffsetsBuilder.build()))
.hasSameClassAs(new IllegalStateException())
.hasMessageMatching("Could not find shard for logId \\d+");
} }
MutationTrackingService.instance.shutdownBlocking(); MutationTrackingService.instance.shutdownBlocking();
CommitLog.instance.stopUnsafe(true);
}
private Range<Token> getShardRange(Mutation mutation)
{
Map<String, Range<Token>> ksRanges = new HashMap<>();
MutationTrackingService.instance.forEachKeyspace(shards -> {
Shard shard = shards.lookUp(mutation);
ksRanges.put(shard.keyspace, shard.range);
});
return ksRanges.get(mutation.getKeyspaceName());
} }
} }

View File

@ -21,7 +21,6 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.Assert;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@ -30,6 +29,7 @@ import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.RowUpdateBuilder; import org.apache.cassandra.db.RowUpdateBuilder;
import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.ByteOrderedPartitioner; import org.apache.cassandra.dht.ByteOrderedPartitioner;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token; import org.apache.cassandra.dht.Token;
import org.apache.cassandra.replication.CoordinatorLog.CoordinatorLogPrimary; import org.apache.cassandra.replication.CoordinatorLog.CoordinatorLogPrimary;
import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.KeyspaceParams;
@ -38,11 +38,21 @@ import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.ByteBufferUtil;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class CoordinatorLogTest public class CoordinatorLogTest
{ {
private static final int LOCAL_HOST_ID = 1; private static final int LOCAL_HOST_ID = 1;
private static final CoordinatorLogId LOG_ID = new CoordinatorLogId(LOCAL_HOST_ID, 1); private static final int REMOTE_HOST_ID_1 = 2;
private static final Participants PARTICIPANTS = new Participants(List.of(LOCAL_HOST_ID, 2, 3)); private static final int REMOTE_HOST_ID_2 = 3;
private static final CoordinatorLogId LOCAL_LOG_ID = new CoordinatorLogId(LOCAL_HOST_ID, 1);
private static final CoordinatorLogId REPLICA_LOG_ID = new CoordinatorLogId(REMOTE_HOST_ID_1, 1);
private static final Participants PARTICIPANTS =
new Participants(List.of(LOCAL_HOST_ID, REMOTE_HOST_ID_1, REMOTE_HOST_ID_2));
private static final String KEYSPACE = "cltks"; private static final String KEYSPACE = "cltks";
private static final String TABLE = "cltt"; private static final String TABLE = "cltt";
@ -57,6 +67,7 @@ public class CoordinatorLogTest
.addClusteringColumn("ck", UTF8Type.instance) .addClusteringColumn("ck", UTF8Type.instance)
.addRegularColumn("value", UTF8Type.instance) .addRegularColumn("value", UTF8Type.instance)
.build()); .build());
MutationJournal.instance.start();
} }
private static Token tk(String key) private static Token tk(String key)
@ -66,7 +77,7 @@ public class CoordinatorLogTest
private static Offsets toOffsets(MutationId... ids) private static Offsets toOffsets(MutationId... ids)
{ {
Offsets.Mutable list = new Offsets.Mutable(LOG_ID); Offsets.Mutable list = new Offsets.Mutable(LOCAL_LOG_ID);
for (MutationId id : ids) for (MutationId id : ids)
list.add(id.offset()); list.add(id.offset());
return list; return list;
@ -74,15 +85,15 @@ public class CoordinatorLogTest
private static void assertUnreconciled(Token token, TableId tableId, CoordinatorLog log, boolean includePending, Offsets expectedReconciled, MutationId... expectedIds) private static void assertUnreconciled(Token token, TableId tableId, CoordinatorLog log, boolean includePending, Offsets expectedReconciled, MutationId... expectedIds)
{ {
Offsets.Mutable reconciled = new Offsets.Mutable(LOG_ID); Offsets.Mutable reconciled = new Offsets.Mutable(LOCAL_LOG_ID);
Offsets.Mutable unreconciled = new Offsets.Mutable(LOG_ID); Offsets.Mutable unreconciled = new Offsets.Mutable(LOCAL_LOG_ID);
log.collectOffsetsFor(token, tableId, includePending, unreconciled, reconciled); log.collectOffsetsFor(token, tableId, includePending, unreconciled, reconciled);
for (MutationId mid : expectedIds) for (MutationId mid : expectedIds)
Assert.assertTrue(unreconciled.contains(mid.offset())); assertTrue(unreconciled.contains(mid.offset()));
Assert.assertEquals(toOffsets(expectedIds), unreconciled); assertEquals(toOffsets(expectedIds), unreconciled);
Assert.assertEquals(expectedReconciled, reconciled); assertEquals(expectedReconciled, reconciled);
} }
@Test @Test
@ -91,24 +102,18 @@ public class CoordinatorLogTest
Token tk = tk("key"); Token tk = tk("key");
TableMetadata metadata = Schema.instance.getTableMetadata(KEYSPACE, TABLE); TableMetadata metadata = Schema.instance.getTableMetadata(KEYSPACE, TABLE);
TableId tableId = metadata.id; TableId tableId = metadata.id;
CoordinatorLogPrimary log = new CoordinatorLogPrimary(LOCAL_HOST_ID, LOG_ID, PARTICIPANTS); CoordinatorLogPrimary log = new CoordinatorLogPrimary(KEYSPACE, new Range<>(tk, tk), LOCAL_HOST_ID, LOCAL_LOG_ID, PARTICIPANTS);
MutationId[] ids = new MutationId[] { log.nextId(), log.nextId(), log.nextId(), }; MutationId[] ids = new MutationId[] { log.nextId(), log.nextId(), log.nextId(), };
List<Mutation> mutations = new ArrayList<>(ids.length); List<Mutation> mutations = new ArrayList<>(ids.length);
for (MutationId id : ids) for (MutationId id : ids)
{ {
Mutation mutation = Mutation mutation = createMutation(id);
new RowUpdateBuilder(metadata, 0, "key")
.clustering("ck")
.add("value", "value")
.build()
.withMutationId(id);
mutations.add(mutation); mutations.add(mutation);
log.startWriting(mutation); log.startWriting(mutation);
} }
Offsets.Mutable reconciled = new Offsets.Mutable(LOG_ID); Offsets.Mutable reconciled = new Offsets.Mutable(LOCAL_LOG_ID);
// we've only started writing, so the ids shouldn't appear without includePending being true // we've only started writing, so the ids shouldn't appear without includePending being true
assertUnreconciled(tk, tableId, log, false, reconciled); assertUnreconciled(tk, tableId, log, false, reconciled);
assertUnreconciled(tk, tableId, log, true, reconciled, ids); assertUnreconciled(tk, tableId, log, true, reconciled, ids);
@ -126,4 +131,86 @@ public class CoordinatorLogTest
reconciled.add(ids[0].offset()); reconciled.add(ids[0].offset());
assertUnreconciled(tk, tableId, log, false, reconciled, ids[1], ids[2]); assertUnreconciled(tk, tableId, log, false, reconciled, ids[1], ids[2]);
} }
private Mutation createMutation(MutationId id)
{
TableMetadata metadata = Schema.instance.getTableMetadata(KEYSPACE, TABLE);
return new RowUpdateBuilder(metadata, 0, "key")
.clustering("ck")
.add("value", "value")
.build()
.withMutationId(id);
}
@Test
public void persistAndLoadPrimaryLogTest()
{
testPersistAndLoadRoundtrip(LOCAL_LOG_ID);
}
@Test
public void persistAndLoadReplicaLogTest()
{
testPersistAndLoadRoundtrip(REPLICA_LOG_ID);
}
private void testPersistAndLoadRoundtrip(CoordinatorLogId logId)
{
Range<Token> range = new Range<>(tk("a"), tk("b"));
Offsets.Mutable offsets1 = new Offsets.Mutable(logId);
offsets1.add(1, 2, 3, 4);
Offsets.Mutable offsets2 = new Offsets.Mutable(logId);
offsets2.add(2, 3, 4, 5);
Offsets.Mutable offsets3 = new Offsets.Mutable(logId);
offsets3.add(3, 4, 5, 6);
Node2OffsetsMap witnessed = new Node2OffsetsMap();
witnessed.set(LOCAL_HOST_ID, offsets1);
witnessed.set(REMOTE_HOST_ID_1, offsets2);
witnessed.set(REMOTE_HOST_ID_2, offsets3);
UnreconciledMutations unreconciled = new UnreconciledMutations();
Mutation mutation1 = createMutation(new MutationId(logId.asLong(), MutationId.sequenceId(1, 0)));
Mutation mutation2 = createMutation(new MutationId(logId.asLong(), MutationId.sequenceId(2, 0)));
unreconciled.addDirectly(mutation1);
unreconciled.addDirectly(mutation2);
MutationJournal.instance.write(mutation1.id(), mutation1);
MutationJournal.instance.write(mutation2.id(), mutation2);
CoordinatorLog log =
CoordinatorLog.recreate(KEYSPACE, range, LOCAL_HOST_ID, logId, PARTICIPANTS, witnessed, witnessed, unreconciled);
Offsets.Mutable reconciled = new Offsets.Mutable(logId);
reconciled.add(3, 4);
assertEquals(reconciled, log.reconciledOffsets);
validatePersistAndLoadRoundtrip(log);
log.deleteFromSystemTable();
}
private static void validatePersistAndLoadRoundtrip(CoordinatorLog log)
{
log.persistToSystemTable();
List<CoordinatorLog> logs = CoordinatorLog.loadFromSystemTable(KEYSPACE, log.range, LOCAL_HOST_ID);
assertEquals(1, logs.size());
CoordinatorLog loaded = logs.get(0);
assertSame(log.getClass(), loaded.getClass());
assertEquals(log.keyspace, loaded.keyspace);
assertEquals(log.range, loaded.range);
assertEquals(log.logId, loaded.logId);
assertEquals(log.participants, loaded.participants);
assertEquals(log.localNodeId, loaded.localNodeId);
assertEquals(log.participants.size(), log.witnessedOffsets.size());
assertEquals(log.participants.size(), log.persistedOffsets.size());
assertEquals(loaded.participants.size(), loaded.witnessedOffsets.size());
assertEquals(loaded.participants.size(), loaded.persistedOffsets.size());
assertEquals(log.witnessedOffsets, loaded.witnessedOffsets);
assertEquals(log.persistedOffsets, loaded.persistedOffsets);
assertEquals(log.reconciledOffsets, loaded.reconciledOffsets);
assertTrue(log.unreconciledMutations.equalsForTesting(loaded.unreconciledMutations));
}
} }

View File

@ -843,21 +843,27 @@ public class OffsetsTest
public void testRemoveWithExactSizedArray() public void testRemoveWithExactSizedArray()
{ {
{ {
Offsets.Mutable offsets = new Offsets.Mutable(LOG_ID, new int[] {10, 11}, 2); Offsets.Mutable offsets = new Offsets.Mutable(LOG_ID, new int[]{ 10, 11 }, 2);
offsets.remove(10); offsets.remove(10);
assertOffsetsEqual(offsets(11, 11), offsets); assertOffsetsEqual(offsets(11, 11), offsets);
} }
{ {
Offsets.Mutable offsets = new Offsets.Mutable(LOG_ID, new int[] {10, 11}, 2); Offsets.Mutable offsets = new Offsets.Mutable(LOG_ID, new int[]{ 10, 11 }, 2);
offsets.remove(11); offsets.remove(11);
assertOffsetsEqual(offsets(10, 10), offsets); assertOffsetsEqual(offsets(10, 10), offsets);
} }
{ {
Offsets.Mutable offsets = new Offsets.Mutable(LOG_ID, new int[] {11, 11}, 2); Offsets.Mutable offsets = new Offsets.Mutable(LOG_ID, new int[]{ 11, 11 }, 2);
offsets.remove(11); offsets.remove(11);
assertOffsetsEqual(offsets(), offsets); assertOffsetsEqual(offsets(), offsets);
} }
} }
public void asListFromListRoundTripTest()
{
for (Offsets.Mutable offsets : new Offsets.Mutable[] { offsets(), offsets(1, 2), offsets(1, 3, 7, 9) })
assertOffsetsEqual(offsets, Offsets.fromList(LOG_ID, offsets.asList()));
}
} }

View File

@ -0,0 +1,112 @@
/*
* 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 java.util.function.LongSupplier;
import org.junit.BeforeClass;
import org.junit.Test;
import org.agrona.collections.MutableInteger;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.ByteOrderedPartitioner;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.junit.Assert.assertEquals;
public class ShardTest
{
private static final int LOCAL_HOST_ID = 1;
private static final int REMOTE_HOST_ID_1 = 2;
private static final int REMOTE_HOST_ID_2 = 3;
private static final String KEYSPACE = "shard_test_ks";
private static final String TABLE = "shard_test_table";
@BeforeClass
public static void setUp() throws IOException
{
SchemaLoader.prepareServer();
SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(3),
TableMetadata.builder(KEYSPACE, TABLE)
.addPartitionKeyColumn("pk", UTF8Type.instance)
.addClusteringColumn("ck", UTF8Type.instance)
.addRegularColumn("value", UTF8Type.instance)
.build());
MutationJournal.instance.start();
}
private static Token tk(String key)
{
return new ByteOrderedPartitioner.BytesToken(ByteBufferUtil.bytes(key));
}
@Test
public void testPersistAndLoadSingleShard()
{
Range<Token> range = new Range<>(tk("a"), tk("z"));
Participants participants = new Participants(List.of(LOCAL_HOST_ID, REMOTE_HOST_ID_1, REMOTE_HOST_ID_2));
MutableInteger logId = new MutableInteger();
LongSupplier logIdProvider = () -> CoordinatorLogId.asLong(LOCAL_HOST_ID, logId.getAndIncrement());
Shard original = new Shard(LOCAL_HOST_ID, KEYSPACE, range, participants, logIdProvider, (s, l) -> {});
original.persistToSystemTables();
ArrayList<Shard> loadedShards = Shard.loadFromSystemTables(LOCAL_HOST_ID, logIdProvider, (s, l) -> {});
assertEquals(1, loadedShards.size());
Shard loaded = loadedShards.get(0);
assertEquals(original.localNodeId, loaded.localNodeId);
assertEquals(original.keyspace, loaded.keyspace);
assertEquals(original.range, loaded.range);
assertEquals(original.participants, loaded.participants);
// TODO: compare the coordinator logs
}
@Test
public void testLogRotation()
{
CoordinatorLog.overrideMaxOffsetForTesting(100);
try
{
Range<Token> range = new Range<>(tk("a"), tk("z"));
Participants participants = new Participants(List.of(LOCAL_HOST_ID, REMOTE_HOST_ID_1, REMOTE_HOST_ID_2));
MutableInteger logId = new MutableInteger();
LongSupplier logIdProvider = () -> CoordinatorLogId.asLong(LOCAL_HOST_ID, logId.getAndIncrement());
Shard shard = new Shard(LOCAL_HOST_ID, KEYSPACE, range, participants, logIdProvider, (s, l) -> {
});
MutationId firstId = shard.nextId();
for (int i = 0; i < 100; i++)
assertEquals(firstId.hostLogId, shard.nextId().hostLogId);
assertEquals(firstId.hostLogId + 1, shard.nextId().hostLogId);
}
finally
{
CoordinatorLog.overrideMaxOffsetForTesting(Integer.MAX_VALUE);
}
}
}

View File

@ -43,10 +43,13 @@ import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.ByteBufferUtil;
import static org.junit.Assert.assertEquals;
public class UnreconciledMutationsTest public class UnreconciledMutationsTest
{ {
private static final String KEYSPACE = "ks"; private static final String KEYSPACE = "ks";
private static final String TABLE = "tbl"; private static final String TABLE = "tbl";
private static TableId TABLE_ID;
@BeforeClass @BeforeClass
public static void setUp() throws IOException public static void setUp() throws IOException
@ -57,6 +60,7 @@ public class UnreconciledMutationsTest
.addPartitionKeyColumn("k", Int32Type.instance) .addPartitionKeyColumn("k", Int32Type.instance)
.addRegularColumn("v", Int32Type.instance) .addRegularColumn("v", Int32Type.instance)
.build()); .build());
TABLE_ID = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
} }
private static Token tokenFor(int key) private static Token tokenFor(int key)
@ -69,9 +73,7 @@ public class UnreconciledMutationsTest
TableMetadata metadata = Schema.instance.getTableMetadata(KEYSPACE, TABLE); TableMetadata metadata = Schema.instance.getTableMetadata(KEYSPACE, TABLE);
// Create a MutationId with logId 1L and sequenceId that produces the desired offset // Create a MutationId with logId 1L and sequenceId that produces the desired offset
long logId = 1L; MutationId mutationId = new MutationId(CoordinatorLogId.asLong(1, 1), MutationId.sequenceId(offset, 0));
long sequenceId = ((long) offset << 32) | 1000; // offset in high bits, timestamp in low bits
MutationId mutationId = new MutationId(logId, sequenceId);
Mutation mutation = new RowUpdateBuilder(metadata, 0, partitionKey) Mutation mutation = new RowUpdateBuilder(metadata, 0, partitionKey)
.add("v", value) .add("v", value)
@ -90,8 +92,7 @@ public class UnreconciledMutationsTest
public void testSingleTokenCollectionIsolation() public void testSingleTokenCollectionIsolation()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
// Create mutations for different partition keys with different tokens // Create mutations for different partition keys with different tokens
Mutation mutation1 = createMutation(100, 1000, 1); Mutation mutation1 = createMutation(100, 1000, 1);
Mutation mutation2 = createMutation(200, 2000, 2); Mutation mutation2 = createMutation(200, 2000, 2);
@ -113,30 +114,30 @@ public class UnreconciledMutationsTest
// Test single token collection for token1 - should ONLY return mutation1 // Test single token collection for token1 - should ONLY return mutation1
Offsets.Mutable offsets1 = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable offsets1 = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean found1 = unreconciled.collect(token1, tableId, false, offsets1); boolean found1 = unreconciled.collect(token1, TABLE_ID, false, offsets1);
Assert.assertTrue("Should find mutations for token1", found1); Assert.assertTrue("Should find mutations for token1", found1);
Assert.assertEquals("Should only contain 1 offset for token1", 1, offsets1.offsetCount()); assertEquals("Should only contain 1 offset for token1", 1, offsets1.offsetCount());
Assert.assertTrue("Should contain offset 1 for mutation1", offsets1.contains(1)); Assert.assertTrue("Should contain offset 1 for mutation1", offsets1.contains(1));
Assert.assertFalse("Should NOT contain offset 2 for mutation2", offsets1.contains(2)); Assert.assertFalse("Should NOT contain offset 2 for mutation2", offsets1.contains(2));
Assert.assertFalse("Should NOT contain offset 3 for mutation3", offsets1.contains(3)); Assert.assertFalse("Should NOT contain offset 3 for mutation3", offsets1.contains(3));
// Test single token collection for token2 - should ONLY return mutation2 // Test single token collection for token2 - should ONLY return mutation2
Offsets.Mutable offsets2 = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable offsets2 = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean found2 = unreconciled.collect(token2, tableId, false, offsets2); boolean found2 = unreconciled.collect(token2, TABLE_ID, false, offsets2);
Assert.assertTrue("Should find mutations for token2", found2); Assert.assertTrue("Should find mutations for token2", found2);
Assert.assertEquals("Should only contain 1 offset for token2", 1, offsets2.offsetCount()); assertEquals("Should only contain 1 offset for token2", 1, offsets2.offsetCount());
Assert.assertTrue("Should contain offset 2 for mutation2", offsets2.contains(2)); Assert.assertTrue("Should contain offset 2 for mutation2", offsets2.contains(2));
Assert.assertFalse("Should NOT contain offset 1 for mutation1", offsets2.contains(1)); Assert.assertFalse("Should NOT contain offset 1 for mutation1", offsets2.contains(1));
Assert.assertFalse("Should NOT contain offset 3 for mutation3", offsets2.contains(3)); Assert.assertFalse("Should NOT contain offset 3 for mutation3", offsets2.contains(3));
// Test single token collection for token3 - should ONLY return mutation3 // Test single token collection for token3 - should ONLY return mutation3
Offsets.Mutable offsets3 = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable offsets3 = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean found3 = unreconciled.collect(token3, tableId, false, offsets3); boolean found3 = unreconciled.collect(token3, TABLE_ID, false, offsets3);
Assert.assertTrue("Should find mutations for token3", found3); Assert.assertTrue("Should find mutations for token3", found3);
Assert.assertEquals("Should only contain 1 offset for token3", 1, offsets3.offsetCount()); assertEquals("Should only contain 1 offset for token3", 1, offsets3.offsetCount());
Assert.assertTrue("Should contain offset 3 for mutation3", offsets3.contains(3)); Assert.assertTrue("Should contain offset 3 for mutation3", offsets3.contains(3));
Assert.assertFalse("Should NOT contain offset 1 for mutation1", offsets3.contains(1)); Assert.assertFalse("Should NOT contain offset 1 for mutation1", offsets3.contains(1));
Assert.assertFalse("Should NOT contain offset 2 for mutation2", offsets3.contains(2)); Assert.assertFalse("Should NOT contain offset 2 for mutation2", offsets3.contains(2));
@ -146,11 +147,10 @@ public class UnreconciledMutationsTest
public void testEmptyCollection() public void testEmptyCollection()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
Token token = tokenFor(100); Token token = tokenFor(100);
Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean found = unreconciled.collect(token, tableId, false, offsets); boolean found = unreconciled.collect(token, TABLE_ID, false, offsets);
Assert.assertFalse("Should not find any mutations in empty collection", found); Assert.assertFalse("Should not find any mutations in empty collection", found);
Assert.assertTrue("Should have no offsets", offsets.isEmpty()); Assert.assertTrue("Should have no offsets", offsets.isEmpty());
@ -160,8 +160,7 @@ public class UnreconciledMutationsTest
public void testPendingVsVisibleMutations() public void testPendingVsVisibleMutations()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
Mutation pendingMutation = createMutation(100, 1000, 1); Mutation pendingMutation = createMutation(100, 1000, 1);
Mutation visibleMutation = createMutation(100, 2000, 2); Mutation visibleMutation = createMutation(100, 2000, 2);
Token token = pendingMutation.key().getToken(); Token token = pendingMutation.key().getToken();
@ -173,19 +172,19 @@ public class UnreconciledMutationsTest
// Test without including pending - should only get visible mutation // Test without including pending - should only get visible mutation
Offsets.Mutable visibleOnly = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable visibleOnly = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean foundVisible = unreconciled.collect(token, tableId, false, visibleOnly); boolean foundVisible = unreconciled.collect(token, TABLE_ID, false, visibleOnly);
Assert.assertTrue("Should find visible mutations", foundVisible); Assert.assertTrue("Should find visible mutations", foundVisible);
Assert.assertEquals("Should only have 1 visible mutation", 1, visibleOnly.offsetCount()); assertEquals("Should only have 1 visible mutation", 1, visibleOnly.offsetCount());
Assert.assertTrue("Should contain visible mutation offset", visibleOnly.contains(2)); Assert.assertTrue("Should contain visible mutation offset", visibleOnly.contains(2));
Assert.assertFalse("Should NOT contain pending mutation offset", visibleOnly.contains(1)); Assert.assertFalse("Should NOT contain pending mutation offset", visibleOnly.contains(1));
// Test including pending - should get both mutations // Test including pending - should get both mutations
Offsets.Mutable includingPending = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable includingPending = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean foundAll = unreconciled.collect(token, tableId, true, includingPending); boolean foundAll = unreconciled.collect(token, TABLE_ID, true, includingPending);
Assert.assertTrue("Should find all mutations", foundAll); Assert.assertTrue("Should find all mutations", foundAll);
Assert.assertEquals("Should have 2 mutations total", 2, includingPending.offsetCount()); assertEquals("Should have 2 mutations total", 2, includingPending.offsetCount());
Assert.assertTrue("Should contain pending mutation offset", includingPending.contains(1)); Assert.assertTrue("Should contain pending mutation offset", includingPending.contains(1));
Assert.assertTrue("Should contain visible mutation offset", includingPending.contains(2)); Assert.assertTrue("Should contain visible mutation offset", includingPending.contains(2));
} }
@ -194,8 +193,7 @@ public class UnreconciledMutationsTest
public void testMultipleMutationsSameToken() public void testMultipleMutationsSameToken()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
// Create multiple mutations for the same partition key (same token) // Create multiple mutations for the same partition key (same token)
Mutation mutation1 = createMutation(100, 1000, 1); Mutation mutation1 = createMutation(100, 1000, 1);
Mutation mutation2 = createMutation(100, 2000, 2); Mutation mutation2 = createMutation(100, 2000, 2);
@ -211,10 +209,10 @@ public class UnreconciledMutationsTest
unreconciled.finishWriting(mutation3); unreconciled.finishWriting(mutation3);
Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean found = unreconciled.collect(token, tableId, false, offsets); boolean found = unreconciled.collect(token, TABLE_ID, false, offsets);
Assert.assertTrue("Should find mutations for token", found); Assert.assertTrue("Should find mutations for token", found);
Assert.assertEquals("Should have all 3 mutations for same token", 3, offsets.offsetCount()); assertEquals("Should have all 3 mutations for same token", 3, offsets.offsetCount());
Assert.assertTrue("Should contain offset 1", offsets.contains(1)); Assert.assertTrue("Should contain offset 1", offsets.contains(1));
Assert.assertTrue("Should contain offset 2", offsets.contains(2)); Assert.assertTrue("Should contain offset 2", offsets.contains(2));
Assert.assertTrue("Should contain offset 3", offsets.contains(3)); Assert.assertTrue("Should contain offset 3", offsets.contains(3));
@ -224,8 +222,7 @@ public class UnreconciledMutationsTest
public void testTableIdFiltering() public void testTableIdFiltering()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
// Create a fake different table ID // Create a fake different table ID
TableId differentTableId = TableId.generate(); TableId differentTableId = TableId.generate();
@ -237,10 +234,10 @@ public class UnreconciledMutationsTest
// Query with correct table ID - should find mutation // Query with correct table ID - should find mutation
Offsets.Mutable correctTable = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable correctTable = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean foundCorrect = unreconciled.collect(token, tableId, false, correctTable); boolean foundCorrect = unreconciled.collect(token, TABLE_ID, false, correctTable);
Assert.assertTrue("Should find mutation for correct table", foundCorrect); Assert.assertTrue("Should find mutation for correct table", foundCorrect);
Assert.assertEquals("Should have 1 mutation", 1, correctTable.offsetCount()); assertEquals("Should have 1 mutation", 1, correctTable.offsetCount());
// Query with different table ID - should find nothing // Query with different table ID - should find nothing
Offsets.Mutable differentTable = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable differentTable = new Offsets.Mutable(new CoordinatorLogId(1, 1));
@ -254,8 +251,7 @@ public class UnreconciledMutationsTest
public void testMutationRemoval() public void testMutationRemoval()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
Mutation mutation = createMutation(100, 1000, 1); Mutation mutation = createMutation(100, 1000, 1);
Token token = mutation.key().getToken(); Token token = mutation.key().getToken();
@ -264,16 +260,16 @@ public class UnreconciledMutationsTest
// Verify mutation is present // Verify mutation is present
Offsets.Mutable beforeRemoval = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable beforeRemoval = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean foundBefore = unreconciled.collect(token, tableId, false, beforeRemoval); boolean foundBefore = unreconciled.collect(token, TABLE_ID, false, beforeRemoval);
Assert.assertTrue("Should find mutation before removal", foundBefore); Assert.assertTrue("Should find mutation before removal", foundBefore);
Assert.assertEquals("Should have 1 mutation before removal", 1, beforeRemoval.offsetCount()); assertEquals("Should have 1 mutation before removal", 1, beforeRemoval.offsetCount());
// Remove the mutation // Remove the mutation
unreconciled.remove(1); unreconciled.remove(1);
// Verify mutation is gone // Verify mutation is gone
Offsets.Mutable afterRemoval = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable afterRemoval = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean foundAfter = unreconciled.collect(token, tableId, false, afterRemoval); boolean foundAfter = unreconciled.collect(token, TABLE_ID, false, afterRemoval);
Assert.assertFalse("Should not find mutation after removal", foundAfter); Assert.assertFalse("Should not find mutation after removal", foundAfter);
Assert.assertTrue("Should have no mutations after removal", afterRemoval.isEmpty()); Assert.assertTrue("Should have no mutations after removal", afterRemoval.isEmpty());
} }
@ -282,8 +278,7 @@ public class UnreconciledMutationsTest
public void testTokenRangeCollection() public void testTokenRangeCollection()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
// Create mutations with different tokens and sort them // Create mutations with different tokens and sort them
List<Integer> keys = List.of(100, 200, 300, 400, 500); List<Integer> keys = List.of(100, 200, 300, 400, 500);
List<Mutation> mutations = new ArrayList<>(); List<Mutation> mutations = new ArrayList<>();
@ -311,7 +306,7 @@ public class UnreconciledMutationsTest
new Bounds<>(startKey, endKey); new Bounds<>(startKey, endKey);
Offsets.Mutable rangeOffsets = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable rangeOffsets = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean foundRange = unreconciled.collect(bounds, tableId, false, rangeOffsets); boolean foundRange = unreconciled.collect(bounds, TABLE_ID, false, rangeOffsets);
Assert.assertTrue("Should find mutations in range", foundRange); Assert.assertTrue("Should find mutations in range", foundRange);
// Should include mutations at positions 0, 1, and 2 (first through middle inclusive) // Should include mutations at positions 0, 1, and 2 (first through middle inclusive)
@ -322,8 +317,7 @@ public class UnreconciledMutationsTest
public void testSingleTokenRangeCollectionBug() public void testSingleTokenRangeCollectionBug()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
// Create mutations for different partition keys with different tokens // Create mutations for different partition keys with different tokens
Mutation mutation1 = createMutation(100, 1000, 1); Mutation mutation1 = createMutation(100, 1000, 1);
Mutation mutation2 = createMutation(200, 2000, 2); Mutation mutation2 = createMutation(200, 2000, 2);
@ -342,11 +336,11 @@ public class UnreconciledMutationsTest
// Test single token range collection // Test single token range collection
Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1));
Bounds<PartitionPosition> bounds = new Bounds<>(mutation1.key(), mutation1.key()); Bounds<PartitionPosition> bounds = new Bounds<>(mutation1.key(), mutation1.key());
boolean found = unreconciled.collect(bounds, tableId, false, offsets); boolean found = unreconciled.collect(bounds, TABLE_ID, false, offsets);
// This should only return mutation1 // This should only return mutation1
Assert.assertTrue("Should find mutations for single token range", found); Assert.assertTrue("Should find mutations for single token range", found);
Assert.assertEquals("Single token range should only return 1 mutation", 1, offsets.offsetCount()); assertEquals("Single token range should only return 1 mutation", 1, offsets.offsetCount());
Assert.assertTrue("Should contain mutation1 offset", offsets.contains(1)); Assert.assertTrue("Should contain mutation1 offset", offsets.contains(1));
// other mutations should not be included // other mutations should not be included
@ -358,8 +352,7 @@ public class UnreconciledMutationsTest
public void testFullRangeCollectionWithMinimumToken() public void testFullRangeCollectionWithMinimumToken()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
// Create mutations for different partition keys with different tokens // Create mutations for different partition keys with different tokens
Mutation mutation1 = createMutation(100, 1000, 1); Mutation mutation1 = createMutation(100, 1000, 1);
Mutation mutation2 = createMutation(200, 2000, 2); Mutation mutation2 = createMutation(200, 2000, 2);
@ -382,11 +375,11 @@ public class UnreconciledMutationsTest
// Test full range collection - this SHOULD return ALL mutations // Test full range collection - this SHOULD return ALL mutations
Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable offsets = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean found = unreconciled.collect(fullRange, tableId, false, offsets); boolean found = unreconciled.collect(fullRange, TABLE_ID, false, offsets);
// Full range should return ALL mutations // Full range should return ALL mutations
Assert.assertTrue("Should find mutations for full range", found); Assert.assertTrue("Should find mutations for full range", found);
Assert.assertEquals("Full range should return ALL mutations", 3, offsets.offsetCount()); assertEquals("Full range should return ALL mutations", 3, offsets.offsetCount());
Assert.assertTrue("Should contain mutation1 offset", offsets.contains(1)); Assert.assertTrue("Should contain mutation1 offset", offsets.contains(1));
Assert.assertTrue("Should contain mutation2 offset", offsets.contains(2)); Assert.assertTrue("Should contain mutation2 offset", offsets.contains(2));
Assert.assertTrue("Should contain mutation3 offset", offsets.contains(3)); Assert.assertTrue("Should contain mutation3 offset", offsets.contains(3));
@ -396,8 +389,7 @@ public class UnreconciledMutationsTest
public void testSingleTokenCollectionVsFullRange() public void testSingleTokenCollectionVsFullRange()
{ {
UnreconciledMutations unreconciled = new UnreconciledMutations(); UnreconciledMutations unreconciled = new UnreconciledMutations();
TableId tableId = Schema.instance.getTableMetadata(KEYSPACE, TABLE).id;
// Create mutations with different tokens // Create mutations with different tokens
Mutation mutation1 = createMutation(100, 1000, 1); Mutation mutation1 = createMutation(100, 1000, 1);
Mutation mutation2 = createMutation(200, 2000, 2); Mutation mutation2 = createMutation(200, 2000, 2);
@ -415,10 +407,10 @@ public class UnreconciledMutationsTest
// Single token collection should only return mutation1 // Single token collection should only return mutation1
Offsets.Mutable singleToken = new Offsets.Mutable(new CoordinatorLogId(1, 1)); Offsets.Mutable singleToken = new Offsets.Mutable(new CoordinatorLogId(1, 1));
boolean foundSingle = unreconciled.collect(targetToken, tableId, false, singleToken); boolean foundSingle = unreconciled.collect(targetToken, TABLE_ID, false, singleToken);
Assert.assertTrue("Should find mutation for single token", foundSingle); Assert.assertTrue("Should find mutation for single token", foundSingle);
Assert.assertEquals("Single token should only return 1 mutation", 1, singleToken.offsetCount()); assertEquals("Single token should only return 1 mutation", 1, singleToken.offsetCount());
Assert.assertTrue("Should contain mutation1 offset", singleToken.contains(1)); Assert.assertTrue("Should contain mutation1 offset", singleToken.contains(1));
// This is the CRITICAL test - single token collection must NOT return all mutations // This is the CRITICAL test - single token collection must NOT return all mutations
@ -426,4 +418,33 @@ public class UnreconciledMutationsTest
Assert.assertFalse("Single token collection should NOT contain mutation2", singleToken.contains(2)); Assert.assertFalse("Single token collection should NOT contain mutation2", singleToken.contains(2));
Assert.assertFalse("Single token collection should NOT contain mutation3", singleToken.contains(3)); Assert.assertFalse("Single token collection should NOT contain mutation3", singleToken.contains(3));
} }
@Test
public void testLoadFromJournal()
{
MutationJournal.instance.start();
CoordinatorLogId logId = new CoordinatorLogId(1, 1);
Offsets.Mutable offsets1 = new Offsets.Mutable(logId);
offsets1.add(1, 2, 3, 4, 5, 6, 7);
Offsets.Mutable offsets2 = new Offsets.Mutable(logId);
offsets2.add(1, 2, 3, 4, 5);
Node2OffsetsMap witnessed = new Node2OffsetsMap();
witnessed.set(1, offsets1);
witnessed.set(2, offsets2);
Mutation mutation6 = createMutation(6, 6, 6);
Mutation mutation7 = createMutation(7, 7, 7);
MutationJournal.instance.write(mutation6.id(), mutation6);
MutationJournal.instance.write(mutation7.id(), mutation7);
Offsets.Mutable loadedOffsets = new Offsets.Mutable(logId);
UnreconciledMutations unreconciled = UnreconciledMutations.loadFromJournal(witnessed, 1);
unreconciled.collect(mutation6.key().getToken(), TABLE_ID, false, loadedOffsets);
unreconciled.collect(mutation7.key().getToken(), TABLE_ID, false, loadedOffsets);
assertEquals(List.of(6, 7), loadedOffsets.asList());
}
} }