This is an automated email from the ASF dual-hosted git repository.
bdeggleston pushed a commit to branch cep-45-mutation-tracking
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/cep-45-mutation-tracking by
this push:
new d8ee241e87 CEP-45: Handle tombstone purging correctly in preview
repair, fix validation timeouts, and add trace logging
d8ee241e87 is described below
commit d8ee241e87f8678fb602829a75f3a92b22777320
Author: Blake Eggleston <[email protected]>
AuthorDate: Tue Nov 3 13:23:18 2026 -0800
CEP-45: Handle tombstone purging correctly in preview repair, fix
validation timeouts, and add trace logging
Patch by Blake Eggleston; reviewed by Caleb Rackliffe for CASSANDRA-21850
---
src/java/org/apache/cassandra/config/Config.java | 2 -
.../cassandra/config/DatabaseDescriptor.java | 10 -
.../db/repair/CassandraTableRepairManager.java | 4 +-
.../db/repair/CassandraValidationIterator.java | 38 +++-
.../repair/JournalMergingValidationIterator.java | 52 ++++-
.../repair/MutationTrackingPreviewRepairTask.java | 35 +++-
.../MutationTrackingValidationCoordinator.java | 11 +-
.../cassandra/replication/ActivatedTransfers.java | 34 ++-
.../ImmutableCoordinatorLogOffsets.java | 30 ++-
.../org/apache/cassandra/replication/Offsets.java | 24 +++
.../cassandra/replication/ValidationOffsets.java | 28 ++-
.../apache/cassandra/service/StorageService.java | 12 --
.../cassandra/service/StorageServiceMBean.java | 3 -
.../test/tracking/TrackedPreviewRepairTest.java | 17 +-
.../JournalMergingValidationIteratorTest.java | 228 +++++++++++++++++++++
15 files changed, 451 insertions(+), 77 deletions(-)
diff --git a/src/java/org/apache/cassandra/config/Config.java
b/src/java/org/apache/cassandra/config/Config.java
index 527f75c79a..614691056b 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -175,8 +175,6 @@ public class Config
public volatile DurationSpec.LongMillisecondsBound
mutation_tracking_sync_timeout = new DurationSpec.LongMillisecondsBound("2m");
- public volatile DurationSpec.LongMillisecondsBound
mutation_tracking_validation_timeout = new
DurationSpec.LongMillisecondsBound("2m");
-
public Integer streaming_connections_per_host = 1;
@Replaces(oldName = "streaming_keep_alive_period_in_secs", converter =
Converters.SECONDS_DURATION, deprecated = true)
public DurationSpec.IntSecondsBound streaming_keep_alive_period = new
DurationSpec.IntSecondsBound("300s");
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index f88406a388..86ca89b02c 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -2695,16 +2695,6 @@ public class DatabaseDescriptor
conf.mutation_tracking_sync_timeout = new
DurationSpec.LongMillisecondsBound(timeoutInMillis);
}
- public static long getMutationTrackingValidationTimeout(TimeUnit unit)
- {
- return conf.mutation_tracking_validation_timeout.to(unit);
- }
-
- public static void setMutationTrackingValidationTimeout(long
timeoutInMillis)
- {
- conf.mutation_tracking_validation_timeout = new
DurationSpec.LongMillisecondsBound(timeoutInMillis);
- }
-
public static boolean hasCrossNodeTimeout()
{
return conf.internode_timeout;
diff --git
a/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java
b/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java
index e4232071f8..21c2dee9af 100644
--- a/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java
+++ b/src/java/org/apache/cassandra/db/repair/CassandraTableRepairManager.java
@@ -62,14 +62,14 @@ public class CassandraTableRepairManager implements
TableRepairManager
public ValidationPartitionIterator
getValidationIterator(Collection<Range<Token>> ranges, TimeUUID parentId,
TimeUUID sessionID, boolean isIncremental, long nowInSec, boolean
dontPurgeTombstones, ValidationOffsets validationOffsets,
TopPartitionTracker.Collector topPartitionCollector) throws IOException,
NoSuchRepairSessionException
{
CassandraValidationIterator sstableIterator =
- new CassandraValidationIterator(cfs, ctx, ranges, parentId,
sessionID, isIncremental, nowInSec, dontPurgeTombstones, validationOffsets,
topPartitionCollector);
+ new CassandraValidationIterator(cfs, ctx, ranges, parentId,
sessionID, isIncremental, nowInSec, validationOffsets != null ||
dontPurgeTombstones, validationOffsets, topPartitionCollector);
if (validationOffsets == null)
return sstableIterator;
try
{
- return new JournalMergingValidationIterator(cfs, sstableIterator,
ranges, validationOffsets);
+ return new JournalMergingValidationIterator(cfs, sstableIterator,
ranges, validationOffsets, nowInSec, dontPurgeTombstones);
}
catch (Throwable t)
{
diff --git
a/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java
b/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java
index e09269c70f..9489254828 100644
--- a/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java
+++ b/src/java/org/apache/cassandra/db/repair/CassandraValidationIterator.java
@@ -61,6 +61,7 @@ import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.service.snapshot.SnapshotManager;
import org.apache.cassandra.service.snapshot.TableSnapshot;
+import org.apache.cassandra.streaming.PreviewKind;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.concurrent.Refs;
@@ -160,10 +161,15 @@ public class CassandraValidationIterator extends
ValidationPartitionIterator
{
for (SSTableReader sstable : sstableCandidates.sstables)
{
- if (new Bounds<>(sstable.getFirst().getToken(),
sstable.getLast().getToken()).intersects(ranges) && predicate.apply(sstable))
- {
+ boolean intersects = new
Bounds<>(sstable.getFirst().getToken(),
sstable.getLast().getToken()).intersects(ranges);
+ boolean selected = intersects && predicate.apply(sstable);
+ if (selected)
sstablesToValidate.add(sstable);
- }
+
+ if (validationOffsets != null && logger.isTraceEnabled())
+ logger.trace("{} Validation selection for {}: {} - {}",
+ prs.previewKind.logPrefix(parentId), sstable,
selected ? "selected" : "skipped",
+ selectionReason(sstable, intersects,
selected, validationOffsets));
}
sstables = Refs.tryRef(sstablesToValidate);
@@ -177,7 +183,31 @@ public class CassandraValidationIterator extends
ValidationPartitionIterator
return sstables;
}
+ private static String selectionReason(SSTableReader sstable, boolean
intersects, boolean selected, ValidationOffsets validationOffsets)
+ {
+ String offsets = sstable.getCoordinatorLogOffsets().toString(true);
+ if (!intersects)
+ return "out of range, offsets=" + offsets;
+ if (sstable.isRepaired())
+ return (selected ? "" : "not selected despite being repaired, ") +
"repaired, no offset check, offsets=" + offsets;
+ if (selected)
+ return "unrepaired, offsets covered, offsets=" + offsets;
+ return "unrepaired, offsets not covered by " +
validationOffsets.toString(true) + ", offsets=" + offsets;
+ }
+
+ TimeUUID parentId()
+ {
+ return parentId;
+ }
+
+ PreviewKind previewKind()
+ {
+ return previewKind;
+ }
+
private final ColumnFamilyStore cfs;
+ private final TimeUUID parentId;
+ private final PreviewKind previewKind;
private final Refs<SSTableReader> sstables;
private final String snapshotName;
private final boolean isGlobalSnapshotValidation;
@@ -195,6 +225,7 @@ public class CassandraValidationIterator extends
ValidationPartitionIterator
public CassandraValidationIterator(ColumnFamilyStore cfs, SharedContext
ctx, Collection<Range<Token>> ranges, TimeUUID parentId, TimeUUID sessionID,
boolean isIncremental, long nowInSec, boolean dontPurgeTombstones,
ValidationOffsets validationOffsets, TopPartitionTracker.Collector
topPartitionCollector) throws IOException, NoSuchRepairSessionException
{
this.cfs = cfs;
+ this.parentId = parentId;
isGlobalSnapshotValidation =
SnapshotManager.instance.exists(cfs.getKeyspaceName(), cfs.getTableName(),
parentId.toString());
if (isGlobalSnapshotValidation)
@@ -234,6 +265,7 @@ public class CassandraValidationIterator extends
ValidationPartitionIterator
Preconditions.checkArgument(sstables != null);
ActiveRepairService.ParentRepairSession prs =
ctx.repair().getParentRepairSession(parentId);
+ previewKind = prs.previewKind;
logger.info("{}, parentSessionId={}: Performing validation compaction
on {} sstables in {}.{}",
prs.previewKind.logPrefix(sessionID),
parentId,
diff --git
a/src/java/org/apache/cassandra/db/repair/JournalMergingValidationIterator.java
b/src/java/org/apache/cassandra/db/repair/JournalMergingValidationIterator.java
index a2d6c537b9..c9877fcda6 100644
---
a/src/java/org/apache/cassandra/db/repair/JournalMergingValidationIterator.java
+++
b/src/java/org/apache/cassandra/db/repair/JournalMergingValidationIterator.java
@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
+import java.util.function.LongPredicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,21 +39,28 @@ import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.PurgeFunction;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.db.rows.UnfilteredRowIterators;
import org.apache.cassandra.db.rows.WrappingUnfilteredRowIterator;
+import org.apache.cassandra.db.transform.Transformation;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.repair.ValidationPartitionIterator;
+import org.apache.cassandra.replication.ImmutableCoordinatorLogOffsets;
import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.replication.ShortMutationId;
import org.apache.cassandra.replication.ValidationOffsets;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.streaming.PreviewKind;
+import org.apache.cassandra.utils.TimeUUID;
+
+import static
org.apache.cassandra.db.repair.CassandraValidationIterator.getDefaultGcBefore;
/**
* Merges a {@link CassandraValidationIterator}'s SSTable-sourced partitions
with
@@ -73,14 +81,27 @@ public class JournalMergingValidationIterator extends
ValidationPartitionIterato
public JournalMergingValidationIterator(ColumnFamilyStore cfs,
CassandraValidationIterator
sstableIterator,
Collection<Range<Token>> ranges,
- ValidationOffsets
validationOffsets)
+ ValidationOffsets
validationOffsets,
+ long nowInSec,
+ boolean dontPurgeTombstones)
{
this.sstableIterator = sstableIterator;
// Narrow the offsets for the journal to exclude anything the selected
SSTables already cover.
// The journal isn't pruned on flush, so without this, we could merge
the same data from both sources.
- ValidationOffsets journalOffsets =
validationOffsets.without(sstableIterator.sstableCoordinatorLogOffsets());
+ ImmutableCoordinatorLogOffsets sstableOffsets =
sstableIterator.sstableCoordinatorLogOffsets();
+ ValidationOffsets journalOffsets =
validationOffsets.without(sstableOffsets);
- JournalPartitionAccessor journalAccessor =
JournalPartitionAccessor.build(cfs, ranges, journalOffsets);
+ if (logger.isTraceEnabled())
+ {
+ logger.trace("{} Journal validtion iterator segments selected for
{}.{}: agreed={}, sstableOffsets={}, journalRequest={}",
+
sstableIterator.previewKind().logPrefix(sstableIterator.parentId()),
+ cfs.getKeyspaceName(), cfs.getTableName(),
+ validationOffsets.toString(true),
sstableOffsets.toString(true), journalOffsets.toString(true));
+ }
+
+ JournalPartitionAccessor journalAccessor =
JournalPartitionAccessor.build(cfs, ranges, journalOffsets,
+
sstableIterator.previewKind(),
+
sstableIterator.parentId());
try
{
JournalPartitionIterator journalIterator = new
JournalPartitionIterator(journalAccessor);
@@ -90,7 +111,18 @@ public class JournalMergingValidationIterator extends
ValidationPartitionIterato
this.rangePartitionCounts = new
HashMap<>(sstableIterator.getRangePartitionCounts());
for (Map.Entry<Range<Token>, Long> entry :
journalAccessor.rangeCounts.entrySet())
rangePartitionCounts.merge(entry.getKey(), entry.getValue(),
Long::sum);
- this.merged =
UnfilteredPartitionIterators.merge(List.of(sstableIterator, journalIterator),
UnfilteredPartitionIterators.MergeListener.NOOP);
+
+ UnfilteredPartitionIterator iter =
UnfilteredPartitionIterators.merge(List.of(sstableIterator, journalIterator),
UnfilteredPartitionIterators.MergeListener.NOOP);
+ long gcBefore = dontPurgeTombstones ? Long.MIN_VALUE :
getDefaultGcBefore(cfs, nowInSec);
+ PurgeFunction purger = new PurgeFunction(nowInSec, gcBefore,
Long.MIN_VALUE, false, cfs.metadata().enforceStrictLiveness())
+ {
+ @Override
+ protected LongPredicate getPurgeEvaluator()
+ {
+ return time -> true;
+ }
+ };
+ this.merged = Transformation.apply(iter, purger);
}
catch (Throwable t)
{
@@ -170,12 +202,12 @@ public class JournalMergingValidationIterator extends
ValidationPartitionIterato
this.snapshot = snapshot;
}
- static JournalPartitionAccessor build(ColumnFamilyStore cfs,
Collection<Range<Token>> ranges, ValidationOffsets validationOffsets)
+ static JournalPartitionAccessor build(ColumnFamilyStore cfs,
Collection<Range<Token>> ranges, ValidationOffsets validationOffsets,
PreviewKind previewKind, TimeUUID parentId)
{
MutationJournal.Snapshot snapshot =
MutationJournal.instance().snapshot(validationOffsets);
try
{
- return buildFromSnapshot(snapshot, cfs, ranges,
validationOffsets);
+ return buildFromSnapshot(snapshot, cfs, ranges,
validationOffsets, previewKind, parentId);
}
catch (Throwable t)
{
@@ -184,7 +216,7 @@ public class JournalMergingValidationIterator extends
ValidationPartitionIterato
}
}
- private static JournalPartitionAccessor
buildFromSnapshot(MutationJournal.Snapshot snapshot, ColumnFamilyStore cfs,
Collection<Range<Token>> ranges, ValidationOffsets validationOffsets)
+ private static JournalPartitionAccessor
buildFromSnapshot(MutationJournal.Snapshot snapshot, ColumnFamilyStore cfs,
Collection<Range<Token>> ranges, ValidationOffsets validationOffsets,
PreviewKind previewKind, TimeUUID parentId)
{
TableMetadata metadata = cfs.metadata();
TableId tableId = metadata.id;
@@ -232,7 +264,11 @@ public class JournalMergingValidationIterator extends
ValidationPartitionIterato
}
});
- logger.info("Performing journal validation on {} partitions ({}
bytes) in {}.{}", byKey.size(), estimatedBytes[0], metadata.keyspace,
metadata.name);
+ String prefix = previewKind.logPrefix(parentId);
+ logger.info("{} Performing journal validation on {} partitions ({}
bytes) in {}.{}", prefix, byKey.size(), estimatedBytes[0], metadata.keyspace,
metadata.name);
+ if (logger.isTraceEnabled())
+ logger.trace("{} Journal scan for {}.{}: journalRequest={},
rangeCounts={}",
+ prefix, metadata.keyspace, metadata.name,
validationOffsets.toString(true), rangeCounts);
return new JournalPartitionAccessor(metadata, byKey,
estimatedBytes[0], rangeCounts, snapshot);
}
diff --git
a/src/java/org/apache/cassandra/repair/MutationTrackingPreviewRepairTask.java
b/src/java/org/apache/cassandra/repair/MutationTrackingPreviewRepairTask.java
index 0ed0c0741b..078292455d 100644
---
a/src/java/org/apache/cassandra/repair/MutationTrackingPreviewRepairTask.java
+++
b/src/java/org/apache/cassandra/repair/MutationTrackingPreviewRepairTask.java
@@ -30,6 +30,9 @@ import java.util.concurrent.atomic.AtomicInteger;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import org.apache.cassandra.concurrent.ExecutorPlus;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
@@ -63,6 +66,8 @@ import org.apache.cassandra.utils.concurrent.Future;
*/
public class MutationTrackingPreviewRepairTask extends AbstractRepairTask
{
+ private static final Logger logger =
LoggerFactory.getLogger(MutationTrackingPreviewRepairTask.class);
+
public static final String ESTABLISHING_OFFSETS = "Establishing validation
offsets";
public static final String OFFSETS_ESTABLISHED = "Validation offsets
established";
public static final String DISPATCHING_VALIDATION = "Dispatching
validation";
@@ -130,10 +135,28 @@ public class MutationTrackingPreviewRepairTask extends
AbstractRepairTask
// Extract captured targets from every sync coordinator IR
ran, merge into one
// flat ValidationOffsets, and pass to the validation dispatch.
List<Map<CoordinatorLogId, Offsets.Immutable>> targetsList =
new ArrayList<>();
+ List<String> targetDescriptions = logger.isTraceEnabled() ?
new ArrayList<>() : null;
for (MutationTrackingSyncCoordinator syncCoordinator :
irTask.getSyncCoordinators())
- targetsList.add(syncCoordinator.getCapturedTargets());
+ {
+ Map<CoordinatorLogId, Offsets.Immutable> capturedTargets =
syncCoordinator.getCapturedTargets();
+ targetsList.add(capturedTargets);
+ if (targetDescriptions != null)
+ {
+ targetDescriptions.add(String.format("%s/%s: %s",
+
syncCoordinator.getKeyspace(),
+
syncCoordinator.getRange(),
+ new
ValidationOffsets(capturedTargets).toString(true)));
+ }
+ }
ValidationOffsets validationOffsets =
ValidationOffsets.flatten(targetsList);
+ if (targetDescriptions != null)
+ {
+ logger.trace("[#{}] Captured validation targets from {}
sync coordinators: {}",
+ parentSession, targetsList.size(),
targetDescriptions);
+ logger.trace("[#{}] Agreed validation target: {}",
parentSession, validationOffsets.toString(true));
+ }
+
runValidation(promise, validationOffsets, executor,
validationScheduler);
},
failure -> promise.tryFailure(new RuntimeException("unable to
capture offset: " + failure.getMessage(), failure))
@@ -176,6 +199,11 @@ public class MutationTrackingPreviewRepairTask extends
AbstractRepairTask
if (!participants.isEmpty())
allParticipants.addAll(participants);
+ if (logger.isTraceEnabled())
+ logger.trace("[#{}] Dispatching tracked validation for
{}.{} over {} to {} participants: {}",
+ parentSession, keyspace, String.join(",",
tableNames), commonRange.ranges,
+ participants.size(), participants);
+
for (String tableName : tableNames)
{
RepairJobDesc desc = new RepairJobDesc(parentSession,
TimeUUID.Generator.nextTimeUUID(), keyspace, tableName, commonRange.ranges);
@@ -211,9 +239,8 @@ public class MutationTrackingPreviewRepairTask extends
AbstractRepairTask
emitValidationComplete(promise, mismatchesByTable,
allParticipants, offset);
},
failure -> {
- // Any validation failure fails the whole preview. Cancel
siblings so they
- // don't keep running to completion or their own timeout,
holding merkle
- // trees and registry entries for a result nobody will
consume.
+ // Any validation failure fails the whole preview. Cancel
siblings so they don't keep running
+ // to completion, holding merkle trees and registry
entries for a result nobody will consume.
promise.tryFailure(new RuntimeException("Tracked
validation failed: " + failure.getMessage(), failure));
for (ValidationHandle sibling : handles)
sibling.coordinator.cancel();
diff --git
a/src/java/org/apache/cassandra/repair/MutationTrackingValidationCoordinator.java
b/src/java/org/apache/cassandra/repair/MutationTrackingValidationCoordinator.java
index f876ebb47b..93feb5ff0a 100644
---
a/src/java/org/apache/cassandra/repair/MutationTrackingValidationCoordinator.java
+++
b/src/java/org/apache/cassandra/repair/MutationTrackingValidationCoordinator.java
@@ -24,8 +24,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
@@ -33,7 +31,6 @@ import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.RequestFailure;
@@ -105,12 +102,6 @@ public class MutationTrackingValidationCoordinator
}
future.addListener(() -> REGISTRY.remove(desc, this));
- long timeout =
DatabaseDescriptor.getMutationTrackingValidationTimeout(TimeUnit.SECONDS);
- ctx.scheduledTasks().schedule(() -> {
- if (!future.isDone())
- fail(new TimeoutException("Tracked validation timed out after
" + timeout + "s for " + desc));
- }, timeout, TimeUnit.SECONDS);
-
ValidationRequest request = new ValidationRequest(desc,
FBUtilities.nowInSeconds(), false, offset);
for (InetAddressAndPort participant : participants)
{
@@ -141,7 +132,7 @@ public class MutationTrackingValidationCoordinator
/**
* Invoked by {@link RepairMessageVerbHandler} on receipt of {@link
Verb#MT_VALIDATION_RSP}.
* Trees may be null if the participant reported validation failure.
Synchronized with
- * {@link #complete()} and {@link #fail(Throwable)} so a concurrent
timeout can't race
+ * {@link #complete()} and {@link #fail(Throwable)} so a concurrent
failure cannot race
* a tree being stored.
*/
synchronized void onTreesReceived(InetAddressAndPort from, MerkleTrees
trees)
diff --git a/src/java/org/apache/cassandra/replication/ActivatedTransfers.java
b/src/java/org/apache/cassandra/replication/ActivatedTransfers.java
index 701038894b..1ab3f56648 100644
--- a/src/java/org/apache/cassandra/replication/ActivatedTransfers.java
+++ b/src/java/org/apache/cassandra/replication/ActivatedTransfers.java
@@ -184,6 +184,11 @@ public class ActivatedTransfers implements
Iterable<ShortMutationId>
return transfers.isEmpty();
}
+ public int size()
+ {
+ return transfers.size();
+ }
+
public static Bounds<Token> covering(Collection<SSTableReader> sstables)
{
Preconditions.checkArgument(!sstables.isEmpty());
@@ -251,9 +256,32 @@ public class ActivatedTransfers implements
Iterable<ShortMutationId>
@Override
public String toString()
{
- return "ActivatedTransfers{" +
- "transfers=" + transfers +
- '}';
+ return toString(false);
+ }
+
+ /**
+ * Transfers have no range structure to collapse, so the verbose form
lists ids. Ids are rendered as
+ * {@code hostId:hostLogId:offset} rather than via {@link
ShortMutationId#toString()} to keep the line
+ * readable when several transfers are present. Only enable verbose output
for trace logging that is expected
+ * to be verbose.
+ */
+ public String toString(boolean verbose)
+ {
+ StringBuilder builder = new
StringBuilder("ActivatedTransfers{transfers=").append(size());
+ if (!verbose || transfers.isEmpty())
+ return builder.append('}').toString();
+
+ boolean first = true;
+ for (ActivatedTransfer transfer : transfers)
+ {
+ if (!first)
+ builder.append(',');
+ first = false;
+ builder.append(transfer.id.hostId())
+ .append(':').append(transfer.id.hostLogId())
+ .append(':').append(transfer.id.offset());
+ }
+ return builder.append('}').toString();
}
public static final VersionedSerializer<ActivatedTransfers> serializer =
new VersionedSerializer<>()
diff --git
a/src/java/org/apache/cassandra/replication/ImmutableCoordinatorLogOffsets.java
b/src/java/org/apache/cassandra/replication/ImmutableCoordinatorLogOffsets.java
index df851339b0..e5c00c2784 100644
---
a/src/java/org/apache/cassandra/replication/ImmutableCoordinatorLogOffsets.java
+++
b/src/java/org/apache/cassandra/replication/ImmutableCoordinatorLogOffsets.java
@@ -129,10 +129,32 @@ public class ImmutableCoordinatorLogOffsets implements
CoordinatorLogOffsets<Off
@Override
public String toString()
{
- return "ImmutableCoordinatorLogOffsets{" +
- "mutations=" + mutations +
- ", transfers=" + transfers +
- '}';
+ return toString(false);
+ }
+
+ /**
+ * The default form reports counts only; the verbose form renders per-log
offset ranges and transfer ids. Use
+ * {@link ValidationOffsets#toString(boolean)} when comparing an SSTable's
offsets with an agreed target, so the
+ * two renderings match. Only enable verbose output for trace logging that
is expected to be verbose.
+ */
+ public String toString(boolean verbose)
+ {
+ StringBuilder builder = new
StringBuilder("ImmutableCoordinatorLogOffsets{logs=").append(mutations.size());
+ if (!verbose)
+ return builder.append(",
transfers=").append(transfers.size()).append('}').toString();
+
+ boolean first = true;
+ for (Long logId : mutations)
+ {
+ builder.append(first ? "" : ", ");
+ first = false;
+
+ Offsets.Immutable offsets = mutations.getOffsets(logId);
+ builder.append(CoordinatorLogId.hostId(logId))
+
.append(':').append(CoordinatorLogId.hostLogId(logId)).append(':');
+ Offsets.appendRanges(builder, offsets);
+ }
+ return builder.append(",
transfers=").append(transfers.toString(true)).append('}').toString();
}
@NotThreadSafe
diff --git a/src/java/org/apache/cassandra/replication/Offsets.java
b/src/java/org/apache/cassandra/replication/Offsets.java
index bb98620eee..f8960e2dbb 100644
--- a/src/java/org/apache/cassandra/replication/Offsets.java
+++ b/src/java/org/apache/cassandra/replication/Offsets.java
@@ -23,6 +23,8 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import javax.annotation.Nullable;
+
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
@@ -180,6 +182,28 @@ public abstract class Offsets implements
Iterable<ShortMutationId>
return builder.append('}').toString();
}
+ static void appendRanges(StringBuilder builder, Offsets.RangeIterator
ranges)
+ {
+ builder.append('{');
+ for (int rendered = 0; ranges.tryAdvance(); rendered++)
+ {
+ if (rendered > 0)
+ builder.append(',');
+
builder.append('[').append(ranges.start()).append(',').append(ranges.end()).append(']');
+ }
+ builder.append('}');
+ }
+
+ static void appendRanges(StringBuilder builder, @Nullable Immutable
offsets)
+ {
+ if (offsets == null)
+ {
+ builder.append("{}");
+ return;
+ }
+ appendRanges(builder, offsets.rangeIterator());
+ }
+
public boolean contains(int offset)
{
if (size == 0)
diff --git a/src/java/org/apache/cassandra/replication/ValidationOffsets.java
b/src/java/org/apache/cassandra/replication/ValidationOffsets.java
index 801d0ff1aa..a22dd35559 100644
--- a/src/java/org/apache/cassandra/replication/ValidationOffsets.java
+++ b/src/java/org/apache/cassandra/replication/ValidationOffsets.java
@@ -149,7 +149,33 @@ public final class ValidationOffsets
@Override
public String toString()
{
- return "ValidationOffsets{logs=" + perLog.size() + '}';
+ return toString(false);
+ }
+
+ /**
+ * Offsets are rendered as ranges, never as expanded ids: a single range
can span thousands of offsets and
+ * {@link Offsets#collectIds} allocates one {@link ShortMutationId} per
offset. Only enable this for trace
+ * logging that is expected to be verbose.
+ */
+ public String toString(boolean verbose)
+ {
+ StringBuilder builder = new
StringBuilder("ValidationOffsets{logs=").append(perLog.size());
+ if (!verbose || perLog.isEmpty())
+ return builder.append('}').toString();
+
+ builder.append(", ");
+ boolean first = true;
+ for (Map.Entry<Long, Offsets.Immutable> entry : perLog.entrySet())
+ {
+ if (!first)
+ builder.append(", ");
+ first = false;
+
+ CoordinatorLogId logId = CoordinatorLogId.fromLong(entry.getKey());
+
builder.append(logId.hostId()).append(':').append(logId.hostLogId()).append(':');
+ Offsets.appendRanges(builder, entry.getValue());
+ }
+ return builder.append('}').toString();
}
public static final UnversionedSerializer<ValidationOffsets> serializer =
new UnversionedSerializer<>()
diff --git a/src/java/org/apache/cassandra/service/StorageService.java
b/src/java/org/apache/cassandra/service/StorageService.java
index 5cb67e0879..2ae74635fa 100644
--- a/src/java/org/apache/cassandra/service/StorageService.java
+++ b/src/java/org/apache/cassandra/service/StorageService.java
@@ -5589,18 +5589,6 @@ public class StorageService extends
NotificationBroadcasterSupport implements IE
logger.info("MutationTrackingSyncTimeout set to {}ms via JMX",
timeoutInMillis);
}
- public long getMutationTrackingValidationTimeout()
- {
- return
DatabaseDescriptor.getMutationTrackingValidationTimeout(MILLISECONDS);
- }
-
- public void setMutationTrackingValidationTimeout(long timeoutInMillis)
- {
- checkArgument(timeoutInMillis > 0);
-
DatabaseDescriptor.setMutationTrackingValidationTimeout(timeoutInMillis);
- logger.info("MutationTrackingValidationTimeout set to {}ms via JMX",
timeoutInMillis);
- }
-
public void evictHungRepairs()
{
logger.info("StorageService#clearPaxosRateLimiters called via jmx");
diff --git a/src/java/org/apache/cassandra/service/StorageServiceMBean.java
b/src/java/org/apache/cassandra/service/StorageServiceMBean.java
index d881d7de5f..277096353f 100644
--- a/src/java/org/apache/cassandra/service/StorageServiceMBean.java
+++ b/src/java/org/apache/cassandra/service/StorageServiceMBean.java
@@ -1361,9 +1361,6 @@ public interface StorageServiceMBean extends
NotificationEmitter
long getMutationTrackingSyncTimeout();
void setMutationTrackingSyncTimeout(long timeoutInMillis);
- long getMutationTrackingValidationTimeout();
- void setMutationTrackingValidationTimeout(long timeoutInMillis);
-
public void evictHungRepairs();
public void clearPaxosRepairs();
public void setSkipPaxosRepairCompatibilityCheck(boolean v);
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedPreviewRepairTest.java
b/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedPreviewRepairTest.java
index c01e0ab06d..3ff9b4e4c6 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedPreviewRepairTest.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedPreviewRepairTest.java
@@ -49,7 +49,6 @@ import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
-import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.CassandraWriteContext;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.DecoratedKey;
@@ -1240,11 +1239,6 @@ public class TrackedPreviewRepairTest extends
TestBaseImpl
createTrackedSchema(CLUSTER, keyspace, table);
IInvokableInstance coordinator = CLUSTER.get(1);
- long originalTimeoutMillis = coordinator.callOnInstance(() ->
DatabaseDescriptor.getMutationTrackingValidationTimeout(TimeUnit.MILLISECONDS));
-
- // Make the validation timeout essentially infinite, so the only path
to resolution is an explicit failure.
- long longTimeoutMillis = TimeUnit.MINUTES.toMillis(10);
- coordinator.runOnInstance(() ->
DatabaseDescriptor.setMutationTrackingValidationTimeout(longTimeoutMillis));
coordinator.runOnInstance(ThrowOnTreeDifference::activate);
ExecutorService executor = Executors.newSingleThreadExecutor();
@@ -1253,13 +1247,12 @@ public class TrackedPreviewRepairTest extends
TestBaseImpl
Future<NodeToolResult> resultFuture = submitValidate(executor,
coordinator, keyspace);
// This should terminate immediately...
- NodeToolResult result = resultFuture.get(longTimeoutMillis / 2,
TimeUnit.MILLISECONDS);
+ NodeToolResult result = resultFuture.get(1, TimeUnit.MINUTES);
result.asserts().failure();
}
finally
{
coordinator.runOnInstance(ThrowOnTreeDifference::deactivate);
- coordinator.runOnInstance(() ->
DatabaseDescriptor.setMutationTrackingValidationTimeout(originalTimeoutMillis));
ExecutorUtils.shutdownAndWait(10, TimeUnit.SECONDS, executor);
}
}
@@ -1271,11 +1264,6 @@ public class TrackedPreviewRepairTest extends
TestBaseImpl
IInvokableInstance coordinator = CLUSTER.get(1);
IInvokableInstance node2 = CLUSTER.get(2);
- long originalTimeoutMillis = coordinator.callOnInstance(() ->
DatabaseDescriptor.getMutationTrackingValidationTimeout(TimeUnit.MILLISECONDS));
-
- // Make the validation timeout essentially infinite, so the only path
to resolution is an explicit failure.
- long longTimeoutMillis = TimeUnit.MINUTES.toMillis(10);
- coordinator.runOnInstance(() ->
DatabaseDescriptor.setMutationTrackingValidationTimeout(longTimeoutMillis));
node2.runOnInstance(PauseValidationRequest::activateToFail);
ExecutorService executor = Executors.newSingleThreadExecutor();
@@ -1284,13 +1272,12 @@ public class TrackedPreviewRepairTest extends
TestBaseImpl
Future<NodeToolResult> resultFuture = submitValidate(executor,
coordinator, keyspace);
// This should terminate immediately...
- NodeToolResult result = resultFuture.get(longTimeoutMillis / 2,
TimeUnit.MILLISECONDS);
+ NodeToolResult result = resultFuture.get(1, TimeUnit.MINUTES);
result.asserts().failure();
}
finally
{
node2.runOnInstance(PauseValidationRequest::releaseAndDeactivate);
- coordinator.runOnInstance(() ->
DatabaseDescriptor.setMutationTrackingValidationTimeout(originalTimeoutMillis));
ExecutorUtils.shutdownAndWait(10, TimeUnit.SECONDS, executor);
}
}
diff --git
a/test/unit/org/apache/cassandra/db/repair/JournalMergingValidationIteratorTest.java
b/test/unit/org/apache/cassandra/db/repair/JournalMergingValidationIteratorTest.java
new file mode 100644
index 0000000000..32d7cf2e9f
--- /dev/null
+++
b/test/unit/org/apache/cassandra/db/repair/JournalMergingValidationIteratorTest.java
@@ -0,0 +1,228 @@
+/*
+ * 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.db.repair;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.repair.ValidationPartitionIterator;
+import org.apache.cassandra.replication.CoordinatorLogId;
+import org.apache.cassandra.replication.MutationId;
+import org.apache.cassandra.replication.MutationJournal;
+import org.apache.cassandra.replication.MutationTrackingService;
+import org.apache.cassandra.replication.Offsets;
+import org.apache.cassandra.replication.ShortMutationId;
+import org.apache.cassandra.replication.ValidationOffsets;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.ReplicationType;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.TableParams;
+import org.apache.cassandra.service.ActiveRepairService;
+import org.apache.cassandra.streaming.PreviewKind;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.TimeUUID;
+
+import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
+
+public class JournalMergingValidationIteratorTest
+{
+ private static final AtomicInteger counter = new AtomicInteger();
+ private static InetAddressAndPort coordinator;
+ private static Token MT;
+
+ @BeforeClass
+ public static void setupClass() throws Exception
+ {
+ SchemaLoader.prepareServer();
+ MutationJournal.start();
+ MutationTrackingService.start();
+ coordinator = InetAddressAndPort.getByName("10.0.0.1");
+ MT = DatabaseDescriptor.getPartitioner().getMinimumToken();
+ }
+
+ private ColumnFamilyStore createTable()
+ {
+ String ks = "ks_" + counter.incrementAndGet();
+ String tbl = "tbl";
+ TableMetadata metadata =
+ TableMetadata.builder(ks, tbl)
+ .addPartitionKeyColumn("k", Int32Type.instance)
+ .addRegularColumn("v", Int32Type.instance)
+
.params(TableParams.builder().gcGraceSeconds(0).build())
+ .build();
+ SchemaLoader.createKeyspace(ks, KeyspaceParams.simple(1,
ReplicationType.tracked), metadata);
+ ColumnFamilyStore cfs =
Schema.instance.getColumnFamilyStoreInstance(metadata.id);
+ cfs.disableAutoCompaction();
+ return cfs;
+ }
+
+ private void registerRepair(TimeUUID parentId, TimeUUID sessionID,
ColumnFamilyStore cfs, Range<Token> range)
+ {
+ ActiveRepairService.instance().registerParentRepairSession(parentId,
+ coordinator,
+
List.of(cfs),
+
Set.of(range),
+ false,
+
ActiveRepairService.UNREPAIRED_SSTABLE,
+ false,
+
PreviewKind.REPAIRED);
+ }
+
+ private Mutation createInsertMutation(TableMetadata tableMetadata, int k,
int v, long timestamp)
+ {
+ DecoratedKey key =
tableMetadata.partitioner.decorateKey(ByteBufferUtil.bytes(k));
+ MutationId mutationId =
MutationTrackingService.instance().nextMutationId(tableMetadata.keyspace,
key.getToken());
+ org.apache.cassandra.db.SimpleBuilders.MutationBuilder builder = new
org.apache.cassandra.db.SimpleBuilders.MutationBuilder(mutationId,
tableMetadata.keyspace, key);
+ builder.timestamp(timestamp);
+ builder.update(tableMetadata).row().add("v", v);
+ return builder.build();
+ }
+
+ private Mutation createDeleteMutation(TableMetadata tableMetadata, int k,
long timestamp, long nowInSec)
+ {
+ DecoratedKey key =
tableMetadata.partitioner.decorateKey(ByteBufferUtil.bytes(k));
+ MutationId mutationId =
MutationTrackingService.instance().nextMutationId(tableMetadata.keyspace,
key.getToken());
+ org.apache.cassandra.db.SimpleBuilders.MutationBuilder builder = new
org.apache.cassandra.db.SimpleBuilders.MutationBuilder(mutationId,
tableMetadata.keyspace, key);
+ builder.timestamp(timestamp);
+ org.apache.cassandra.db.partitions.PartitionUpdate.SimpleBuilder
update = builder.update(tableMetadata);
+ update.nowInSec(nowInSec);
+ update.delete();
+ return builder.build();
+ }
+
+ @Test
+ public void testTombstoneInSSTableShadowsOlderWriteInJournal() throws
Exception
+ {
+ ColumnFamilyStore cfs = createTable();
+ Range<Token> range = new Range<>(MT, MT);
+
+ // Write a deletion for key 1 with timestamp 2000L and
localDeletionTime in the past into the table and flush to SSTable.
+ long deleteTimestamp = 2000L;
+ long localDeletionTime = 100;
+ Mutation delete = createDeleteMutation(cfs.metadata(), 1,
deleteTimestamp, localDeletionTime);
+ delete.apply();
+ cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS);
+
+ Assert.assertEquals(1, cfs.getLiveSSTables().size());
+
+ // Write an insert into the journal with timestamp 1000L.
+ // It is not flushed, so it only exists in the journal.
+ Mutation insert = createInsertMutation(cfs.metadata(), 1, 1, 1000L);
+ CoordinatorLogId coordLogId =
CoordinatorLogId.fromLong(insert.id().logId());
+ ShortMutationId journalId = new ShortMutationId(coordLogId.asLong(),
insert.id().offset());
+ MutationJournal.instance().write(journalId, insert);
+ MutationJournal.instance().closeCurrentSegmentForTestingIfNonEmpty();
+
+ int maxOffset = Math.max(delete.id().offset(), insert.id().offset());
+ ValidationOffsets validationOffsets = new
ValidationOffsets(Map.of(coordLogId, new Offsets.Immutable(coordLogId, new
int[]{ 0, maxOffset })));
+
+ TimeUUID parentId = nextTimeUUID();
+ TimeUUID sessionID = nextTimeUUID();
+ registerRepair(parentId, sessionID, cfs, range);
+
+ long nowInSec = 200; // nowInSec > localDeletionTime (100)
+ boolean dontPurgeTombstones = false;
+
+ try (ValidationPartitionIterator vi =
cfs.getRepairManager().getValidationIterator(Collections.singleton(range),
parentId, sessionID, false, nowInSec, dontPurgeTombstones, validationOffsets,
null))
+ {
+ // If the SSTable iterator prematurely purges the tombstone before
merging,
+ // the tombstone will fail to shadow the insert from the journal,
and the insert will be resurrected.
+ // With correct purging on the final iterator, the tombstone
shadows the insert, then is purged,
+ // leaving no live or resurrected data.
+ Assert.assertFalse("Deleted row was resurrected because tombstone
was purged before merge", vi.hasNext());
+ }
+ }
+
+ @Test
+ public void testTombstoneInJournalIsPurgedOnFinalIterator() throws
Exception
+ {
+ ColumnFamilyStore cfs = createTable();
+ Range<Token> range = new Range<>(MT, MT);
+
+ // Write an expired deletion into the journal.
+ long deleteTimestamp = 2000L;
+ long localDeletionTime = 100;
+ Mutation delete = createDeleteMutation(cfs.metadata(), 2,
deleteTimestamp, localDeletionTime);
+ CoordinatorLogId coordLogId =
CoordinatorLogId.fromLong(delete.id().logId());
+ ShortMutationId journalId = new ShortMutationId(coordLogId.asLong(),
delete.id().offset());
+ MutationJournal.instance().write(journalId, delete);
+ MutationJournal.instance().closeCurrentSegmentForTestingIfNonEmpty();
+
+ ValidationOffsets validationOffsets = new
ValidationOffsets(Map.of(coordLogId, new Offsets.Immutable(coordLogId, new
int[]{ 0, delete.id().offset() })));
+
+ TimeUUID parentId = nextTimeUUID();
+ TimeUUID sessionID = nextTimeUUID();
+ registerRepair(parentId, sessionID, cfs, range);
+
+ long nowInSec = 200;
+ boolean dontPurgeTombstones = false;
+
+ try (ValidationPartitionIterator vi =
cfs.getRepairManager().getValidationIterator(Collections.singleton(range),
parentId, sessionID, false, nowInSec, dontPurgeTombstones, validationOffsets,
null))
+ {
+ // With final iterator purging, expired tombstones from the
journal must be purged when dontPurgeTombstones is false.
+ Assert.assertFalse("Expired tombstone from journal must be purged
on final iterator", vi.hasNext());
+ }
+ }
+
+ @Test
+ public void testTombstonesRetainedWhenDontPurgeTombstonesIsTrue() throws
Exception
+ {
+ ColumnFamilyStore cfs = createTable();
+ Range<Token> range = new Range<>(MT, MT);
+
+ long deleteTimestamp = 2000L;
+ long localDeletionTime = 100;
+ Mutation delete = createDeleteMutation(cfs.metadata(), 3,
deleteTimestamp, localDeletionTime);
+ CoordinatorLogId coordLogId =
CoordinatorLogId.fromLong(delete.id().logId());
+ ShortMutationId journalId = new ShortMutationId(coordLogId.asLong(),
delete.id().offset());
+ MutationJournal.instance().write(journalId, delete);
+ MutationJournal.instance().closeCurrentSegmentForTestingIfNonEmpty();
+
+ ValidationOffsets validationOffsets = new
ValidationOffsets(Map.of(coordLogId, new Offsets.Immutable(coordLogId, new
int[]{ 0, delete.id().offset() })));
+
+ TimeUUID parentId = nextTimeUUID();
+ TimeUUID sessionID = nextTimeUUID();
+ registerRepair(parentId, sessionID, cfs, range);
+
+ long nowInSec = 200;
+ boolean dontPurgeTombstones = true;
+
+ try (ValidationPartitionIterator vi =
cfs.getRepairManager().getValidationIterator(Collections.singleton(range),
parentId, sessionID, false, nowInSec, dontPurgeTombstones, validationOffsets,
null))
+ {
+ Assert.assertTrue("Tombstones must be retained when
dontPurgeTombstones is true", vi.hasNext());
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]