Bootstrap must first wait for a quorum to apply the sync point to ensure the DurableBefore Majority condition holds transitively,

since later quorums may include the bootstrapping node which does not participate in the durability of preceding transactions
Also Improve:
 - Apply MaxDecidedRX filtering to CommandsForKey RX dependencies
 - Optimise AbstractRanges.sliceMinimal (burn test hotspot)
 - Don't start shard schedulers on topology changes if not already started
 - Only registerTransitive if waitingOnSync
 - Reserve DurabilityRequest until ShardDurability is started
 - Don't notify progress log if stopped
 - Remove duplicated CFK unmanaged filtering
Also fix:
 - Edge case in notification across bootstrap boundary by resubmitting Unmanaged to be recomputed
 - Serialization of CommandsForKey.TxnInfo.missing() collection when non-identity flag bits are present

patch by Benedict; reviewed by Alex Petrov for CASSANDRA-20811
This commit is contained in:
Benedict Elliott Smith 2025-08-01 11:03:43 +01:00
parent f02020f06d
commit 46bda174d5
9 changed files with 31 additions and 177 deletions

@ -1 +1 @@
Subproject commit 20df7e5e5ad7bae442ff6fe4ee716eb7104336c1
Subproject commit d5de79b56f792d3b6d868ca46845b830e8906828

View File

@ -55,11 +55,9 @@ import accord.local.RedundantBefore;
import accord.local.SafeCommandStore;
import accord.local.cfk.CommandsForKey;
import accord.primitives.PartialTxn;
import accord.primitives.RangeDeps;
import accord.primitives.Ranges;
import accord.primitives.RoutableKey;
import accord.primitives.Route;
import accord.primitives.Status;
import accord.primitives.Timestamp;
import accord.primitives.TxnId;
import accord.utils.Invariants;
@ -226,14 +224,6 @@ public class AccordCommandStore extends CommandStore
return commandsForRanges;
}
@Override
public void markShardDurable(SafeCommandStore safeStore, TxnId globalSyncId, Ranges ranges, Status.Durability durability)
{
super.markShardDurable(safeStore, globalSyncId, ranges, durability);
if (durability.compareTo(Status.Durability.UniversalOrInvalidated) >= 0)
commandsForRanges.gcBefore(globalSyncId, ranges);
}
@Override
public boolean inStore()
{
@ -400,43 +390,6 @@ public class AccordCommandStore extends CommandStore
{
}
public void registerTransitive(SafeCommandStore safeStore, RangeDeps rangeDeps)
{
if (rangeDeps.isEmpty())
return;
RedundantBefore redundantBefore = unsafeGetRedundantBefore();
CommandStores.RangesForEpoch ranges = safeStore.ranges();
// used in places such as accord.local.CommandStore.fetchMajorityDeps
// We find a set of dependencies for a range then update CommandsFor to know about them
Ranges allRanges = safeStore.ranges().all();
Ranges coordinateRanges = Ranges.EMPTY;
long coordinateEpoch = -1;
try (ExclusiveCaches caches = lockCaches())
{
for (int i = 0; i < rangeDeps.txnIdCount(); i++)
{
TxnId txnId = rangeDeps.txnId(i);
AccordCacheEntry<TxnId, Command> state = caches.commands().getUnsafe(txnId);
if (state != null && state.isLoaded() && state.getExclusive() != null && state.getExclusive().known().isDefinitionKnown())
continue;
Ranges addRanges = rangeDeps.ranges(i).slice(allRanges);
if (addRanges.isEmpty()) continue;
if (coordinateEpoch != txnId.epoch())
{
coordinateEpoch = txnId.epoch();
coordinateRanges = ranges.allAt(txnId.epoch());
}
if (addRanges.intersects(coordinateRanges)) continue;
addRanges = redundantBefore.removeGcBefore(txnId, txnId, addRanges);
if (addRanges.isEmpty()) continue;
commandsForRanges().mergeTransitive(txnId, addRanges, Ranges::with);
}
}
}
public void appendCommands(List<CommandUpdate> diffs, Runnable onFlush)
{
for (int i = 0; i < diffs.size(); i++)
@ -668,8 +621,8 @@ public class AccordCommandStore extends CommandStore
StringBuilder sb = new StringBuilder("[");
if (metadata != null)
sb.append(metadata).append('|');
sb.append(tableId.lsb());
sb.append(',').append(id).append(',').append(node.id().id).append(']');
sb.append(tableId);
sb.append('|').append(id).append(',').append(node.id().id).append(']');
return sb.toString();
}
}

View File

@ -85,13 +85,6 @@ class AccordExecutorLoops
};
}
AccordExecutor.Task activeTaskForCurrentThread()
{
Thread thread = Thread.currentThread();
LoopTask task = tasks.get(thread.getId());
return task == null ? null : task.runningTask();
}
public Stream<? extends DebuggableTaskRunner> active()
{
return tasks.values().stream();

View File

@ -302,7 +302,8 @@ public class AccordKeyspace
if (current == null)
return null;
CommandsForKey updated = current.withRedundantBeforeAtLeast(redundantBefore.gcBefore());
// TODO (desired): consider whether better to not compact any validation failures, since we expect is already overwritten
CommandsForKey updated = current.withRedundantBeforeAtLeast(redundantBefore.gcBefore(), false);
if (current == updated)
return row;

View File

@ -1038,7 +1038,7 @@ public abstract class AccordTask<R> extends SubmittableTask implements Function<
{
CommandsForRanges.Summary summary = summaryLoader.ifRelevant(state);
if (summary != null)
summaries.put(summary.txnId, summary);
summaries.put(summary.plainTxnId(), summary);
}
}
@ -1059,7 +1059,10 @@ public abstract class AccordTask<R> extends SubmittableTask implements Function<
CommandsForRanges.Summary summary = summaryLoader.load(txnId);
if (summary != null)
{
summaries.putIfAbsent(txnId, summary);
summaryLoader.maybeRecordFutureRx(summary);
}
});
}
@ -1086,7 +1089,7 @@ public abstract class AccordTask<R> extends SubmittableTask implements Function<
void startInternal(Caches caches)
{
summaryLoader = commandStore.commandsForRanges().loader(preLoadContext.primaryTxnId(), preLoadContext.loadKeysFor(), keysOrRanges);
summaryLoader.forEachInCache(keysOrRanges, summary -> summaries.put(summary.txnId, summary), caches);
summaryLoader.forEachInCache(keysOrRanges, summary -> summaries.put(summary.plainTxnId(), summary), caches);
caches.commands().register(commandWatcher);
}

View File

@ -21,18 +21,14 @@ package org.apache.cassandra.service.accord;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import javax.annotation.Nullable;
import accord.api.RoutingKey;
@ -49,7 +45,6 @@ import accord.primitives.RangeRoute;
import accord.primitives.Ranges;
import accord.primitives.Routable;
import accord.primitives.Timestamp;
import accord.primitives.Txn;
import accord.primitives.Txn.Kind.Kinds;
import accord.primitives.TxnId;
import accord.primitives.Unseekable;
@ -65,7 +60,6 @@ import org.apache.cassandra.utils.btree.BTreeSet;
import org.apache.cassandra.utils.btree.IntervalBTree;
import org.apache.cassandra.utils.concurrent.IntrusiveStack;
import static accord.local.CommandSummaries.SummaryStatus.NOT_DIRECTLY_WITNESSED;
import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndHelper.endWithStart;
import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndHelper.keyEndWithStart;
import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndHelper.keyStartWithEnd;
@ -176,7 +170,6 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
private final AccordCommandStore commandStore;
private final RangeSearcher searcher;
private final AtomicReference<NavigableMap<TxnId, Ranges>> transitive = new AtomicReference<>(new TreeMap<>());
// TODO (desired): manage memory consumed by this auxillary information
private final Object2ObjectHashMap<TxnId, RangeRoute> cachedRangeTxnsById = new Object2ObjectHashMap<>();
private Object[] cachedRangeTxnsByRange = IntervalBTree.empty();
@ -356,80 +349,28 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
public CommandsForRanges.Loader loader(@Nullable TxnId primaryTxnId, LoadKeysFor loadKeysFor, Unseekables<?> keysOrRanges)
{
RedundantBefore redundantBefore = commandStore.unsafeGetRedundantBefore();
return Loader.loader(redundantBefore, primaryTxnId, loadKeysFor, keysOrRanges, this::newLoader);
MaxDecidedRX maxDecidedRX = commandStore.unsafeGetMaxDecidedRX();
return SummaryLoader.loader(redundantBefore, maxDecidedRX, primaryTxnId, loadKeysFor, keysOrRanges, this::newLoader);
}
private Loader newLoader(@Nullable TxnId primaryTxnId, Unseekables<?> searchKeysOrRanges, RedundantBefore redundantBefore, Kinds testKind, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep)
private Loader newLoader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, @Nullable TxnId primaryTxnId, Unseekables<?> searchKeysOrRanges, Kinds testKind, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep)
{
MaxDecidedRX maxDecidedRX = null;
if (primaryTxnId != null && primaryTxnId.is(Txn.Kind.ExclusiveSyncPoint) && findAsDep == null)
maxDecidedRX = commandStore.unsafeGetMaxDecidedRX();
return new Loader(this, primaryTxnId, searchKeysOrRanges, redundantBefore, testKind, minTxnId, maxTxnId, findAsDep, maxDecidedRX);
}
private void updateTransitive(UnaryOperator<NavigableMap<TxnId, Ranges>> update)
{
while (true)
{
NavigableMap<TxnId, Ranges> prev = transitive.get();
NavigableMap<TxnId, Ranges> next = update.apply(prev);
if (next == null || prev == next)
return;
if (transitive.compareAndSet(prev, next))
return;
}
}
public void mergeTransitive(TxnId txnId, Ranges ranges, BiFunction<? super Ranges, ? super Ranges, ? extends Ranges> remappingFunction)
{
updateTransitive(transitive -> {
NavigableMap<TxnId, Ranges> next = new TreeMap<>(transitive);
next.merge(txnId, ranges, remappingFunction);
return next;
});
}
public void gcBefore(TxnId gcBefore, Ranges ranges)
{
updateTransitive(transitive -> {
NavigableMap<TxnId, Ranges> next = null;
Iterator<Map.Entry<TxnId, Ranges>> iterator = transitive.headMap(gcBefore).entrySet().iterator();
while (iterator.hasNext())
{
Map.Entry<TxnId, Ranges> e = iterator.next();
Ranges newRanges = e.getValue().without(ranges);
if (!newRanges.isEmpty())
{
if (next == null)
next = new TreeMap<>();
next.put(e.getKey(), newRanges);
}
}
return next;
});
return new Loader(this, redundantBefore, maxDecidedRX, primaryTxnId, searchKeysOrRanges, testKind, minTxnId, maxTxnId, findAsDep);
}
}
public static class Loader extends Summary.Loader
public static class Loader extends SummaryLoader
{
private final Manager manager;
private final MaxDecidedRX maxDecidedRX;
private final TxnId primaryTxnId;
@Nullable
private final TxnId minDecidedId;
public Loader(Manager manager, TxnId primaryTxnId, Unseekables<?> searchKeysOrRanges, RedundantBefore redundantBefore, Kinds testKinds, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep, @Nullable MaxDecidedRX maxDecidedRX)
public Loader(Manager manager, RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Unseekables<?> searchKeysOrRanges, Kinds testKinds, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep)
{
super(primaryTxnId, searchKeysOrRanges, redundantBefore, testKinds, minTxnId, maxTxnId, findAsDep);
super(redundantBefore, maxDecidedRX, primaryTxnId, searchKeysOrRanges, testKinds, minTxnId, maxTxnId, findAsDep);
this.manager = manager;
this.maxDecidedRX = maxDecidedRX;
this.primaryTxnId = primaryTxnId;
this.minDecidedId = MaxDecidedRX.minDecidedDependencyId(maxDecidedRX, searchKeysOrRanges, primaryTxnId);
}
public void intersects(Consumer<TxnId> forEach)
{
// TODO (expected): use the ranges we find to filter results by MaxDecidedRX (don't just consume the TxnId)
switch (searchKeysOrRanges.domain())
{
case Range:
@ -440,38 +381,11 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
for (Unseekable key : searchKeysOrRanges)
manager.searcher.search(manager.commandStore.id(), (TokenKey) key, minTxnId, maxTxnId, minDecidedId).consume(forEach);
}
NavigableMap<TxnId, Ranges> transitive = manager.transitive.get();
if (!transitive.isEmpty())
{
for (Map.Entry<TxnId, Ranges> e : transitive.tailMap(minTxnId, true).entrySet())
{
if (e.getValue().intersects(searchKeysOrRanges))
forEach.accept(e.getKey());
}
}
}
boolean isRelevant(TxnIdInterval txnIdInterval)
boolean isMaybeRelevant(TxnIdInterval txnIdInterval)
{
if (maxDecidedRX == null)
return true;
if (!isMaybeRelevant(txnIdInterval.txnId))
return false;
TxnId minRelevantId = MaxDecidedRX.minDecidedDependencyId(maxDecidedRX, Ranges.of(txnIdInterval), primaryTxnId);
return isRelevant(minRelevantId, primaryTxnId);
}
private boolean isRelevant(@Nullable TxnId minRelevantId, TxnId txnId)
{
return minRelevantId == null || minRelevantId.compareTo(txnId) <= 0;
}
boolean isMaybeRelevant(TxnId txnId)
{
return isRelevant(minDecidedId, txnId);
return isMaybeRelevant(txnIdInterval.txnId, null, Ranges.of(txnIdInterval));
}
public void forEachInCache(Unseekables<?> keysOrRanges, Consumer<Summary> forEach, AccordCommandStore.Caches caches)
@ -484,10 +398,9 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
for (RoutingKey key : (AbstractUnseekableKeys)keysOrRanges)
{
IntervalBTree.accumulate(manager.cachedRangeTxnsByRange(), KEY_COMPARATORS, key, (f, s, i, c) -> {
TxnIdInterval interval = (TxnIdInterval)i;
if (isRelevant(interval))
if (isMaybeRelevant(i))
{
TxnId txnId = ((TxnIdInterval)i).txnId;
TxnId txnId = i.txnId;
Summary summary = ifRelevant(c.getUnsafe(txnId));
if (summary != null)
f.accept(summary);
@ -502,7 +415,7 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
for (Range range : (AbstractRanges)keysOrRanges)
{
IntervalBTree.accumulate(manager.cachedRangeTxnsByRange(), COMPARATORS, new TxnIdInterval(range.start(), range.end(), TxnId.NONE), (f, s, i, c) -> {
if (isRelevant(i))
if (isMaybeRelevant(i))
{
TxnId txnId = i.txnId;
AccordCacheEntry<TxnId, Command> entry = c.getUnsafe(txnId);
@ -540,15 +453,7 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
return ifRelevant(cmd);
}
Ranges ranges = manager.transitive.get().get(txnId);
if (ranges == null)
return null;
ranges = ranges.intersecting(searchKeysOrRanges);
if (ranges.isEmpty())
return null;
return new Summary(txnId, txnId, NOT_DIRECTLY_WITNESSED, ranges, null, null);
return null;
}
public Summary ifRelevant(AccordCacheEntry<TxnId, Command> state)
@ -581,15 +486,10 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
return ifRelevant(command);
}
public Summary ifRelevant(Command cmd)
{
return ifRelevant(cmd.txnId(), cmd.executeAt(), cmd.saveStatus(), cmd.participants(), cmd.partialDeps());
}
public Summary ifRelevant(Command.Minimal cmd)
{
Invariants.require(findAsDep == null);
return ifRelevant(cmd.txnId, cmd.executeAt, cmd.saveStatus, cmd.participants, null);
return ifRelevant(cmd.txnId, cmd.executeAt, cmd.saveStatus, cmd.durability, cmd.participants, null);
}
}
}

View File

@ -253,7 +253,8 @@ public class AccordAgent implements Agent
long mostRecentStart = Math.max(command.txnId().hlc(), promisedHlc);
long waitMicros = recover(txnId).computeWait(retryCount, MICROSECONDS);
long nowMicros = MILLISECONDS.toMicros(Clock.Global.currentTimeMillis());
Invariants.expect(mostRecentStart <= nowMicros + SECONDS.toMicros(1L), "max(%s,%s)>%d", command.txnId(), command.promised(), nowMicros);
if (mostRecentStart > nowMicros + SECONDS.toMicros(1L))
logger.warn("max({},{})>{}", command.txnId(), command.promised(), nowMicros);
long startTime = mostRecentStart + waitMicros;
if (startTime < nowMicros)
startTime = nowMicros + waitMicros/2;

View File

@ -23,9 +23,12 @@ import com.google.common.base.Throwables;
import accord.api.RoutingKey;
import accord.local.CommandStores;
import accord.local.LoadKeys;
import accord.local.Node;
import accord.local.PreLoadContext;
import accord.local.cfk.CommandsForKey;
import accord.primitives.Ranges;
import accord.primitives.Routable;
import accord.primitives.Txn;
import accord.primitives.TxnId;
import accord.utils.async.AsyncChains;
import org.apache.cassandra.distributed.Cluster;
@ -129,7 +132,8 @@ public class AccordDropTableBase extends TestBaseImpl
inst.runOnInstance(() -> {
TableId tableId = TableId.fromString(s);
AccordService accord = (AccordService) AccordService.instance();
PreLoadContext ctx = PreLoadContext.contextFor(Ranges.single(TokenRange.fullRange(tableId, getPartitioner())), LoadKeys.SYNC, READ_WRITE, "Test");
TxnId syntheticTxnId = new TxnId(TxnId.MAX_EPOCH, 0, Txn.Kind.ExclusiveSyncPoint, Routable.Domain.Range, new Node.Id(1));
PreLoadContext ctx = PreLoadContext.contextFor(syntheticTxnId, Ranges.single(TokenRange.fullRange(tableId, getPartitioner())), LoadKeys.SYNC, READ_WRITE, "Test");
CommandStores stores = accord.node().commandStores();
for (int storeId : stores.ids())
{

View File

@ -656,7 +656,6 @@ public class CommandsForKeySerializerTest
@Override public AsyncChain<Void> build(PreLoadContext context, Consumer<? super SafeCommandStore> consumer) { return null; }
@Override public <T> AsyncChain<T> build(PreLoadContext context, Function<? super SafeCommandStore, T> apply) { throw new UnsupportedOperationException(); }
@Override public void shutdown() { }
@Override protected void registerTransitive(SafeCommandStore safeStore, RangeDeps deps){ }
@Override public <T> AsyncChain<T> build(Callable<T> task) { throw new UnsupportedOperationException(); }
@Override public void onInconsistentTimestamp(Command command, Timestamp prev, Timestamp next) { throw new UnsupportedOperationException(); }
@Override public void onFailedBootstrap(int attempts, String phase, Ranges ranges, Runnable retry, Throwable failure) { throw new UnsupportedOperationException(); }