You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by be...@apache.org on 2024/08/08 11:17:46 UTC
(cassandra-accord) branch trunk updated: CommandsForKey Improvements incl Pruning
This is an automated email from the ASF dual-hosted git repository.
benedict pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-accord.git
The following commit(s) were added to refs/heads/trunk by this push:
new 2297b984 CommandsForKey Improvements incl Pruning
2297b984 is described below
commit 2297b9846c1687a8b212638172f9dfee98bc024c
Author: Benedict Elliott Smith <be...@apache.org>
AuthorDate: Thu Jun 6 22:04:37 2024 +0100
CommandsForKey Improvements incl Pruning
CommandsForKey periodically self-prunes, so as to continue functioning well in-between garbage collections. Once we prune we are left with potentially incomplete information, and have to sometimes load per-command information from disk. But the payoff is ensuring CommandsForKey objects - which drive the majority of the state machine - are kept to a reasonable size.
patch by Benedict; reviewed by Blake Eggleston and David Capwell
---
accord-core/src/main/java/accord/api/Agent.java | 16 +
accord-core/src/main/java/accord/api/Read.java | 1 -
accord-core/src/main/java/accord/api/Update.java | 1 -
.../accord/coordinate/CoordinationAdapter.java | 2 +-
.../src/main/java/accord/coordinate/Infer.java | 6 +-
.../src/main/java/accord/coordinate/Propose.java | 3 +
.../java/accord/impl/AbstractSafeCommandStore.java | 9 +-
.../java/accord/impl/InMemoryCommandStore.java | 35 +-
.../accord/impl/InMemorySafeCommandsForKey.java | 6 +-
.../src/main/java/accord/local/Command.java | 357 ++--
.../src/main/java/accord/local/CommandStore.java | 83 +-
.../src/main/java/accord/local/Commands.java | 40 +-
.../src/main/java/accord/local/CommandsForKey.java | 1726 --------------------
.../src/main/java/accord/local/PreLoadContext.java | 4 +
.../main/java/accord/local/RedundantBefore.java | 6 +-
.../src/main/java/accord/local/SafeCommand.java | 6 +-
.../main/java/accord/local/SafeCommandStore.java | 104 +-
.../main/java/accord/local/SafeCommandsForKey.java | 99 --
accord-core/src/main/java/accord/local/Status.java | 59 +-
.../main/java/accord/local/cfk/CommandsForKey.java | 1471 +++++++++++++++++
.../accord/local/cfk/CommandsForKeyUpdate.java | 72 +
.../src/main/java/accord/local/cfk/NotifySink.java | 113 ++
.../main/java/accord/local/cfk/PostProcess.java | 245 +++
.../src/main/java/accord/local/cfk/Pruning.java | 332 ++++
.../java/accord/local/cfk/SafeCommandsForKey.java | 112 ++
.../src/main/java/accord/local/cfk/Updating.java | 898 ++++++++++
.../src/main/java/accord/local/cfk/Utils.java | 393 +++++
.../main/java/accord/messages/BeginRecovery.java | 9 +-
.../src/main/java/accord/messages/CheckStatus.java | 9 +-
.../src/main/java/accord/messages/Commit.java | 1 +
.../src/main/java/accord/messages/Propagate.java | 2 +-
.../src/main/java/accord/messages/ReadData.java | 32 +-
.../java/accord/messages/ReadEphemeralTxnData.java | 2 +-
.../main/java/accord/messages/WaitOnCommit.java | 2 +-
.../main/java/accord/primitives/AbstractKeys.java | 16 +
.../src/main/java/accord/primitives/Ballot.java | 6 +
.../src/main/java/accord/primitives/Deps.java | 222 +--
.../src/main/java/accord/primitives/KeyDeps.java | 5 +
.../main/java/accord/primitives/LatestDeps.java | 14 +-
.../main/java/accord/primitives/PartialDeps.java | 33 +-
.../main/java/accord/primitives/PartialTxn.java | 15 -
.../src/main/java/accord/primitives/RangeDeps.java | 38 +-
.../src/main/java/accord/primitives/SyncPoint.java | 4 +
.../src/main/java/accord/utils/ArrayBuffers.java | 141 +-
.../src/main/java/accord/utils/Invariants.java | 49 +-
.../utils/MergeFewDisjointSortedListsCursor.java | 156 ++
.../main/java/accord/utils/RelationMultiMap.java | 4 +-
.../src/main/java/accord/utils/SortedArrays.java | 83 +-
.../{api/Read.java => utils/SortedCursor.java} | 25 +-
.../src/main/java/accord/utils/btree/BTree.java | 17 +
accord-core/src/test/java/accord/impl/IntKey.java | 6 +
.../test/java/accord/impl/PrefixedIntHashKey.java | 4 +
.../src/test/java/accord/impl/TestAgent.java | 12 +
.../accord/impl/basic/DelayedCommandStores.java | 9 +-
.../src/test/java/accord/impl/basic/Journal.java | 12 +-
.../src/test/java/accord/impl/list/ListAgent.java | 12 +
.../src/test/java/accord/local/WaitingOnTest.java | 136 ++
.../java/accord/local/cfk/CommandsForKeyTest.java | 1093 +++++++++++++
.../test/java/accord/messages/PreAcceptTest.java | 10 +-
.../src/test/java/accord/primitives/DepsTest.java | 119 ++
.../test/java/accord/primitives/KeyDepsTest.java | 33 +
.../src/test/java/accord/utils/AccordGens.java | 120 +-
accord-core/src/test/java/accord/utils/Gen.java | 16 +
accord-core/src/test/java/accord/utils/Gens.java | 5 +
.../test/java/accord/utils/ReflectionUtils.java | 15 +
.../src/main/java/accord/maelstrom/Json.java | 31 +-
.../main/java/accord/maelstrom/MaelstromAgent.java | 12 +
.../src/test/java/accord/maelstrom/JsonTest.java | 2 +-
68 files changed, 6400 insertions(+), 2331 deletions(-)
diff --git a/accord-core/src/main/java/accord/api/Agent.java b/accord-core/src/main/java/accord/api/Agent.java
index 0bb21cd0..8e26d891 100644
--- a/accord-core/src/main/java/accord/api/Agent.java
+++ b/accord-core/src/main/java/accord/api/Agent.java
@@ -74,6 +74,22 @@ public interface Agent extends UncaughtExceptionListener
*/
long preAcceptTimeout();
+ /**
+ * Controls pruning of CommandsForKey
+ *
+ * The timestamp delta between the prune point and any pruned TxnId. This works primarily to minimise the
+ * chance of encountering a TxnId that precedes prunedBefore.
+ */
+ long cfkHlcPruneDelta();
+
+ /**
+ * Controls pruning of CommandsForKey.
+ *
+ * The number of entries before the candidate prune point that we require before we try to prune.
+ * This only works to reduce the time wasted pruning when there is limited benefit.
+ */
+ int cfkPruneInterval();
+
Txn emptyTxn(Txn.Kind kind, Seekables<?, ?> keysOrRanges);
default EventsListener metricsEventsListener()
diff --git a/accord-core/src/main/java/accord/api/Read.java b/accord-core/src/main/java/accord/api/Read.java
index 3b77ed3c..fe0d9403 100644
--- a/accord-core/src/main/java/accord/api/Read.java
+++ b/accord-core/src/main/java/accord/api/Read.java
@@ -34,5 +34,4 @@ public interface Read
AsyncChain<Data> read(Seekable key, SafeCommandStore commandStore, Timestamp executeAt, DataStore store);
Read slice(Ranges ranges);
Read merge(Read other);
- default boolean isEqualOrFuller(Read other) { return true; }
}
diff --git a/accord-core/src/main/java/accord/api/Update.java b/accord-core/src/main/java/accord/api/Update.java
index fa8493e5..8a56efcc 100644
--- a/accord-core/src/main/java/accord/api/Update.java
+++ b/accord-core/src/main/java/accord/api/Update.java
@@ -36,5 +36,4 @@ public interface Update
Write apply(Timestamp executeAt, @Nullable Data data);
Update slice(Ranges ranges);
Update merge(Update other);
- default boolean isEqualOrFuller(Update other) { return true; }
}
diff --git a/accord-core/src/main/java/accord/coordinate/CoordinationAdapter.java b/accord-core/src/main/java/accord/coordinate/CoordinationAdapter.java
index 89068df7..0aced3c6 100644
--- a/accord-core/src/main/java/accord/coordinate/CoordinationAdapter.java
+++ b/accord-core/src/main/java/accord/coordinate/CoordinationAdapter.java
@@ -101,7 +101,7 @@ public interface CoordinationAdapter<R>
else if (any.currentEpoch() >= executeAt.epoch() && any.oldestEpoch() <= txnId.epoch()) all = any.forEpochs(txnId.epoch(), executeAt.epoch());
else all = node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
- adapter.stabilise(node, any, all, route, ballot, txnId, txn, executeAt, deps, callback);
+ adapter.stabilise(node, coordinates, all, route, ballot, txnId, txn, executeAt, deps, callback);
}
public static <R> void execute(CoordinationAdapter<R> adapter, Node node, Topologies any, FullRoute<?> route, ExecutePath path, TxnId txnId, Txn txn, Timestamp executeAt, Deps deps, BiConsumer<? super R, Throwable> callback)
diff --git a/accord-core/src/main/java/accord/coordinate/Infer.java b/accord-core/src/main/java/accord/coordinate/Infer.java
index e38d1502..eecae95a 100644
--- a/accord-core/src/main/java/accord/coordinate/Infer.java
+++ b/accord-core/src/main/java/accord/coordinate/Infer.java
@@ -142,10 +142,10 @@ public class Infer
return a.compareTo(b) <= 0 ? a : b;
}
- public boolean inferInvalidWithQuorum(IsPreempted isPreempted, Known known)
+ public boolean inferInvalidWithQuorum(IsPreempted isPreempted, Known minKnown)
{
- return inferInvalidWithQuorum(undecided, isPreempted, !known.isDecided())
- || inferInvalidWithQuorum(unknown, isPreempted, !known.hasDefinitionBeenKnown());
+ return inferInvalidWithQuorum(undecided, isPreempted, !minKnown.isDecided())
+ || inferInvalidWithQuorum(unknown, isPreempted, !minKnown.hasDefinitionBeenKnown());
}
private static boolean inferInvalidWithQuorum(InvalidIf invalidIf, IsPreempted isPreempted, boolean hasCondition)
diff --git a/accord-core/src/main/java/accord/coordinate/Propose.java b/accord-core/src/main/java/accord/coordinate/Propose.java
index b72408ee..cb5783bc 100644
--- a/accord-core/src/main/java/accord/coordinate/Propose.java
+++ b/accord-core/src/main/java/accord/coordinate/Propose.java
@@ -43,6 +43,7 @@ import accord.primitives.Txn;
import accord.primitives.TxnId;
import accord.topology.Shard;
import accord.topology.Topologies;
+import accord.utils.Invariants;
import static accord.coordinate.tracking.AbstractTracker.ShardOutcomes.Fail;
import static accord.coordinate.tracking.RequestStatus.Failed;
@@ -77,6 +78,8 @@ abstract class Propose<R> implements Callback<AcceptReply>
this.callback = callback;
this.acceptOks = new ArrayList<>();
this.acceptTracker = new QuorumTracker(topologies);
+ Invariants.checkState(txnId.kind().isSyncPoint() || deps.keyDeps.txnIdCount() == 0 || deps.keyDeps.txnId(deps.keyDeps.txnIdCount() - 1).compareTo(executeAt) < 0,
+ "Attempted to propose a standard transaction with an earlier executeAt than a conflicting transaction it witnessed: %s vs executeAt: %s", deps, executeAt);
}
void start()
diff --git a/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java b/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java
index 4d8d2a64..f005e5b7 100644
--- a/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java
+++ b/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java
@@ -26,6 +26,7 @@ import com.google.common.annotations.VisibleForTesting;
import accord.api.Key;
import accord.api.VisibleForImplementation;
import accord.local.*;
+import accord.local.cfk.SafeCommandsForKey;
import accord.primitives.*;
import accord.utils.Invariants;
@@ -160,15 +161,7 @@ public abstract class AbstractSafeCommandStore<CommandType extends SafeCommand,
return context.isSubsetOf(this.context);
}
- protected abstract void invalidateSafeState();
-
public void postExecute()
{
}
-
- public void complete()
- {
- postExecute();
- invalidateSafeState();
- }
}
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
index 4932b181..ab1b3e2b 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
@@ -52,6 +52,8 @@ import accord.api.DataStore;
import accord.api.Key;
import accord.api.ProgressLog;
import accord.local.CommandStores.RangesForEpoch;
+import accord.local.cfk.CommandsForKey;
+import accord.local.cfk.SafeCommandsForKey;
import accord.primitives.AbstractKeys;
import accord.primitives.Deps;
import accord.primitives.PartialDeps;
@@ -254,7 +256,7 @@ public abstract class InMemoryCommandStore extends CommandStore
return timestampsForKey.get(key);
}
- private <O> O mapReduceForKey(InMemorySafeStore safeStore, Routables<?> keysOrRanges, Ranges slice, BiFunction<accord.local.CommandsForKey, O, O> map, O accumulate)
+ private <O> O mapReduceForKey(InMemorySafeStore safeStore, Routables<?> keysOrRanges, Ranges slice, BiFunction<CommandsForKey, O, O> map, O accumulate)
{
switch (keysOrRanges.domain()) {
default:
@@ -264,7 +266,7 @@ public abstract class InMemoryCommandStore extends CommandStore
for (Key key : keys)
{
if (!slice.contains(key)) continue;
- accord.local.CommandsForKey commands = safeStore.ifLoadedAndInitialised(key).current();
+ CommandsForKey commands = safeStore.ifLoadedAndInitialised(key).current();
if (commands == null)
continue;
@@ -281,7 +283,7 @@ public abstract class InMemoryCommandStore extends CommandStore
for (Map.Entry<RoutableKey, GlobalCommandsForKey> entry : commandsForKey.subMap(range.start(), range.startInclusive(), range.end(), range.endInclusive()).entrySet())
{
GlobalCommandsForKey globalCommands = entry.getValue();
- accord.local.CommandsForKey commands = globalCommands.value();
+ CommandsForKey commands = globalCommands.value();
if (commands == null)
continue;
accumulate = map.apply(commands, accumulate);
@@ -444,9 +446,10 @@ public abstract class InMemoryCommandStore extends CommandStore
{
if (store != current)
throw illegalState("This operation has already been cleared");
+
try
{
- current.complete();
+ current.postExecute();
}
catch (Throwable t)
{
@@ -515,9 +518,7 @@ public abstract class InMemoryCommandStore extends CommandStore
@Override
public String toString()
{
- return getClass().getSimpleName() + "{" +
- "id=" + id +
- '}';
+ return getClass().getSimpleName() + "{" + "id=" + id + ",node=" + time.id().id + '}';
}
static class RangeCommand
@@ -1003,7 +1004,7 @@ public abstract class InMemoryCommandStore extends CommandStore
}
@Override
- protected void invalidateSafeState()
+ public void postExecute()
{
commands.values().forEach(c -> {
if (c != null && c.current() != null)
@@ -1017,9 +1018,21 @@ public abstract class InMemoryCommandStore extends CommandStore
}
});
- commands.values().forEach(InMemorySafeCommand::invalidate);
- timestampsForKey.values().forEach(InMemorySafeTimestampsForKey::invalidate);
- commandsForKey.values().forEach(InMemorySafeCommandsForKey::invalidate);
+ commands.values().forEach(c -> {
+ if (c.isUnset())
+ commandStore.commands.remove(c.txnId());
+ c.invalidate();
+ });
+ timestampsForKey.values().forEach(tfk -> {
+ if (tfk.isUnset())
+ commandStore.timestampsForKey.remove(tfk.key());
+ tfk.invalidate();
+ });
+ commandsForKey.values().forEach(cfk -> {
+ if (cfk.isUnset())
+ commandStore.commandsForKey.remove(cfk.key());
+ cfk.invalidate();
+ });
}
}
diff --git a/accord-core/src/main/java/accord/impl/InMemorySafeCommandsForKey.java b/accord-core/src/main/java/accord/impl/InMemorySafeCommandsForKey.java
index ed758065..14957750 100644
--- a/accord-core/src/main/java/accord/impl/InMemorySafeCommandsForKey.java
+++ b/accord-core/src/main/java/accord/impl/InMemorySafeCommandsForKey.java
@@ -20,8 +20,8 @@ package accord.impl;
import accord.api.Key;
import accord.impl.InMemoryCommandStore.GlobalCommandsForKey;
-import accord.local.CommandsForKey;
-import accord.local.SafeCommandsForKey;
+import accord.local.cfk.CommandsForKey;
+import accord.local.cfk.SafeCommandsForKey;
public class InMemorySafeCommandsForKey extends SafeCommandsForKey
{
@@ -35,7 +35,7 @@ public class InMemorySafeCommandsForKey extends SafeCommandsForKey
}
@Override
- public accord.local.CommandsForKey current()
+ public CommandsForKey current()
{
return global.value();
}
diff --git a/accord-core/src/main/java/accord/local/Command.java b/accord-core/src/main/java/accord/local/Command.java
index 51bd9410..d082b676 100644
--- a/accord-core/src/main/java/accord/local/Command.java
+++ b/accord-core/src/main/java/accord/local/Command.java
@@ -29,25 +29,29 @@ import accord.api.RoutingKey;
import accord.api.VisibleForImplementation;
import accord.local.Status.Durability;
import accord.local.Status.Known;
+import accord.local.cfk.CommandsForKey;
import accord.primitives.Ballot;
import accord.primitives.Deps;
import accord.primitives.FullRoute;
+import accord.primitives.KeyDeps;
import accord.primitives.Keys;
import accord.primitives.PartialDeps;
import accord.primitives.PartialTxn;
import accord.primitives.RangeDeps;
+import accord.primitives.Ranges;
import accord.primitives.Route;
import accord.primitives.Seekables;
import accord.primitives.Timestamp;
import accord.primitives.Txn;
import accord.primitives.TxnId;
+import accord.primitives.Unseekables;
import accord.primitives.Writes;
import accord.utils.ImmutableBitSet;
import accord.utils.IndexedQuadConsumer;
import accord.utils.IndexedTriConsumer;
import accord.utils.Invariants;
import accord.utils.SimpleBitSet;
-import accord.utils.SortedArrays.SortedArrayList;
+
import javax.annotation.Nullable;
import static accord.local.Command.AbstractCommand.validate;
@@ -61,6 +65,8 @@ import static accord.local.Status.Invalidated;
import static accord.local.Status.KnownExecuteAt.ExecuteAtKnown;
import static accord.local.Status.Stable;
import static accord.primitives.Routable.Domain.Range;
+import static accord.primitives.Routables.Slice.Minimal;
+import static accord.utils.Invariants.illegalArgument;
import static accord.utils.Invariants.illegalState;
import static accord.utils.SortedArrays.forEachIntersection;
import static accord.utils.Utils.ensureImmutable;
@@ -236,13 +242,7 @@ public abstract class Command implements CommonAttributes
}
}
- /**
- * @return true if this command is equivalent to {@code other}; it could also be fuller - have some of its
- * registers be not sliced, whereas {@code other} may have them sliced
- */
- public abstract boolean isEqualOrFuller(Command other);
-
- abstract static class AbstractCommand extends Command
+ public abstract static class AbstractCommand extends Command
{
private final TxnId txnId;
private final SaveStatus status;
@@ -291,19 +291,6 @@ public abstract class Command implements CommonAttributes
&& Objects.equals(durableListeners(), command.durableListeners());
}
- @Override
- public boolean isEqualOrFuller(Command command)
- {
- if (this == command) return true;
- if (command == null || getClass() != command.getClass()) return false;
- return txnId().equals(command.txnId())
- && saveStatus() == command.saveStatus()
- && durability() == command.durability()
- && Objects.equals(route(), command.route())
- && Objects.equals(promised(), command.promised())
- && durableListeners().containsAll(command.durableListeners());
- }
-
@Override
public String toString()
{
@@ -387,7 +374,7 @@ public abstract class Command implements CommonAttributes
break;
case ExecuteAtProposed:
case ExecuteAtKnown:
- Invariants.checkState(executeAt != null && !executeAt.equals(Timestamp.NONE));
+ Invariants.checkState(executeAt != null && executeAt.compareTo(validate.txnId()) >= 0);
break;
case NoExecuteAt:
Invariants.checkState(executeAt.equals(Timestamp.NONE));
@@ -532,7 +519,7 @@ public abstract class Command implements CommonAttributes
private static void checkSameClass(Command command, Class<? extends Command> klass, String errorMsg)
{
if (!isSameClass(command, klass))
- throw new IllegalArgumentException(errorMsg + format(" expected %s got %s", klass.getSimpleName(), command.getClass().getSimpleName()));
+ throw illegalArgument(errorMsg + format(" expected %s got %s", klass.getSimpleName(), command.getClass().getSimpleName()));
}
public abstract Timestamp executeAt();
@@ -785,18 +772,6 @@ public abstract class Command implements CommonAttributes
&& Objects.equals(result, that.result);
}
- @Override
- public boolean isEqualOrFuller(Command c)
- {
- if (this == c) return true;
- if (c == null || getClass() != c.getClass()) return false;
- if (!super.isEqualOrFuller(c)) return false;
- Truncated that = (Truncated) c;
- return Objects.equals(executeAt(), that.executeAt())
- && Objects.equals(writes(), that.writes())
- && Objects.equals(result(), that.result());
- }
-
public static Truncated erased(Command command)
{
Durability durability = Durability.mergeAtLeast(command.durability(), UniversalOrInvalidated);
@@ -959,14 +934,6 @@ public abstract class Command implements CommonAttributes
if (!super.equals(o)) return false;
return Objects.equals(executesAtLeast, ((TruncatedAwaitsOnlyDeps)o).executesAtLeast);
}
-
- @Override
- public boolean isEqualOrFuller(Command command)
- {
- if (!super.isEqualOrFuller(command)) return false;
- return Objects.equals(executesAtLeast, ((TruncatedAwaitsOnlyDeps)command).executesAtLeast);
- }
-
}
public static class PreAccepted extends AbstractCommand
@@ -1006,19 +973,6 @@ public abstract class Command implements CommonAttributes
&& Objects.equals(partialDeps, that.partialDeps);
}
- @Override
- public boolean isEqualOrFuller(Command c)
- {
- if (this == c) return true;
- if (c == null || getClass() != c.getClass()) return false;
- if (!super.isEqualOrFuller(c)) return false;
- PreAccepted that = (PreAccepted) c;
- if (!executeAt().equals(that.executeAt()) || !partialTxn().isEqualOrFuller(that.partialTxn()))
- return false;
- return (partialDeps() == null && that.partialDeps() == null)
- || (partialDeps() != null && that.partialDeps() != null && partialDeps().isEqualOrFuller(that.partialDeps()));
- }
-
public static PreAccepted preAccepted(CommonAttributes common, Timestamp executeAt, Ballot promised)
{
return validate(new PreAccepted(common, SaveStatus.PreAccepted, promised, executeAt));
@@ -1097,16 +1051,6 @@ public abstract class Command implements CommonAttributes
return Objects.equals(acceptedOrCommitted, that.acceptedOrCommitted);
}
- @Override
- public boolean isEqualOrFuller(Command c)
- {
- if (this == c) return true;
- if (c == null || getClass() != c.getClass()) return false;
- if (!super.isEqualOrFuller(c)) return false;
- Accepted that = (Accepted) c;
- return Objects.equals(acceptedOrCommitted(), that.acceptedOrCommitted());
- }
-
public static Accepted accepted(CommonAttributes common, SaveStatus status, Timestamp executeAt, Ballot promised, Ballot accepted)
{
return validate(new Accepted(common, status, promised, executeAt, accepted));
@@ -1140,7 +1084,6 @@ public abstract class Command implements CommonAttributes
super(common, status, promised, executeAt, accepted);
this.waitingOn = waitingOn;
Invariants.checkState(common.route().kind().isFullRoute(), "Expected a full route but given %s", common.route().kind());
- if (status.hasBeen(Stable)) Invariants.checkState(waitingOn == WaitingOn.EMPTY || (waitingOn.txnIds.equals(common.partialDeps().rangeDeps.txnIds()) && waitingOn.keys.equals(common.partialDeps().keyDeps.keys())), "Deps do not match; expected (%s, %s) == (%s, %s)", waitingOn.keys, waitingOn.txnIds, common.partialDeps().keyDeps.keys(), common.partialDeps().rangeDeps.txnIds());
}
@Override
@@ -1167,16 +1110,6 @@ public abstract class Command implements CommonAttributes
return Objects.equals(waitingOn, committed.waitingOn);
}
- @Override
- public boolean isEqualOrFuller(Command c)
- {
- if (this == c) return true;
- if (c == null || getClass() != c.getClass()) return false;
- // TODO (required): revisit this method to make sure it is correct in context of usage
- // but checking fullness of WaitingOn doesn't make much sense
- return super.isEqualOrFuller(c);
- }
-
private static Committed committed(Committed command, CommonAttributes common, Ballot promised, SaveStatus status, WaitingOn waitingOn)
{
checkPromised(command, promised);
@@ -1194,7 +1127,7 @@ public abstract class Command implements CommonAttributes
return committed(command, common, command.promised(), status, command.waitingOn());
}
- static Committed committed(Committed command, CommonAttributes common, WaitingOn waitingOn)
+ public static Committed committed(Committed command, CommonAttributes common, WaitingOn waitingOn)
{
return committed(command, common, command.promised(), command.saveStatus(), waitingOn);
}
@@ -1245,17 +1178,6 @@ public abstract class Command implements CommonAttributes
&& Objects.equals(result, executed.result);
}
- @Override
- public boolean isEqualOrFuller(Command c)
- {
- if (this == c) return true;
- if (c == null || getClass() != c.getClass()) return false;
- if (!super.isEqualOrFuller(c)) return false;
- Executed executed = (Executed) c;
- return Objects.equals(writes(), executed.writes())
- && Objects.equals(result(), executed.result());
- }
-
public static Executed executed(Executed command, CommonAttributes common, SaveStatus status, Ballot promised, WaitingOn waitingOn)
{
checkSameClass(command, Executed.class, "Cannot update");
@@ -1295,27 +1217,61 @@ public abstract class Command implements CommonAttributes
public static class WaitingOn
{
- public static final WaitingOn EMPTY = new WaitingOn(Keys.EMPTY, RangeDeps.NONE.txnIds(), ImmutableBitSet.EMPTY, ImmutableBitSet.EMPTY);
+ public static final WaitingOn EMPTY = new WaitingOn(Keys.EMPTY, RangeDeps.NONE, KeyDeps.NONE, ImmutableBitSet.EMPTY, ImmutableBitSet.EMPTY);
public final Keys keys;
- public final SortedArrayList<TxnId> txnIds;
+ public final RangeDeps directRangeDeps;
+ public final KeyDeps directKeyDeps;
// waitingOn ONLY encodes both txnIds and keys
public final ImmutableBitSet waitingOn;
public final @Nullable ImmutableBitSet appliedOrInvalidated;
public WaitingOn(WaitingOn copy)
{
- this(copy.keys, copy.txnIds, copy.waitingOn, copy.appliedOrInvalidated);
+ this(copy.keys, copy.directRangeDeps, copy.directKeyDeps, copy.waitingOn, copy.appliedOrInvalidated);
}
- public WaitingOn(Keys keys, SortedArrayList<TxnId> txnIds, ImmutableBitSet waitingOn, ImmutableBitSet appliedOrInvalidated)
+ public WaitingOn(Keys keys, RangeDeps directRangeDeps, KeyDeps directKeyDeps, ImmutableBitSet waitingOn, ImmutableBitSet appliedOrInvalidated)
{
this.keys = keys;
- this.txnIds = txnIds;
+ this.directRangeDeps = directRangeDeps;
+ this.directKeyDeps = directKeyDeps;
this.waitingOn = waitingOn;
this.appliedOrInvalidated = appliedOrInvalidated;
}
+ public int txnIdCount()
+ {
+ return directRangeDeps.txnIdCount() + directKeyDeps.txnIdCount();
+ }
+
+ TxnId txnId(int i)
+ {
+ int ki = i - directRangeDeps.txnIdCount();
+ if (ki < 0)
+ return directRangeDeps.txnId(i);
+
+ if (ki < directKeyDeps.txnIdCount())
+ return directKeyDeps.txnId(ki);
+
+ throw new IndexOutOfBoundsException(i + " >= " + txnIdCount());
+ }
+
+ int indexOf(TxnId txnId)
+ {
+ if (txnId.domain() == Range)
+ return directRangeDeps.indexOf(txnId);
+
+ if (!CommandsForKey.managesExecution(txnId))
+ {
+ int i = directKeyDeps.indexOf(txnId);
+ int offset = directRangeDeps.txnIdCount();
+ return i < 0 ? i - offset : i + offset;
+ }
+
+ throw new IllegalArgumentException("WaitingOn does not track this kind of TxnId: " + txnId);
+ }
+
public Timestamp executeAtLeast()
{
return null;
@@ -1328,8 +1284,8 @@ public abstract class Command implements CommonAttributes
public static WaitingOn none(Deps deps)
{
- ImmutableBitSet empty = new ImmutableBitSet(deps.txnIdCount() + deps.keyDeps.keys().size());
- return new WaitingOn(deps.keyDeps.keys(), deps.rangeDeps.txnIds(), empty, empty);
+ ImmutableBitSet empty = new ImmutableBitSet(deps.rangeDeps.txnIdCount() + deps.directKeyDeps.txnIdCount() + deps.keyDeps.keys().size());
+ return new WaitingOn(deps.keyDeps.keys(), deps.rangeDeps, deps.directKeyDeps, empty, empty);
}
public boolean isWaiting()
@@ -1339,54 +1295,63 @@ public abstract class Command implements CommonAttributes
public boolean isWaitingOnKey()
{
- return waitingOn.lastSetBit() >= txnIds.size();
+ return waitingOn.lastSetBit() >= txnIdCount();
}
public boolean isWaitingOnKey(int keyIndex)
{
- return waitingOn.get(txnIds.size() + keyIndex);
+ Invariants.checkIndex(keyIndex, keys.size());
+ return waitingOn.get(txnIdCount() + keyIndex);
}
public boolean isWaitingOnCommand()
{
- return waitingOn.firstSetBit() < txnIds.size();
+ return waitingOn.firstSetBit() < txnIdCount();
}
public boolean isWaitingOn(TxnId txnId)
{
- int index = txnIds.indexOf(txnId);
+ int index = indexOf(txnId);
return index >= 0 && waitingOn.get(index);
}
public TxnId nextWaitingOn()
{
- int i = waitingOn.prevSetBit(txnIds.size());
- return i < 0 ? null : txnIds.get(i);
+ int i = nextWaitingOnIndex();
+ return i < 0 ? null : txnId(i);
}
private int nextWaitingOnIndex()
{
- return waitingOn.prevSetBit(txnIds.size());
+ int directRangeTxnIdCount = directRangeDeps.txnIdCount();
+ int nextWaitingOnDirectRangeIndex = waitingOn.prevSetBit(directRangeTxnIdCount);
+ if (directKeyDeps == KeyDeps.NONE)
+ return nextWaitingOnDirectRangeIndex;
+
+ int directKeyTxnIdCount = directKeyDeps.txnIdCount();
+ int txnIdCount = directKeyTxnIdCount + directRangeTxnIdCount;
+ int nextWaitingOnDirectKeyIndex = waitingOn.prevSetBitNotBefore(txnIdCount, directRangeTxnIdCount);
+ if (nextWaitingOnDirectKeyIndex < 0)
+ return nextWaitingOnDirectRangeIndex;
+ if (nextWaitingOnDirectRangeIndex < 0)
+ return nextWaitingOnDirectKeyIndex;
+ int c = directRangeDeps.txnId(nextWaitingOnDirectRangeIndex).compareTo(directKeyDeps.txnId(nextWaitingOnDirectKeyIndex - directRangeDeps.txnIdCount()));
+ return c > 0 ? nextWaitingOnDirectRangeIndex : nextWaitingOnDirectKeyIndex;
}
- private static String toString(Keys keys, SortedArrayList<TxnId> txnIds, SimpleBitSet waitingOn)
+ @Override
+ public String toString()
{
List<TxnId> waitingOnTxnIds = new ArrayList<>();
List<Key> waitingOnKeys = new ArrayList<>();
- waitingOn.reverseForEach(waitingOnTxnIds, waitingOnKeys, txnIds, keys, (outIds, outKeys, ids, ks, i) -> {
- if (i < ids.size()) outIds.add(ids.get(i));
- else outKeys.add(ks.get(i - ids.size()));
+ waitingOn.reverseForEach(waitingOnTxnIds, waitingOnKeys, this, keys, (outIds, outKeys, self, ks, i) -> {
+ if (i < self.txnIdCount()) outIds.add(self.txnId(i));
+ else outKeys.add(ks.get(i - self.txnIdCount()));
});
Collections.reverse(waitingOnKeys);
return "keys=" + waitingOnKeys + ", txnIds=" + waitingOnTxnIds;
}
- @Override
- public String toString()
- {
- return toString(keys, txnIds, waitingOn);
- }
-
@Override
public boolean equals(Object other)
{
@@ -1396,7 +1361,8 @@ public abstract class Command implements CommonAttributes
boolean equals(WaitingOn other)
{
return this.keys.equals(other.keys)
- && txnIds.equals(other.txnIds)
+ && directKeyDeps.equals(other.directKeyDeps)
+ && directRangeDeps.equals(other.directRangeDeps)
&& this.waitingOn.equals(other.waitingOn)
&& Objects.equals(this.appliedOrInvalidated, other.appliedOrInvalidated);
}
@@ -1404,7 +1370,8 @@ public abstract class Command implements CommonAttributes
public static class Update
{
final Keys keys;
- final SortedArrayList<TxnId> txnIds;
+ final RangeDeps directRangeDeps;
+ final KeyDeps directKeyDeps;
private SimpleBitSet waitingOn;
private @Nullable SimpleBitSet appliedOrInvalidated;
private Timestamp executeAtLeast;
@@ -1412,7 +1379,8 @@ public abstract class Command implements CommonAttributes
public Update(WaitingOn waitingOn)
{
this.keys = waitingOn.keys;
- this.txnIds = waitingOn.txnIds;
+ this.directRangeDeps = waitingOn.directRangeDeps;
+ this.directKeyDeps = waitingOn.directKeyDeps;
this.waitingOn = waitingOn.waitingOn;
this.appliedOrInvalidated = waitingOn.appliedOrInvalidated;
if (waitingOn.getClass() == WaitingOnWithExecuteAt.class)
@@ -1424,17 +1392,28 @@ public abstract class Command implements CommonAttributes
this(committed.waitingOn);
}
- public Update(TxnId txnId, Deps deps)
+ private Update(TxnId txnId, Keys keys, RangeDeps directRangeDeps, KeyDeps directKeyDeps)
{
- this(txnId, deps.keyDeps.keys(), deps.rangeDeps.txnIds());
+ this.keys = keys;
+ this.directRangeDeps = directRangeDeps;
+ this.directKeyDeps = directKeyDeps;
+ this.waitingOn = new SimpleBitSet(txnIdCount() + keys.size(), false);
+ this.appliedOrInvalidated = CommandsForKey.managesExecution(txnId) ? null : new SimpleBitSet(txnIdCount(), false);
}
- public Update(TxnId txnId, Keys keys, SortedArrayList<TxnId> txnIds)
+ public static Update initialise(TxnId txnId, Route route, Ranges ranges, Deps deps)
{
- this.keys = keys;
- this.txnIds = txnIds;
- this.waitingOn = new SimpleBitSet(txnIds.size() + keys.size(), false);
- this.appliedOrInvalidated = txnId.domain() == Range ? new SimpleBitSet(txnIds.size(), false) : null;
+ Unseekables<?> executionParticipants = route.participants().slice(ranges, Minimal);
+ Update update = new Update(txnId, deps.keyDeps.keys(), deps.rangeDeps, deps.directKeyDeps);
+ deps.rangeDeps.forEach(executionParticipants, update, Update::initialise);
+ deps.directKeyDeps.forEach(ranges, 0, deps.directKeyDeps.txnIdCount(), update, deps.rangeDeps, (upd, rdeps, index) -> upd.initialise(index + rdeps.txnIdCount()));
+ deps.keyDeps.keys().forEach(ranges, (upd, key, index) -> upd.initialise(index + upd.txnIdCount()), update);
+ return update;
+ }
+
+ private void initialise(int i)
+ {
+ waitingOn.set(i);
}
public boolean hasChanges()
@@ -1443,9 +1422,33 @@ public abstract class Command implements CommonAttributes
|| (appliedOrInvalidated != null && !(appliedOrInvalidated instanceof ImmutableBitSet));
}
+ public TxnId txnId(int i)
+ {
+ int ki = i - directRangeDeps.txnIdCount();
+ if (ki < 0)
+ return directRangeDeps.txnId(i);
+ if (ki < directKeyDeps.txnIdCount())
+ return directKeyDeps.txnId(ki);
+ throw new IndexOutOfBoundsException(i + " >= " + txnIdCount());
+ }
+
+ public int indexOf(TxnId txnId)
+ {
+ if (txnId.domain() == Range)
+ return directRangeDeps.indexOf(txnId);
+ if (!CommandsForKey.managesExecution(txnId))
+ return directRangeDeps.txnIdCount() + directKeyDeps.indexOf(txnId);
+ throw new IllegalArgumentException("WaitingOn does not track this kind of TxnId: " + txnId);
+ }
+
+ public int txnIdCount()
+ {
+ return directRangeDeps.txnIdCount() + directKeyDeps.txnIdCount();
+ }
+
public boolean removeWaitingOn(TxnId txnId)
{
- int index = txnIds.indexOf(txnId);
+ int index = indexOf(txnId);
if (!waitingOn.get(index))
return false;
@@ -1456,7 +1459,7 @@ public abstract class Command implements CommonAttributes
public boolean isWaitingOn(TxnId txnId)
{
- int index = txnIds.indexOf(txnId);
+ int index = indexOf(txnId);
return index >= 0 && waitingOn.get(index);
}
@@ -1473,7 +1476,7 @@ public abstract class Command implements CommonAttributes
// TODO (expected): simply remove ourselves from any CFK we aren't waiting on
if (index < 0) return false;
- index += txnIds.size();
+ index += txnIdCount();
if (!waitingOn.get(index))
return false;
@@ -1484,15 +1487,10 @@ public abstract class Command implements CommonAttributes
public boolean isWaitingOn(Key key)
{
- int index = keys.indexOf(key) + txnIds.size();
+ int index = keys.indexOf(key) + txnIdCount();
return index >= 0 && waitingOn.get(index);
}
- void initialiseWaiting(int index)
- {
- waitingOn.set(index);
- }
-
public boolean isWaiting()
{
return !waitingOn.isEmpty();
@@ -1500,13 +1498,38 @@ public abstract class Command implements CommonAttributes
public TxnId minWaitingOnTxnId()
{
- int index = waitingOn.firstSetBit();
- return index < 0 || index >= txnIds.size() ? null : txnIds.get(index);
+ int index = minWaitingOnTxnIdx();
+ return index < 0 ? null : txnId(index);
+ }
+
+ public int minWaitingOnTxnIdx()
+ {
+ int directRangeTxnIdCount = directRangeDeps.txnIdCount();
+ int minWaitingOnDirectRangeIndex = waitingOn.nextSetBitBefore(0, directRangeTxnIdCount);
+ if (directKeyDeps == KeyDeps.NONE)
+ return minWaitingOnDirectRangeIndex;
+
+ int directKeyTxnIdCount = directKeyDeps.txnIdCount();
+ int txnIdCount = directKeyTxnIdCount + directRangeTxnIdCount;
+ int minWaitingOnDirectKeyIndex = waitingOn.nextSetBitBefore(directRangeTxnIdCount, txnIdCount);
+ if (minWaitingOnDirectKeyIndex < 0)
+ return minWaitingOnDirectRangeIndex;
+ if (minWaitingOnDirectRangeIndex < 0)
+ return minWaitingOnDirectKeyIndex;
+ int c = directRangeDeps.txnId(minWaitingOnDirectRangeIndex).compareTo(directKeyDeps.txnId(minWaitingOnDirectKeyIndex - directRangeDeps.txnIdCount()));
+ return c < 0 ? minWaitingOnDirectRangeIndex : minWaitingOnDirectKeyIndex;
+ }
+
+ public boolean isWaitingOnDirectRangeTxnIdx(int idx)
+ {
+ Invariants.checkIndex(idx, directRangeDeps.txnIdCount());
+ return waitingOn.get(idx);
}
- public boolean isWaitingOn(int txnIdx)
+ public boolean isWaitingOnDirectKeyTxnIdx(int idx)
{
- return waitingOn.get(txnIdx);
+ Invariants.checkIndex(idx, directKeyDeps.txnIdCount());
+ return waitingOn.get(idx + directRangeDeps.txnIdCount());
}
public void updateExecuteAtLeast(Timestamp executeAtLeast)
@@ -1514,17 +1537,25 @@ public abstract class Command implements CommonAttributes
this.executeAtLeast = Timestamp.nonNullOrMax(executeAtLeast, this.executeAtLeast);
}
- boolean removeWaitingOnTxnId(int i)
+ boolean removeWaitingOnDirectRangeTxnId(int i)
{
+ Invariants.checkIndex(i, directRangeDeps.txnIdCount());
return removeWaitingOn(i);
}
+ boolean removeWaitingOnDirectKeyTxnId(int i)
+ {
+ Invariants.checkIndex(i, directKeyDeps.txnIdCount());
+ return removeWaitingOn(i + directRangeDeps.txnIdCount());
+ }
+
boolean removeWaitingOnKey(int i)
{
- return removeWaitingOn(txnIds.size() + i);
+ Invariants.checkIndex(i, keys.size());
+ return removeWaitingOn(txnIdCount() + i);
}
- boolean removeWaitingOn(int i)
+ private boolean removeWaitingOn(int i)
{
if (waitingOn.get(i))
{
@@ -1547,11 +1578,23 @@ public abstract class Command implements CommonAttributes
*/
public boolean setAppliedOrInvalidated(TxnId txnId)
{
- int index = txnIds.indexOf(txnId);
+ int index = indexOf(txnId);
return setAppliedOrInvalidated(index);
}
- public boolean setAppliedOrInvalidated(int i)
+ private boolean setAppliedOrInvalidatedDirectRangeTxn(int i)
+ {
+ Invariants.checkIndex(i, directRangeDeps.txnIdCount());
+ return setAppliedOrInvalidated(i);
+ }
+
+ private boolean setAppliedOrInvalidatedDirectKeyTxn(int i)
+ {
+ Invariants.checkIndex(i, directKeyDeps.txnIdCount());
+ return setAppliedOrInvalidated(i + directRangeDeps.txnIdCount());
+ }
+
+ private boolean setAppliedOrInvalidated(int i)
{
if (appliedOrInvalidated == null)
return removeWaitingOn(i);
@@ -1569,17 +1612,26 @@ public abstract class Command implements CommonAttributes
public boolean setAppliedAndPropagate(TxnId txnId, WaitingOn propagate)
{
- int index = txnIds.indexOf(txnId);
+ int index = indexOf(txnId);
if (!setAppliedOrInvalidated(index))
return false;
if (propagate.appliedOrInvalidated != null && !propagate.appliedOrInvalidated.isEmpty())
{
- forEachIntersection(propagate.txnIds, txnIds,
+ forEachIntersection(propagate.directRangeDeps.txnIds(), directRangeDeps.txnIds(),
(from, to, ignore, i1, i2) -> {
if (from.get(i1))
- to.setAppliedOrInvalidated(i2);
+ to.setAppliedOrInvalidatedDirectRangeTxn(i2);
}, propagate.appliedOrInvalidated, this, null);
+
+ if (propagate.directKeyDeps != KeyDeps.NONE)
+ {
+ forEachIntersection(propagate.directKeyDeps.txnIds(), directKeyDeps.txnIds(),
+ (from, to, ignore, i1, i2) -> {
+ if (from.get(i1))
+ to.setAppliedOrInvalidatedDirectKeyTxn(i2);
+ }, propagate.appliedOrInvalidated, this, null);
+ }
}
return true;
@@ -1587,17 +1639,17 @@ public abstract class Command implements CommonAttributes
public <P1, P2, P3, P4> void forEachWaitingOnId(P1 p1, P2 p2, P3 p3, P4 p4, IndexedQuadConsumer<P1, P2, P3, P4> forEach)
{
- waitingOn.reverseForEach(0, txnIds.size(), p1, p2, p3, p4, forEach);
+ waitingOn.reverseForEach(0, txnIdCount(), p1, p2, p3, p4, forEach);
}
public <P1, P2, P3, P4> void forEachWaitingOnKey(P1 p1, P2 p2, P3 p3, IndexedTriConsumer<P1, P2, P3> forEach)
{
- waitingOn.reverseForEach(txnIds.size(), keys.size(), p1, p2, p3, txnIds, (pp1, pp2, pp3, pp4, i) -> forEach.accept(pp1, pp2, pp3, i - pp4.size()));
+ waitingOn.reverseForEach(txnIdCount(), keys.size(), p1, p2, p3, this, (pp1, pp2, pp3, pp4, i) -> forEach.accept(pp1, pp2, pp3, i - pp4.txnIdCount()));
}
public WaitingOn build()
{
- WaitingOn result = new WaitingOn(keys, txnIds, ensureImmutable(waitingOn), ensureImmutable(appliedOrInvalidated));
+ WaitingOn result = new WaitingOn(keys, directRangeDeps, directKeyDeps, ensureImmutable(waitingOn), ensureImmutable(appliedOrInvalidated));
if (executeAtLeast == null)
return result;
return new WaitingOnWithExecuteAt(result, executeAtLeast);
@@ -1606,7 +1658,14 @@ public abstract class Command implements CommonAttributes
@Override
public String toString()
{
- return WaitingOn.toString(keys, txnIds, waitingOn);
+ List<TxnId> waitingOnTxnIds = new ArrayList<>();
+ List<Key> waitingOnKeys = new ArrayList<>();
+ waitingOn.reverseForEach(waitingOnTxnIds, waitingOnKeys, this, keys, (outIds, outKeys, self, ks, i) -> {
+ if (i < self.txnIdCount()) outIds.add(self.txnId(i));
+ else outKeys.add(ks.get(i - self.txnIdCount()));
+ });
+ Collections.reverse(waitingOnKeys);
+ return "keys=" + waitingOnKeys + ", txnIds=" + waitingOnTxnIds;
}
}
}
@@ -1640,7 +1699,7 @@ public abstract class Command implements CommonAttributes
boolean equals(WaitingOnWithExecuteAt other)
{
- return super.equals((WaitingOn) other) && executeAtLeast == other.executeAtLeast;
+ return super.equals(other) && executeAtLeast == other.executeAtLeast;
}
}
diff --git a/accord-core/src/main/java/accord/local/CommandStore.java b/accord-core/src/main/java/accord/local/CommandStore.java
index c7baf2f5..cdb8a467 100644
--- a/accord-core/src/main/java/accord/local/CommandStore.java
+++ b/accord-core/src/main/java/accord/local/CommandStore.java
@@ -28,7 +28,8 @@ import javax.annotation.Nullable;
import accord.api.Agent;
import accord.local.CommandStores.RangesForEpoch;
-import accord.primitives.Deps;
+import accord.primitives.KeyDeps;
+import accord.primitives.Keys;
import accord.primitives.Range;
import accord.primitives.Routables;
import accord.utils.async.AsyncChain;
@@ -64,6 +65,7 @@ import accord.primitives.Timestamp;
import accord.primitives.TxnId;
import accord.primitives.Unseekables;
import accord.utils.async.AsyncResults;
+import org.agrona.collections.Int2ObjectHashMap;
import static accord.api.ConfigurationService.EpochReady.DONE;
import static accord.local.KeyHistory.COMMANDS;
@@ -382,9 +384,7 @@ public abstract class CommandStore implements AgentExecutor
@Override
public String toString()
{
- return getClass().getSimpleName() + "{" +
- "id=" + id +
- '}';
+ return getClass().getSimpleName() + "{id=" + id + ", node=" + time.id().id + '}';
}
@Nullable
@@ -594,8 +594,68 @@ public abstract class CommandStore implements AgentExecutor
return null != rejectBefore.foldl(participants, (rejectIfBefore, test) -> rejectIfBefore.compareTo(test) > 0 ? null : test, txnId, Objects::isNull);
}
- public final void removeRedundantDependencies(Unseekables<?> participants, Deps deps, WaitingOn.Update builder)
+ public final void removeRedundantDependencies(Unseekables<?> participants, WaitingOn.Update builder)
{
+ // Note: we do not need to track the bootstraps we implicitly depend upon, because we will not serve any read requests until this has completed
+ // and since we are a timestamp store, and we write only this will sort itself out naturally
+ // TODO (required): make sure we have no races on HLC around SyncPoint else this resolution may not work (we need to know the micros equivalent timestamp of the snapshot)
+ class KeyState
+ {
+ Int2ObjectHashMap<Keys> partiallyBootstrapping;
+
+ /**
+ * Are the participating ranges for the txn fully covered by bootstrapping ranges for this command store
+ */
+ boolean isFullyBootstrapping(WaitingOn.Update builder, Range range, int txnIdx)
+ {
+ if (builder.directKeyDeps.foldEachKey(txnIdx, range, true, (r0, k, p) -> p && r0.contains(k)))
+ return true;
+
+ if (partiallyBootstrapping == null)
+ partiallyBootstrapping = new Int2ObjectHashMap<>();
+ Keys prev = partiallyBootstrapping.get(txnIdx);
+ Keys remaining = prev;
+ if (remaining == null) remaining = builder.directKeyDeps.participatingKeys(txnIdx);
+ else Invariants.checkState(!remaining.isEmpty());
+ remaining = remaining.subtract(range);
+ if (prev == null) Invariants.checkState(!remaining.isEmpty());
+ partiallyBootstrapping.put(txnIdx, remaining);
+ return remaining.isEmpty();
+ }
+ }
+
+ KeyDeps directKeyDeps = builder.directKeyDeps;
+ if (!directKeyDeps.isEmpty())
+ {
+ redundantBefore().foldl(directKeyDeps.keys(), (e, s, d, b) -> {
+ // TODO (desired, efficiency): foldlInt so we can track the lower rangeidx bound and not revisit unnecessarily
+ // find the txnIdx below which we are known to be fully redundant locally due to having been applied or invalidated
+ int bootstrapIdx = d.txnIds().find(e.bootstrappedAt);
+ if (bootstrapIdx < 0) bootstrapIdx = -1 - bootstrapIdx;
+ int appliedIdx = d.txnIds().find(e.locallyAppliedOrInvalidatedBefore);
+ if (appliedIdx < 0) appliedIdx = -1 - appliedIdx;
+
+ // remove intersecting transactions with known redundant txnId
+ // note that we must exclude all transactions that are pre-bootstrap, and perform the more complicated dance below,
+ // as these transactions may be only partially applied, and we may need to wait for them on another key.
+ if (appliedIdx > bootstrapIdx)
+ {
+ d.forEach(e.range, bootstrapIdx, appliedIdx, b, s, (b0, s0, txnIdx) -> {
+ b0.removeWaitingOnDirectKeyTxnId(txnIdx);
+ });
+ }
+
+ if (bootstrapIdx > 0)
+ {
+ d.forEach(e.range, 0, bootstrapIdx, b, s, e.range, (b0, s0, r, txnIdx) -> {
+ if (b0.isWaitingOnDirectKeyTxnIdx(txnIdx) && s0.isFullyBootstrapping(b0, r, txnIdx))
+ b0.removeWaitingOnDirectKeyTxnId(txnIdx);
+ });
+ }
+ return s;
+ }, new KeyState(), directKeyDeps, builder, ignore -> false);
+ }
+
/**
* If we have to handle bootstrapping ranges for range transactions, these may only partially cover the
* transaction, in which case we should not remove the transaction as a dependency. But if it is fully
@@ -613,14 +673,14 @@ public abstract class CommandStore implements AgentExecutor
boolean isFullyBootstrapping(int rangeTxnIdx)
{
// if all deps for the txnIdx are contained in the range, don't inflate any shared object state
- if (deps.rangeDeps.foldEachRange(rangeTxnIdx, range, true, (r1, r2, p) -> p && r1.contains(r2)))
+ if (builder.directRangeDeps.foldEachRange(rangeTxnIdx, range, true, (r1, r2, p) -> p && r1.contains(r2)))
return true;
if (partiallyBootstrapping == null)
partiallyBootstrapping = new HashMap<>();
Ranges prev = partiallyBootstrapping.get(rangeTxnIdx);
Ranges remaining = prev;
- if (remaining == null) remaining = deps.rangeDeps.ranges(rangeTxnIdx);
+ if (remaining == null) remaining = builder.directRangeDeps.ranges(rangeTxnIdx);
else Invariants.checkState(!remaining.isEmpty());
remaining = remaining.subtract(Ranges.of(range));
if (prev == null) Invariants.checkState(!remaining.isEmpty());
@@ -628,7 +688,8 @@ public abstract class CommandStore implements AgentExecutor
return remaining.isEmpty();
}
}
- RangeDeps rangeDeps = deps.rangeDeps;
+
+ RangeDeps rangeDeps = builder.directRangeDeps;
// TODO (required, consider): slice to only those ranges we own, maybe don't even construct rangeDeps.covering()
redundantBefore().foldl(participants, (e, s, d, b) -> {
int bootstrapIdx = d.txnIds().find(e.bootstrappedAt);
@@ -646,7 +707,7 @@ public abstract class CommandStore implements AgentExecutor
// TODO (desired): move the bounds check into forEach, matching structure used for keys
d.forEach(e.range, b, s, (b0, s0, txnIdx) -> {
if (txnIdx >= s0.bootstrapIdx && txnIdx < s0.appliedIdx)
- b0.removeWaitingOn(txnIdx);
+ b0.removeWaitingOnDirectRangeTxnId(txnIdx);
});
}
@@ -657,8 +718,8 @@ public abstract class CommandStore implements AgentExecutor
s.range = e.range;
// TODO (desired): move the bounds check into forEach, matching structure used for keys
d.forEach(e.range, b, s, (b0, s0, txnIdx) -> {
- if (txnIdx < s0.bootstrapIdx && b0.isWaitingOn(txnIdx) && s0.isFullyBootstrapping(txnIdx))
- b0.removeWaitingOn(txnIdx);
+ if (txnIdx < s0.bootstrapIdx && b0.isWaitingOnDirectRangeTxnIdx(txnIdx) && s0.isFullyBootstrapping(txnIdx))
+ b0.removeWaitingOnDirectRangeTxnId(txnIdx);
});
}
return s;
diff --git a/accord-core/src/main/java/accord/local/Commands.java b/accord-core/src/main/java/accord/local/Commands.java
index 42dcb371..ecb0d6da 100644
--- a/accord-core/src/main/java/accord/local/Commands.java
+++ b/accord-core/src/main/java/accord/local/Commands.java
@@ -342,7 +342,6 @@ public class Commands
if (!validate(command.status(), command, coordinateRanges, acceptRanges, shard, route, Add, partialTxn, Add, partialDeps, Set))
return CommitOutcome.Insufficient;
- // FIXME: split up set
CommonAttributes attrs = set(command, coordinateRanges, acceptRanges, shard, route, partialTxn, Add, partialDeps, Set);
logger.trace("{}: committed with executeAt: {}, deps: {}", txnId, executeAt, partialDeps);
@@ -648,12 +647,6 @@ public class Commands
}
private static boolean maybeExecute(SafeCommandStore safeStore, SafeCommand safeCommand, boolean alwaysNotifyListeners, boolean notifyWaitingOn)
- {
- return maybeExecute(safeStore, safeCommand, alwaysNotifyListeners, notifyWaitingOn, false);
- }
-
- // TODO (expected, API consistency): maybe split into maybeExecute and maybeApply?
- private static boolean maybeExecute(SafeCommandStore safeStore, SafeCommand safeCommand, boolean alwaysNotifyListeners, boolean notifyWaitingOn, boolean executeAsync)
{
Command command = safeCommand.current();
if (logger.isTraceEnabled())
@@ -677,15 +670,6 @@ public class Commands
return false;
}
- if (executeAsync)
- {
- TxnId txnId = command.txnId();
- safeStore.commandStore().execute(contextFor(command.txnId(), command.keysOrRanges(), KeyHistory.COMMANDS), safeStore0 -> {
- maybeExecute(safeStore0, safeStore0.get(txnId), alwaysNotifyListeners, notifyWaitingOn, false);
- }).begin(safeStore.agent());
- return false;
- }
-
// TODO (required): slice our execute ranges based on any pre-bootstrap state
// FIXME: need to communicate to caller that we didn't execute if we take one of the above paths
@@ -706,6 +690,7 @@ public class Commands
if (intersects)
{
+ // TODO (now): we should set applying within apply to avoid applying multiple times
safeCommand.applying(safeStore);
safeStore.notifyListeners(safeCommand);
logger.trace("{}: applying", command.txnId());
@@ -739,16 +724,8 @@ public class Commands
Ranges ranges = safeStore.ranges().allAt(waitingExecuteAt);
PartialDeps deps = waiting.partialDeps();
- WaitingOn.Update update = new WaitingOn.Update(waitingId, deps);
- // we select range deps on actual participants rather than covered ranges,
- // since we may otherwise adopt false dependencies for range txns
-
- Unseekables<?> executionParticipants = route.participants().slice(ranges, Minimal);
- deps.rangeDeps.forEach(executionParticipants, update, WaitingOn.Update::initialiseWaiting);
- deps.keyDeps.keys().foldl(ranges, (u, rd, k, v, i) -> {
- u.initialiseWaiting(rd.txnIdCount() + i);
- return null;
- }, update, deps.rangeDeps, null);
+
+ WaitingOn.Update update = WaitingOn.Update.initialise(waitingId, route, ranges, deps);
return updateWaitingOn(safeStore, waiting, waitingExecuteAt, update, route.participants()).build();
}
@@ -757,10 +734,10 @@ public class Commands
CommandStore commandStore = safeStore.commandStore();
TxnId minWaitingOnTxnId = update.minWaitingOnTxnId();
if (minWaitingOnTxnId != null && commandStore.hasLocallyRedundantDependencies(update.minWaitingOnTxnId(), executeAt, participants))
- safeStore.commandStore().removeRedundantDependencies(participants, waiting.partialDeps(), update);
+ safeStore.commandStore().removeRedundantDependencies(participants, update);
update.forEachWaitingOnId(safeStore, update, waiting, executeAt, (store, upd, w, exec, i) -> {
- SafeCommand dep = store.ifLoadedAndInitialised(upd.txnIds.get(i));
+ SafeCommand dep = store.ifLoadedAndInitialised(upd.txnId(i));
if (dep == null || !dep.current().hasBeen(PreCommitted))
return;
updateWaitingOn(store, w, exec, upd, dep);
@@ -856,7 +833,7 @@ public class Commands
}
}
- static void removeWaitingOnKeyAndMaybeExecute(SafeCommandStore safeStore, SafeCommand safeCommand, Key key, boolean executeAsync)
+ public static void removeWaitingOnKeyAndMaybeExecute(SafeCommandStore safeStore, SafeCommand safeCommand, Key key)
{
if (safeCommand.current().hasBeen(Applied))
return;
@@ -872,7 +849,7 @@ public class Commands
waitingOn.removeWaitingOnKey(keyIndex);
safeCommand.updateWaitingOn(waitingOn);
if (!waitingOn.isWaiting())
- maybeExecute(safeStore, safeCommand, false, true, executeAsync);
+ maybeExecute(safeStore, safeCommand, false, true);
}
// TODO (now): document and justify all calls
@@ -1195,10 +1172,11 @@ public class Commands
{
CommandStore commandStore = safeStore.commandStore();
Command.Committed current = safeCommand.current().asCommitted();
+
WaitingOn.Update update = new WaitingOn.Update(current.waitingOn);
TxnId minWaitingOnTxnId = update.minWaitingOnTxnId();
if (minWaitingOnTxnId != null && commandStore.hasLocallyRedundantDependencies(update.minWaitingOnTxnId(), current.executeAt(), current.route().participants()))
- safeStore.commandStore().removeRedundantDependencies(current.route().participants(), current.partialDeps(), update);
+ safeStore.commandStore().removeRedundantDependencies(current.route().participants(), update);
// if we are a range transaction, being redundant for this transaction does not imply we are redundant for all transactions
if (redundant != null)
diff --git a/accord-core/src/main/java/accord/local/CommandsForKey.java b/accord-core/src/main/java/accord/local/CommandsForKey.java
deleted file mode 100644
index cc44f242..00000000
--- a/accord-core/src/main/java/accord/local/CommandsForKey.java
+++ /dev/null
@@ -1,1726 +0,0 @@
-/*
- * 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 accord.local;
-
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.EnumMap;
-import java.util.Objects;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import com.google.common.annotations.VisibleForTesting;
-
-import accord.api.Key;
-
-import accord.impl.CommandsSummary;
-import accord.local.SafeCommandStore.CommandFunction;
-import accord.local.SafeCommandStore.TestDep;
-import accord.local.SafeCommandStore.TestStartedAt;
-import accord.local.SafeCommandStore.TestStatus;
-import accord.primitives.Ballot;
-import accord.primitives.Keys;
-import accord.primitives.Participants;
-import accord.primitives.Seekables;
-import accord.primitives.Timestamp;
-import accord.primitives.Txn.Kind;
-import accord.primitives.Txn.Kind.Kinds;
-import accord.primitives.TxnId;
-import accord.utils.ArrayBuffers;
-import accord.utils.Invariants;
-import accord.utils.RelationMultiMap.SortedRelationList;
-import accord.utils.SortedArrays;
-import accord.utils.SortedList;
-
-import static accord.local.CommandsForKey.InternalStatus.ACCEPTED;
-import static accord.local.CommandsForKey.InternalStatus.APPLIED;
-import static accord.local.CommandsForKey.InternalStatus.COMMITTED;
-import static accord.local.CommandsForKey.InternalStatus.PREACCEPTED;
-import static accord.local.CommandsForKey.InternalStatus.STABLE;
-import static accord.local.CommandsForKey.InternalStatus.HISTORICAL;
-import static accord.local.CommandsForKey.InternalStatus.INVALID_OR_TRUNCATED;
-import static accord.local.CommandsForKey.InternalStatus.TRANSITIVELY_KNOWN;
-import static accord.local.CommandsForKey.InternalStatus.from;
-import static accord.local.CommandsForKey.Unmanaged.Pending.APPLY;
-import static accord.local.CommandsForKey.Unmanaged.Pending.COMMIT;
-import static accord.local.SafeCommandStore.TestDep.ANY_DEPS;
-import static accord.local.SafeCommandStore.TestDep.WITH;
-import static accord.local.SaveStatus.LocalExecution.WaitingToApply;
-import static accord.local.SaveStatus.LocalExecution.WaitingToExecute;
-import static accord.primitives.Txn.Kind.Kinds.AnyGloballyVisible;
-import static accord.utils.ArrayBuffers.cachedTxnIds;
-import static accord.utils.Invariants.illegalState;
-import static accord.utils.Invariants.isParanoid;
-import static accord.utils.SortedArrays.Search.CEIL;
-import static accord.utils.SortedArrays.Search.FAST;
-
-/**
- * A specialised collection for efficiently representing and querying everything we need for making coordination
- * and recovery decisions about a key's command conflicts.
- *
- * Every command we know about that is not shard-redundant is listed in the TxnId[] collection, which is sorted by TxnId.
- * This list implies the contents of the deps of all commands in the collection - it is assumed that in the normal course
- * of events every transaction will include the full set of TxnId we know. We only encode divergences from this, stored
- * in each command's {@code missing} collection.
- *
- * We then go one step further, exploiting the fact that the missing collection exists solely to implement recovery,
- * and so we elide from this missing collection any TxnId we have recorded as Committed or higher.
- * Any recovery coordinator that contacts this replica will report that the command has been agreed to execute,
- * and so will not need to decipher any fast-path decisions. So the missing collection is redundant, as no command's deps
- * will not be queried for this TxnId's presence/absence.
- * TODO (expected) this logic applies equally well to Accepted
- *
- * The goal with these behaviours is that this missing collection will ordinarily be empty, and represented by the exact
- * same NO_TXNIDS array instance as every other command.
- *
- * We also exploit the property that most commands will also agree to execute at their proposed TxnId. If we have
- * no missing collection to encode, and no modified executeAt, we store a global NoInfo object that takes up no
- * space on heap. These NoInfo objects permit further efficiencies, as we may perform class-pointer comparisons
- * before querying any contents to skip uninteresting contents, permitting fast iteration of the collection's contents.
- *
- * We also impose the condition that every TxnId is uniquely represented in the collection, so any executeAt and missing
- * collection that represents the same value as the TxnId must be the same object present in the main TxnId[].
- *
- * This collection also implements transitive dependency elision.
- * When evaluating mapReduceActive, we first establish the last-executing Stable write command (i.e. those whose deps
- * are considered durably decided, and so must wait for all commands Committed with a lower executeAt).
- * We then elide any Committed command that has a lower executeAt than this command.
- *
- * Both commands must be known at a majority, but neither might be Committed at any other replica.
- * Either command may therefore be recovered.
- * If the later command is recovered, this replica will report its Stable deps thereby recovering them.
- * If this replica is not contacted, some other replica must participate that either has taken the same action as this replica,
- * or else does not know the later command is Stable, and so will report the earlier command as a dependency again.
- * If the earlier command is recovered, this replica will report that it is Committed, and so will not consult
- * this replica's collection to decipher any fast path decision. Any other replica must either do the same, or else
- * will correctly record this transaction as present in any relevant deps of later transactions.
- *
- * TODO (expected): optimisations:
- * 3) consider storing a prefix of TxnId that are all NoInfo PreApplied encoded as a BitStream as only required for computing missing collection
- * 4) consider storing (or caching) an int[] of records with an executeAt that occurs out of order, sorted by executeAt
- *
- * TODO (expected): maintain separate redundantBefore and closedBefore timestamps, latter implied by any exclusivesyncpoint;
- * advance former based on Applied status of all TxnId before the latter
- * TODO (expected): track whether a TxnId is a write on this key only for execution (rather than globally)
- * TODO (expected): merge with TimestampsForKey
- * TODO (expected): save bytes by encoding InternalStatus in TxnId.flags()
- * TODO (expected): migrate to BTree
- * TODO (expected): remove a command that is committed to not intersect with the key for this store (i.e. if accepted in a later epoch than committed on, so ownership changes)
- * TODO (expected): mark a command as notified once ready-to-execute or applying
- * TODO (required): randomised testing
- * TODO (required): linearizability violation detection
- * TODO (required): account for whether transactions should witness each other for determining the missing collection
- * TODO (required): enforce that a transaction is not added with a key dependency on a SyncPoint or ExclusiveSyncPoint;
- * also impose this invariant elsewhere, and create a compile-time dependency between these code locations
- */
-public class CommandsForKey implements CommandsSummary
-{
- private static final boolean PRUNE_TRANSITIVE_DEPENDENCIES = true;
- public static final RedundantBefore.Entry NO_REDUNDANT_BEFORE = new RedundantBefore.Entry(null, Long.MIN_VALUE, Long.MAX_VALUE, TxnId.NONE, TxnId.NONE, TxnId.NONE, null);
- public static final TxnId[] NO_TXNIDS = new TxnId[0];
- public static final TxnInfo[] NO_INFOS = new TxnInfo[0];
- public static final Unmanaged[] NO_PENDING_UNMANAGED = new Unmanaged[0];
-
- public static class Unmanaged implements Comparable<Unmanaged>
- {
- public enum Pending { COMMIT, APPLY }
-
- public final Pending pending;
- public final Timestamp waitingUntil;
- public final TxnId txnId;
-
- public Unmanaged(Pending pending, TxnId txnId, Timestamp waitingUntil)
- {
- this.pending = pending;
- this.txnId = txnId;
- this.waitingUntil = waitingUntil;
- }
-
- @Override
- public int compareTo(Unmanaged that)
- {
- if (this.pending != that.pending) return this.pending == COMMIT ? -1 : 1;
- int c = this.waitingUntil.compareTo(that.waitingUntil);
- if (c == 0) c = this.txnId.compareTo(that.txnId);
- return c;
- }
-
- @Override
- public boolean equals(Object o)
- {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Unmanaged unmanaged = (Unmanaged) o;
- return pending == unmanaged.pending && waitingUntil.equals(unmanaged.waitingUntil) && txnId.equals(unmanaged.txnId);
- }
-
- @Override
- public int hashCode()
- {
- return Objects.hash(pending, waitingUntil, txnId);
- }
-
- @Override
- public String toString()
- {
- return "Pending{" + txnId + " until:" + waitingUntil + " " + pending + "}";
- }
- }
-
- public static class SerializerSupport
- {
- public static CommandsForKey create(Key key, TxnInfo[] txns, Unmanaged[] unmanageds)
- {
- return new CommandsForKey(key, NO_REDUNDANT_BEFORE, txns, unmanageds);
- }
- }
-
- public enum InternalStatus
- {
- TRANSITIVELY_KNOWN(false, false), // (unwitnessed; no need for mapReduce to witness)
- HISTORICAL(false, false),
- PREACCEPTED(false),
- ACCEPTED(true),
- COMMITTED(true),
- STABLE(true),
- APPLIED(true),
- INVALID_OR_TRUNCATED(false);
-
- static final EnumMap<SaveStatus, InternalStatus> convert = new EnumMap<>(SaveStatus.class);
- static final InternalStatus[] VALUES = values();
- static
- {
- convert.put(SaveStatus.PreAccepted, PREACCEPTED);
- convert.put(SaveStatus.AcceptedInvalidateWithDefinition, PREACCEPTED);
- convert.put(SaveStatus.Accepted, ACCEPTED);
- convert.put(SaveStatus.AcceptedWithDefinition, ACCEPTED);
- convert.put(SaveStatus.PreCommittedWithDefinition, PREACCEPTED);
- convert.put(SaveStatus.PreCommittedWithAcceptedDeps, ACCEPTED);
- convert.put(SaveStatus.PreCommittedWithDefinitionAndAcceptedDeps, ACCEPTED);
- convert.put(SaveStatus.Committed, COMMITTED);
- convert.put(SaveStatus.Stable, STABLE);
- convert.put(SaveStatus.ReadyToExecute, STABLE);
- convert.put(SaveStatus.PreApplied, STABLE);
- convert.put(SaveStatus.Applying, STABLE);
- convert.put(SaveStatus.Applied, APPLIED);
- convert.put(SaveStatus.TruncatedApplyWithDeps, INVALID_OR_TRUNCATED);
- convert.put(SaveStatus.TruncatedApplyWithOutcome, INVALID_OR_TRUNCATED);
- convert.put(SaveStatus.TruncatedApply, INVALID_OR_TRUNCATED);
- convert.put(SaveStatus.ErasedOrInvalidated, INVALID_OR_TRUNCATED);
- convert.put(SaveStatus.Erased, INVALID_OR_TRUNCATED);
- convert.put(SaveStatus.Invalidated, INVALID_OR_TRUNCATED);
- }
-
- public final boolean hasInfo;
- // TODO (desired): cleanup expectation logic re: accepted invalidate
- final InternalStatus expectMatch;
-
- InternalStatus(boolean hasInfo)
- {
- this(hasInfo, true);
- }
-
- InternalStatus(boolean hasInfo, boolean expectMatch)
- {
- this.hasInfo = hasInfo;
- this.expectMatch = expectMatch ? this : null;
- }
-
- boolean hasExecuteAt()
- {
- return hasInfo;
- }
-
- boolean hasDeps()
- {
- return hasInfo;
- }
-
- boolean hasStableDeps()
- {
- return this == STABLE || this == APPLIED;
- }
-
- public Timestamp depsKnownBefore(TxnId txnId, @Nullable Timestamp executeAt)
- {
- switch (this)
- {
- default: throw new AssertionError("Unhandled InternalStatus: " + this);
- case TRANSITIVELY_KNOWN:
- case INVALID_OR_TRUNCATED:
- case HISTORICAL:
- throw new AssertionError("Invalid InternalStatus to know deps");
-
- case PREACCEPTED:
- case ACCEPTED:
- return txnId;
-
- case APPLIED:
- case STABLE:
- case COMMITTED:
- return executeAt == null ? txnId : executeAt;
- }
- }
-
- @VisibleForTesting
- public static InternalStatus from(SaveStatus status)
- {
- return convert.get(status);
- }
-
- public static InternalStatus get(int ordinal)
- {
- return VALUES[ordinal];
- }
- }
-
- public static class TxnInfo extends TxnId
- {
- public final InternalStatus status;
- public final Timestamp executeAt;
-
- TxnInfo(TxnId txnId, InternalStatus status, Timestamp executeAt)
- {
- super(txnId);
- this.status = status;
- this.executeAt = executeAt == txnId ? this : executeAt;
- }
-
- public static TxnInfo create(@Nonnull TxnId txnId, InternalStatus status)
- {
- return new TxnInfo(txnId, status, txnId);
- }
-
- public static TxnInfo create(@Nonnull TxnId txnId, InternalStatus status, @Nonnull Timestamp executeAt)
- {
- Invariants.checkState(executeAt == txnId || !executeAt.equals(txnId));
- return new TxnInfo(txnId, status, executeAt);
- }
-
- public static TxnInfo create(@Nonnull TxnId txnId, InternalStatus status, @Nonnull Timestamp executeAt, @Nonnull TxnId[] missing)
- {
- Invariants.checkState(executeAt == txnId || !executeAt.equals(txnId));
- if (missing == NO_TXNIDS) return new TxnInfo(txnId, status, executeAt);
- return new TxnInfoWithMissing(txnId, status, executeAt, missing);
- }
-
- public static TxnInfo createMock(TxnId txnId, InternalStatus status, @Nullable Timestamp executeAt, @Nullable TxnId[] missing)
- {
- Invariants.checkState(executeAt == null || executeAt == txnId || !executeAt.equals(txnId));
- Invariants.checkArgument(missing == null || missing == NO_TXNIDS);
- if (missing == NO_TXNIDS) return new TxnInfo(txnId, status, executeAt);
- return new TxnInfoWithMissing(txnId, status, executeAt, missing);
- }
-
- Timestamp depsKnownBefore()
- {
- return status.depsKnownBefore(this, executeAt);
- }
-
- public TxnInfo update(TxnId[] newMissing)
- {
- return newMissing == NO_TXNIDS ? new TxnInfo(this, status, executeAt)
- : new TxnInfoWithMissing(this, status, executeAt, newMissing);
- }
-
- @Override
- public boolean equals(Object o)
- {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TxnInfo info = (TxnInfo) o;
- return status == info.status
- && (executeAt == this ? info.executeAt == info : Objects.equals(executeAt, info.executeAt))
- && Arrays.equals(missing(), info.missing());
- }
-
- TxnId asPlainTxnId()
- {
- return new TxnId(this);
- }
-
- public TxnId[] missing()
- {
- return NO_TXNIDS;
- }
-
- @Override
- public int hashCode()
- {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public String toString()
- {
- return "Info{" +
- "txnId=" + toPlainString() +
- ", status=" + status +
- ", executeAt=" + (executeAt == this ? toPlainString() : executeAt.toString()) +
- '}';
- }
-
- public String toPlainString()
- {
- return super.toString();
- }
- }
-
- public static class TxnInfoWithMissing extends TxnInfo
- {
- public final TxnId[] missing;
-
- TxnInfoWithMissing(TxnId txnId, InternalStatus status, Timestamp executeAt, TxnId[] missing)
- {
- super(txnId, status, executeAt);
- this.missing = missing;
- }
-
- public TxnId[] missing()
- {
- return missing;
- }
-
- @Override
- public String toString()
- {
- return "Info{" +
- "txnId=" + toPlainString() +
- ", status=" + status +
- ", executeAt=" + (this == executeAt ? toPlainString() : executeAt) +
- ", missing=" + Arrays.toString(missing) +
- '}';
- }
- }
-
- private final Key key;
- private final RedundantBefore.Entry redundantBefore;
- // all transactions, sorted by TxnId
- private final TxnInfo[] txns;
- // next and nextWrite are only non-null if they and their dependencies are all committed
- private final TxnInfo minUncommitted, next, nextWrite;
- // transactions that are committed or stable plus MAYBE Applied, keyed by executeAt
- private final TxnInfo[] committed;
- private final Unmanaged[] unmanageds;
-
- CommandsForKey(Key key, RedundantBefore.Entry redundantBefore, TxnInfo[] txns, Unmanaged[] unmanageds)
- {
- this.key = key;
- this.redundantBefore = Invariants.nonNull(redundantBefore);
- this.txns = txns;
- this.unmanageds = unmanageds;
- if (isParanoid()) Invariants.checkArgument(SortedArrays.isSortedUnique(txns));
-
- // TODO (required): update these efficiently
- int countCommitted = 0;
- TxnInfo minUncommitted = null, nextWrite = null, next = null;
- for (int i = 0 ; i < txns.length ; ++i)
- {
- TxnInfo txn = txns[i];
- if (txn.status == INVALID_OR_TRUNCATED) continue;
-
- if (txn.status.compareTo(COMMITTED) >= 0)
- {
- if (txn.status.compareTo(APPLIED) < 0)
- {
- if (txn.kind().isWrite() && (nextWrite == null || nextWrite.executeAt.compareTo(txn.executeAt) > 0))
- nextWrite = txn;
- if (next == null || next.executeAt.compareTo(txn.executeAt) > 0)
- next = txn;
- }
- ++countCommitted;
- }
- else if (minUncommitted == null) minUncommitted = txn;
- }
- if (minUncommitted != null)
- {
- // TODO (expected): discount minUncommitted if not a dependency of next/nextWrite
- if (next != null && minUncommitted.compareTo(next.executeAt) < 0)
- next = null;
- if (nextWrite != null && minUncommitted.compareTo(nextWrite.executeAt) < 0)
- nextWrite = null;
- }
- this.minUncommitted = minUncommitted;
- this.nextWrite = nextWrite;
- this.next = next;
- this.committed = new TxnInfo[countCommitted];
- countCommitted = 0;
- for (int i = 0 ; i < txns.length ; ++i)
- {
- if (txns[i].status.compareTo(COMMITTED) >= 0 && txns[i].status != INVALID_OR_TRUNCATED)
- committed[countCommitted++] = txns[i];
- }
- Arrays.sort(committed, Comparator.comparing(c -> c.executeAt));
- }
-
- public CommandsForKey(Key key)
- {
- this.key = key;
- this.redundantBefore = NO_REDUNDANT_BEFORE;
- this.txns = NO_INFOS;
- this.committed = NO_INFOS;
- this.minUncommitted = null;
- this.nextWrite = null;
- this.next = null;
- this.unmanageds = NO_PENDING_UNMANAGED;
- }
-
- @Override
- public String toString()
- {
- return "CommandsForKey@" + System.identityHashCode(this) + '{' + key + '}';
- }
-
- public Key key()
- {
- return key;
- }
-
- public int size()
- {
- return txns.length;
- }
-
- public int unmanagedCount()
- {
- return unmanageds.length;
- }
-
- public int indexOf(TxnId txnId)
- {
- return Arrays.binarySearch(txns, txnId);
- }
-
- public TxnId txnId(int i)
- {
- return txns[i];
- }
-
- public TxnInfo get(int i)
- {
- return txns[i];
- }
-
- public Unmanaged getUnmanaged(int i)
- {
- return unmanageds[i];
- }
-
- public TxnInfo get(TxnId txnId)
- {
- int i = indexOf(txnId);
- return i < 0 ? null : txns[i];
- }
-
- public RedundantBefore.Entry redundantBefore()
- {
- return redundantBefore;
- }
-
- public TxnId locallyRedundantBefore()
- {
- return redundantBefore.locallyRedundantBefore();
- }
-
- public TxnId shardRedundantBefore()
- {
- return redundantBefore.shardRedundantBefore();
- }
-
- /**
- * All commands before/after (exclusive of) the given timestamp
- * <p>
- * Note that {@code testDep} applies only to commands that MAY have the command in their deps; if specified any
- * commands that do not know any deps will be ignored, as will any with an executeAt prior to the txnId.
- * <p>
- */
- public <P1, T> T mapReduceFull(TxnId testTxnId,
- Kinds testKind,
- TestStartedAt testStartedAt,
- TestDep testDep,
- TestStatus testStatus,
- CommandFunction<P1, T, T> map, P1 p1, T initialValue)
- {
- int start, end;
- boolean isKnown;
- {
- int insertPos = Arrays.binarySearch(txns, testTxnId);
- isKnown = insertPos >= 0;
- if (!isKnown && testDep == WITH) return initialValue;
- if (!isKnown) insertPos = -1 - insertPos;
- switch (testStartedAt)
- {
- default: throw new AssertionError("Unhandled TestStartedAt: " + testTxnId);
- case STARTED_BEFORE: start = 0; end = insertPos; break;
- case STARTED_AFTER: start = insertPos; end = txns.length; break;
- case ANY: start = 0; end = txns.length;
- }
- }
-
- for (int i = start; i < end ; ++i)
- {
- TxnInfo txn = txns[i];
- if (!testKind.test(txn.kind())) continue;
- InternalStatus status = txn.status;
- switch (testStatus)
- {
- default: throw new AssertionError("Unhandled TestStatus: " + testStatus);
- case IS_PROPOSED:
- if (status == ACCEPTED || status == COMMITTED) break;
- else continue;
- case IS_STABLE:
- if (status.compareTo(STABLE) >= 0 && status.compareTo(INVALID_OR_TRUNCATED) < 0) break;
- else continue;
- case ANY_STATUS:
- if (status == TRANSITIVELY_KNOWN)
- continue;
- }
-
- Timestamp executeAt = txn.executeAt;
- if (testDep != ANY_DEPS)
- {
- if (!status.hasInfo)
- continue;
-
- if (executeAt.compareTo(testTxnId) <= 0)
- continue;
-
- boolean hasAsDep = Arrays.binarySearch(txn.missing(), testTxnId) < 0;
- if (hasAsDep != (testDep == WITH))
- continue;
- }
-
- initialValue = map.apply(p1, key, txn.asPlainTxnId(), executeAt, initialValue);
- }
- return initialValue;
- }
-
- public <P1, T> T mapReduceActive(Timestamp startedBefore,
- Kinds testKind,
- CommandFunction<P1, T, T> map, P1 p1, T initialValue)
- {
- Timestamp maxCommittedBefore;
- {
- int i = SortedArrays.binarySearch(committed, 0, committed.length, startedBefore, (f, v) -> f.compareTo(v.executeAt), FAST);
- if (i < 0) i = -2 - i;
- else --i;
- while (i >= 0 && !committed[i].kind().isWrite()) --i;
- maxCommittedBefore = i < 0 ? null : committed[i].executeAt;
- }
- int start = 0, end = insertPos(start, startedBefore);
-
- for (int i = start; i < end ; ++i)
- {
- TxnInfo txn = txns[i];
- if (!testKind.test(txn.kind()))
- continue;
-
- switch (txn.status)
- {
- case COMMITTED:
- case STABLE:
- case APPLIED:
- // TODO (expected): prove the correctness of this approach
- if (!PRUNE_TRANSITIVE_DEPENDENCIES || maxCommittedBefore == null || txn.executeAt.compareTo(maxCommittedBefore) >= 0)
- break;
- case TRANSITIVELY_KNOWN:
- case INVALID_OR_TRUNCATED:
- continue;
- }
-
- initialValue = map.apply(p1, key, txn.asPlainTxnId(), txn.executeAt, initialValue);
- }
- return initialValue;
- }
-
- public CommandsForKey update(Command prev, Command next)
- {
- return update(prev, next, true);
- }
-
- CommandsForKey update(Command prev, Command next, boolean hasPrev)
- {
- InternalStatus newStatus = InternalStatus.from(next.saveStatus());
- if (newStatus == null)
- return this;
-
- TxnId txnId = next.txnId();
- int pos = Arrays.binarySearch(txns, txnId);
- CommandsForKey result;
- if (pos < 0)
- {
- pos = -1 - pos;
- if (newStatus.hasInfo) result = insert(pos, txnId, newStatus, next);
- else result = insert(pos, txnId, TxnInfo.create(txnId, newStatus, txnId));
- }
- else
- {
- // update
- TxnInfo cur = txns[pos];
- if (hasPrev)
- {
- if (newStatus.compareTo(cur.status) <= 0)
- {
- // TODO (required): this validation is not safe for replay where we may have to "catch up" commands that are behind CFK
- // we can redundantly update the same transaction via notifyWaitingOnCommit since updates to CFK may be asynchronous
- // (particularly for invalidations). So we should expect that we might already represent the latest information for this transaction.
- // TODO (desired): consider only accepting this for Invalidation
- // TODO (desired): also clean-up special casing for AcceptedInvalidate, which exists because it currently has no effect on the CFK state
- // so it could be any of Transitively Known, Historic, PreAccept or Accept
- Invariants.checkState(cur.status == newStatus || next.status() == Status.AcceptedInvalidate, "Attempted update to CommandsForKey with %s, implying stale status; found %s", next, cur);
- if (!newStatus.hasInfo || next.acceptedOrCommitted().equals(prev.acceptedOrCommitted()))
- return this;
- }
-
- InternalStatus prevStatus = prev == null ? null : InternalStatus.from(prev.saveStatus());
- Invariants.checkState(cur.status.expectMatch == prevStatus || cur.status.expectMatch == null && !prev.keysOrRanges().contains(key) || (prev != null && prev.status() == Status.AcceptedInvalidate));
- }
- else if (newStatus.compareTo(cur.status) <= 0)
- {
- Invariants.checkState(cur.status == newStatus || next.status() == Status.AcceptedInvalidate);
- if (!newStatus.hasInfo)
- return this;
- }
-
- if (newStatus.hasInfo) result = update(pos, txnId, cur, newStatus, next);
- else result = update(pos, txnId, cur, TxnInfo.create(txnId, newStatus, txnId));
- }
-
- return result;
- }
-
- public static boolean needsUpdate(Command prev, Command updated)
- {
- if (!updated.txnId().kind().isGloballyVisible())
- return false;
-
- SaveStatus prevStatus;
- Ballot prevAcceptedOrCommitted;
- if (prev == null)
- {
- prevStatus = SaveStatus.NotDefined;
- prevAcceptedOrCommitted = Ballot.ZERO;
- }
- else
- {
- prevStatus = prev.saveStatus();
- prevAcceptedOrCommitted = prev.acceptedOrCommitted();
- }
-
- return needsUpdate(prevStatus, prevAcceptedOrCommitted, updated.saveStatus(), updated.acceptedOrCommitted());
- }
-
- public static boolean needsUpdate(SaveStatus prevStatus, Ballot prevAcceptedOrCommitted, SaveStatus updatedStatus, Ballot updatedAcceptedOrCommitted)
- {
- InternalStatus prev = InternalStatus.from(prevStatus);
- InternalStatus updated = InternalStatus.from(updatedStatus);
- return updated != prev || (updated != null && updated.hasInfo && !prevAcceptedOrCommitted.equals(updatedAcceptedOrCommitted));
- }
-
- private CommandsForKey insert(int insertPos, TxnId insertTxnId, InternalStatus newStatus, Command command)
- {
- Object newInfo = computeInsert(insertPos, insertTxnId, newStatus, command);
- if (newInfo.getClass() != InfoWithAdditions.class)
- return insert(insertPos, insertTxnId, (TxnInfo)newInfo);
-
- InfoWithAdditions newInfoWithAdditions = (InfoWithAdditions) newInfo;
- TxnInfo[] newTxns = new TxnInfo[txns.length + newInfoWithAdditions.additionCount + 1];
- insertWithAdditions(insertPos, insertTxnId, newInfoWithAdditions, newTxns);
- return update(newTxns);
- }
-
- private CommandsForKey update(int updatePos, TxnId plainTxnId, TxnInfo prevInfo, InternalStatus newStatus, Command command)
- {
- Object newInfo = computeUpdate(updatePos, plainTxnId, newStatus, command);
- if (newInfo.getClass() != InfoWithAdditions.class)
- return update(updatePos, plainTxnId, prevInfo, (TxnInfo)newInfo);
-
- InfoWithAdditions newInfoWithAdditions = (InfoWithAdditions) newInfo;
- TxnInfo[] newTxns = new TxnInfo[txns.length + newInfoWithAdditions.additionCount];
- updateWithAdditions(updatePos, plainTxnId, newInfoWithAdditions, newTxns);
- if (prevInfo.status.compareTo(COMMITTED) < 0 && newStatus.compareTo(COMMITTED) >= 0)
- removeMissing(newTxns, plainTxnId);
- return update(newTxns);
- }
-
- private void updateWithAdditions(int updatePos, TxnId updateTxnId, InfoWithAdditions withInfo, TxnInfo[] newInfos)
- {
- updateOrInsertWithAdditions(updatePos, updatePos, updateTxnId, withInfo, newInfos);
- }
-
- private void insertWithAdditions(int pos, TxnId updateTxnId, InfoWithAdditions withInfo, TxnInfo[] newInfos)
- {
- updateOrInsertWithAdditions(pos, -1, updateTxnId, withInfo, newInfos);
- }
-
- private void updateOrInsertWithAdditions(int sourceInsertPos, int sourceUpdatePos, TxnId updatePlainTxnId, InfoWithAdditions withInfo, TxnInfo[] newTxns)
- {
- TxnId[] additions = withInfo.additions;
- int additionCount = withInfo.additionCount;
- int additionInsertPos = Arrays.binarySearch(additions, 0, additionCount, updatePlainTxnId);
- additionInsertPos = Invariants.checkArgument(-1 - additionInsertPos, additionInsertPos < 0);
- int targetInsertPos = sourceInsertPos + additionInsertPos;
-
- // additions plus the updateTxnId when necessary
- TxnId[] missingSource = additions;
- boolean insertSelfMissing = sourceUpdatePos < 0 && withInfo.info.status.compareTo(COMMITTED) < 0;
-
- // the most recently constructed pure insert missing array, so that it may be reused if possible
- int i = 0, j = 0, missingCount = 0, missingLimit = additionCount, count = 0;
- while (i < txns.length)
- {
- if (count == targetInsertPos)
- {
- newTxns[count] = withInfo.info;
- if (i == sourceUpdatePos) ++i;
- else if (insertSelfMissing) ++missingCount;
- ++count;
- continue;
- }
-
- int c = j == additionCount ? -1 : txns[i].compareTo(additions[j]);
- if (c < 0)
- {
- TxnInfo txn = txns[i];
- if (i == sourceUpdatePos)
- {
- txn = withInfo.info;
- }
- else if (txn.status.hasDeps())
- {
- Timestamp depsKnownBefore = txn.depsKnownBefore();
- if (insertSelfMissing && missingSource == additions && (missingCount != j || (depsKnownBefore != txn && depsKnownBefore.compareTo(updatePlainTxnId) > 0)))
- {
- missingSource = insertMissing(additions, additionCount, updatePlainTxnId, additionInsertPos);
- ++missingLimit;
- }
-
- int to = to(txn, depsKnownBefore, missingSource, missingCount, missingLimit);
- if (to > 0)
- {
- TxnId[] prevMissing = txn.missing();
- TxnId[] newMissing = mergeAndFilterMissing(txn, prevMissing, missingSource, to);
- if (newMissing != prevMissing)
- txn = txn.update(newMissing);
- }
- }
- newTxns[count] = txn;
- i++;
- }
- else if (c > 0)
- {
- TxnId txnId = additions[j++];
- newTxns[count] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId);
- ++missingCount;
- }
- else
- {
- throw illegalState(txns[i] + " should be an insertion, but found match when merging with origin");
- }
- count++;
- }
-
- if (j < additionCount)
- {
- if (count <= targetInsertPos)
- {
- while (count < targetInsertPos)
- {
- TxnId txnId = additions[j++];
- newTxns[count++] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId);
- }
- newTxns[targetInsertPos] = withInfo.info;
- count = targetInsertPos + 1;
- }
- while (j < additionCount)
- {
- TxnId txnId = additions[j++];
- newTxns[count++] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId);
- }
- }
- else if (count == targetInsertPos)
- {
- newTxns[targetInsertPos] = withInfo.info;
- }
-
- cachedTxnIds().forceDiscard(additions, additionCount);
- }
-
- private CommandsForKey update(int pos, TxnId plainTxnId, TxnInfo curInfo, TxnInfo newInfo)
- {
- if (curInfo == newInfo)
- return this;
-
- TxnInfo[] newTxns = txns.clone();
- newTxns[pos] = newInfo;
- if (curInfo.status.compareTo(COMMITTED) < 0 && newInfo.status.compareTo(COMMITTED) >= 0)
- removeMissing(newTxns, plainTxnId);
- return update(newTxns);
- }
-
- /**
- * Insert a new txnId and info
- */
- private CommandsForKey insert(int pos, TxnId insertPlainTxnId, TxnInfo insert)
- {
- TxnInfo[] newTxns = new TxnInfo[txns.length + 1];
- if (insert.status.compareTo(COMMITTED) >= 0)
- {
- System.arraycopy(txns, 0, newTxns, 0, pos);
- newTxns[pos] = insert;
- System.arraycopy(txns, pos, newTxns, pos + 1, txns.length - pos);
- }
- else
- {
- insertInfoAndOneMissing(pos, insertPlainTxnId, insert, txns, newTxns, 1);
- }
- return update(newTxns);
- }
-
- /**
- * Insert a new txnId and info, then insert the txnId into the missing collection of any command that should have already caused us to witness it
- */
- private static void insertInfoAndOneMissing(int insertPos, TxnId insertedPlainTxnId, TxnInfo inserted, TxnInfo[] oldTxns, TxnInfo[] newTxns, int offsetAfterInsertPos)
- {
- TxnId[] oneMissing = null;
- for (int i = 0 ; i < insertPos ; ++i)
- {
- TxnInfo txn = oldTxns[i];
- if (txn.status.hasDeps())
- {
- Timestamp depsKnownBefore = txn.depsKnownBefore();
- if (depsKnownBefore != null && depsKnownBefore.compareTo(inserted) > 0 && txn.kind().witnesses(inserted))
- {
- TxnId[] missing = txn.missing();
- if (missing == NO_TXNIDS)
- missing = oneMissing = ensureOneMissing(insertedPlainTxnId, oneMissing);
- else
- missing = SortedArrays.insert(missing, insertedPlainTxnId, TxnId[]::new);
-
- txn = txn.update(missing);
- }
- }
- newTxns[i] = txn;
- }
-
- newTxns[insertPos] = inserted;
-
- for (int i = insertPos; i < oldTxns.length ; ++i)
- {
- int newIndex = i + offsetAfterInsertPos;
- TxnInfo txn = oldTxns[i];
- if (txn.status.hasDeps())
- {
- if (txn.kind().witnesses(inserted))
- {
- TxnId[] missing = txn.missing();
- if (missing == NO_TXNIDS)
- missing = oneMissing = ensureOneMissing(insertedPlainTxnId, oneMissing);
- else
- missing = SortedArrays.insert(missing, insertedPlainTxnId, TxnId[]::new);
-
- txn = txn.update(missing);
- }
- }
-
- newTxns[newIndex] = txn;
- }
- }
-
- private static void removeMissing(TxnInfo[] txns, TxnId removeTxnId)
- {
- for (int i = 0 ; i < txns.length ; ++i)
- {
- TxnInfo txn = txns[i];
- TxnId[] missing = txn.missing();
- if (missing == NO_TXNIDS) continue;
-
- int j = Arrays.binarySearch(missing, removeTxnId);
- if (j < 0) continue;
-
- if (missing.length == 1)
- {
- missing = NO_TXNIDS;
- }
- else
- {
- int length = missing.length;
- TxnId[] newMissing = new TxnId[length - 1];
- System.arraycopy(missing, 0, newMissing, 0, j);
- System.arraycopy(missing, j + 1, newMissing, j, length - (1 + j));
- missing = newMissing;
- }
-
- txns[i] = txn.update(missing);
- }
- }
-
- private static TxnId[] ensureOneMissing(TxnId txnId, TxnId[] oneMissing)
- {
- return oneMissing != null ? oneMissing : new TxnId[] { txnId };
- }
-
- private static TxnId[] insertMissing(TxnId[] additions, int additionCount, TxnId updateTxnId, int additionInsertPos)
- {
- TxnId[] result = new TxnId[additionCount + 1];
- System.arraycopy(additions, 0, result, 0, additionInsertPos);
- System.arraycopy(additions, additionInsertPos, result, additionInsertPos + 1, additionCount - additionInsertPos);
- result[additionInsertPos] = updateTxnId;
- return result;
- }
-
- private static TxnId[] mergeAndFilterMissing(TxnId owner, TxnId[] current, TxnId[] additions, int additionCount)
- {
- Kinds kinds = owner.kind().witnesses();
- int additionLength = additionCount;
- for (int i = additionCount - 1 ; i >= 0 ; --i)
- {
- if (!kinds.test(additions[i].kind()))
- --additionCount;
- }
-
- if (additionCount == 0)
- return current;
-
- TxnId[] buffer = cachedTxnIds().get(current.length + additionCount);
- int i = 0, j = 0, count = 0;
- while (i < additionLength && j < current.length)
- {
- if (kinds.test(additions[i].kind()))
- {
- int c = additions[i].compareTo(current[j]);
- if (c < 0) buffer[count++] = additions[i++];
- else buffer[count++] = current[j++];
- }
- else i++;
- }
- while (i < additionLength)
- {
- if (kinds.test(additions[i].kind()))
- buffer[count++] = additions[i];
- i++;
- }
- while (j < current.length)
- {
- buffer[count++] = current[j++];
- }
- Invariants.checkState(count == additionCount + current.length);
- return cachedTxnIds().completeAndDiscard(buffer, count);
- }
-
- private static int to(TxnId txnId, Timestamp depsKnownBefore, TxnId[] missingSource, int missingCount, int missingLimit)
- {
- if (depsKnownBefore == txnId) return missingCount;
- int to = Arrays.binarySearch(missingSource, 0, missingLimit, depsKnownBefore);
- if (to < 0) to = -1 - to;
- return to;
- }
-
- static class InfoWithAdditions
- {
- final TxnInfo info;
- final TxnId[] additions;
- final int additionCount;
-
- InfoWithAdditions(TxnInfo info, TxnId[] additions, int additionCount)
- {
- this.info = info;
- this.additions = additions;
- this.additionCount = additionCount;
- }
- }
-
- private Object computeInsert(int insertPos, TxnId txnId, InternalStatus newStatus, Command command)
- {
- return computeInfoAndAdditions(insertPos, -1, txnId, newStatus, command);
- }
-
- private Object computeUpdate(int updatePos, TxnId txnId, InternalStatus newStatus, Command command)
- {
- return computeInfoAndAdditions(updatePos, updatePos, txnId, newStatus, command);
- }
-
- private Object computeInfoAndAdditions(int insertPos, int updatePos, TxnId txnId, InternalStatus newStatus, Command command)
- {
- Timestamp executeAt = txnId;
- if (newStatus.hasInfo)
- {
- executeAt = command.executeAt();
- if (executeAt.equals(txnId)) executeAt = txnId;
- }
- Timestamp depsKnownBefore = newStatus.depsKnownBefore(txnId, executeAt);
- return computeInfoAndAdditions(insertPos, updatePos, txnId, newStatus, executeAt, depsKnownBefore, command.partialDeps().keyDeps.txnIds(key));
- }
-
- private Object computeInfoAndAdditions(int insertPos, int updatePos, TxnId plainTxnId, InternalStatus newStatus, Timestamp executeAt, Timestamp depsKnownBefore, SortedList<TxnId> deps)
- {
- int depsKnownBeforePos;
- if (depsKnownBefore == plainTxnId)
- {
- depsKnownBeforePos = insertPos;
- }
- else
- {
- depsKnownBeforePos = Arrays.binarySearch(txns, insertPos, txns.length, depsKnownBefore);
- Invariants.checkState(depsKnownBeforePos < 0);
- depsKnownBeforePos = -1 - depsKnownBeforePos;
- }
-
- TxnId[] additions = NO_TXNIDS, missing = NO_TXNIDS;
- int additionCount = 0, missingCount = 0;
-
- int depsIndex = deps.find(redundantBefore.shardRedundantBefore());
- if (depsIndex < 0) depsIndex = -1 - depsIndex;
- int txnIdsIndex = 0;
- while (txnIdsIndex < depsKnownBeforePos && depsIndex < deps.size())
- {
- TxnInfo t = txns[txnIdsIndex];
- TxnId d = deps.get(depsIndex);
- int c = t.compareTo(d);
- if (c == 0)
- {
- ++txnIdsIndex;
- ++depsIndex;
- }
- else if (c < 0)
- {
- // we expect to be missing ourselves
- // we also permit any transaction we have recorded as COMMITTED or later to be missing, as recovery will not need to consult our information
- if (txnIdsIndex != updatePos && t.status.compareTo(COMMITTED) < 0 && plainTxnId.kind().witnesses(t))
- {
- if (missingCount == missing.length)
- missing = cachedTxnIds().resize(missing, missingCount, Math.max(8, missingCount * 2));
- missing[missingCount++] = t.asPlainTxnId();
- }
- txnIdsIndex++;
- }
- else
- {
- if (additionCount >= additions.length)
- additions = cachedTxnIds().resize(additions, additionCount, Math.max(8, additionCount * 2));
-
- additions[additionCount++] = d;
- depsIndex++;
- }
- }
-
- while (txnIdsIndex < depsKnownBeforePos)
- {
- if (txnIdsIndex != updatePos && txns[txnIdsIndex].status.compareTo(COMMITTED) < 0)
- {
- TxnId txnId = txns[txnIdsIndex].asPlainTxnId();
- if ((plainTxnId.kind().witnesses(txnId)))
- {
- if (missingCount == missing.length)
- missing = cachedTxnIds().resize(missing, missingCount, Math.max(8, missingCount * 2));
- missing[missingCount++] = txnId;
- }
- }
- txnIdsIndex++;
- }
-
- while (depsIndex < deps.size())
- {
- if (additionCount >= additions.length)
- additions = cachedTxnIds().resize(additions, additionCount, Math.max(8, additionCount * 2));
- additions[additionCount++] = deps.get(depsIndex++);
- }
-
- TxnInfo info = TxnInfo.create(plainTxnId, newStatus, executeAt, cachedTxnIds().completeAndDiscard(missing, missingCount));
- if (additionCount == 0)
- return info;
-
- return new InfoWithAdditions(info, additions, additionCount);
- }
-
- private CommandsForKey update(TxnInfo[] newTxns)
- {
- return new CommandsForKey(key, redundantBefore, newTxns, unmanageds);
- }
-
- // directly notify the progress log of any command we're now awaiting the coordination of (as the lowest TxnId not committed)
- public CommandsForKey notifyAndUpdatePending(SafeCommandStore safeStore, Command command, CommandsForKey prevCfk)
- {
- return notifyAndUpdatePending(safeStore, command.txnId(), from(command.saveStatus()), command.executeAt(), prevCfk);
- }
-
- public CommandsForKey notifyAndUpdatePending(SafeCommandStore safeStore, TxnId updatedTxnId, InternalStatus newStatus, Timestamp newExecuteAt, CommandsForKey prevCfk)
- {
- TxnInfo prevInfo = prevCfk.get(updatedTxnId);
-
- switch (newStatus)
- {
- default: throw new AssertionError("Unhandled InternalStatus: " + newStatus);
- case TRANSITIVELY_KNOWN:
- case PREACCEPTED:
- case HISTORICAL:
- case ACCEPTED:
- break;
-
- case STABLE:
- case COMMITTED:
- int compareWithNext = nextWrite == null ? -1 : newExecuteAt.compareTo(nextWrite.executeAt);
- if (compareWithNext <= 0)
- {
- if (newStatus == COMMITTED)
- break;
-
- // the updated transaction itself might be executable
- notify(safeStore, AnyGloballyVisible, next == null ? Timestamp.NONE : next.executeAt, newExecuteAt);
- }
- else
- {
- // somebody waiting on us might now be ready to execute, but only if we execute after them and were not previously committed, but WERE previously known
- // if the next command executes after us, or executes before we were started
- // then our change in status cannot have triggered any change in immediate execution dependencies
- if (prevInfo == null || prevInfo.status == COMMITTED || nextWrite.executeAt.compareTo(updatedTxnId) < 0 || newExecuteAt.equals(updatedTxnId))
- break;
-
- notify(safeStore, updatedTxnId.kind().witnessedBy(), next.executeAt, nextWrite.executeAt);
- }
- break;
-
- case APPLIED:
- case INVALID_OR_TRUNCATED:
- if (next != null)
- notify(safeStore, updatedTxnId.kind().witnessedBy(), next.executeAt, nextWrite != null ? nextWrite.executeAt : Timestamp.MAX);
- break;
- }
-
- Unmanaged[] pending = this.unmanageds;
- if (minUncommitted != null && (prevCfk.minUncommitted == null || !prevCfk.minUncommitted.equals(minUncommitted)))
- notifyWaitingOnCommit(safeStore, minUncommitted);
- if (newStatus.compareTo(COMMITTED) >= 0 && (prevInfo == null || prevInfo.status.compareTo(COMMITTED) < 0))
- pending = notifyUnmanaged(safeStore, COMMIT, pending, minUncommitted == null ? Timestamp.MAX : minUncommitted, this);
- if (minUncommitted == null || next != null)
- pending = notifyUnmanaged(safeStore, APPLY, pending, next == null ? Timestamp.MAX : next.executeAt, this);
-
- return new CommandsForKey(key, redundantBefore, txns, pending);
- }
-
- public CommandsForKey notifyAndUpdatePending(SafeCommandStore safeStore, CommandsForKey prevCfk)
- {
- Unmanaged[] pending = this.unmanageds;
- if (minUncommitted != null && (prevCfk.minUncommitted == null || !prevCfk.minUncommitted.equals(minUncommitted)))
- notifyWaitingOnCommit(safeStore, minUncommitted);
- if (minUncommitted == null || next != null)
- pending = notifyUnmanaged(safeStore, APPLY, pending, next == null ? Timestamp.MAX : next.executeAt, this);
-
- return new CommandsForKey(key, redundantBefore, txns, pending);
- }
-
- private void notifyWaitingOnCommit(SafeCommandStore safeStore, TxnInfo uncommitted)
- {
- if (redundantBefore.endEpoch <= uncommitted.epoch())
- return;
-
- TxnId txnId = uncommitted.asPlainTxnId();
- if (uncommitted.status.compareTo(PREACCEPTED) < 0)
- {
- Key key = this.key;
- Keys keys = Keys.of(key);
- safeStore = safeStore; // make unsafe for compiler to permit in lambda
- safeStore.commandStore().execute(PreLoadContext.contextFor(txnId, keys, KeyHistory.COMMANDS), safeStore0 -> {
- SafeCommand safeCommand0 = safeStore0.get(txnId);
- safeCommand0.initialise();
- Command command = safeCommand0.current();
- Seekables<?, ?> keysOrRanges = command.keysOrRanges();
- if (keysOrRanges == null || !keysOrRanges.contains(key))
- {
- CommonAttributes.Mutable attrs = command.mutable();
- if (command.additionalKeysOrRanges() == null) attrs.additionalKeysOrRanges(keys);
- else attrs.additionalKeysOrRanges(keys.with((Keys)command.additionalKeysOrRanges()));
- safeCommand0.update(safeStore0, command.updateAttributes(attrs));
- }
- if (command.hasBeen(Status.Committed))
- {
- // if we're committed but not invalidated, that means EITHER we have raced with a commit+
- // OR we adopted as a dependency a
- safeStore0.get(key).update(safeStore0, safeCommand0.current());
- }
- else
- {
- // TODO (desired): we could complicate our state machine to replicate PreCommitted here, so we can simply wait for ReadyToExclude
- safeStore0.progressLog().waiting(txnId, WaitingToExecute, null, keys.toParticipants());
- }
- }).begin(safeStore.agent());
- }
- else
- {
- safeStore.progressLog().waiting(txnId, WaitingToExecute, null, Keys.of(key).toParticipants());
- }
- }
-
- private static Unmanaged[] notifyUnmanaged(SafeCommandStore safeStore, Unmanaged.Pending pendingStatus, Unmanaged[] pending, Timestamp timestamp, CommandsForKey cfk)
- {
- switch (pendingStatus)
- {
- default: throw new AssertionError("Unhandled PendingRangeTransaction.Pending: " + pendingStatus);
- case COMMIT:
- {
- // notify commit uses exclusive bounds, as we use minUncommitted
- int end = findPendingCommit(pending, timestamp);
- if (end < 0) end = -1 - end;
-
- Unmanaged[] addPending = new Unmanaged[end];
- int addPendingCount = 0;
- for (int i = 0 ; i < end ; ++i)
- {
- TxnId txnId = pending[i].txnId;
- SafeCommand safeCommand = safeStore.ifLoadedAndInitialised(txnId);
- if (safeCommand != null)
- {
- addPending[addPendingCount] = updatePending(safeStore, safeCommand, cfk);
- if (addPending[addPendingCount] != null) ++addPendingCount;
- }
- else updatePendingAsync(safeStore, txnId, cfk.key);
- }
-
- Arrays.sort(addPending, 0, addPendingCount);
- return SortedArrays.linearUnion(pending, end, pending.length, addPending, 0, addPendingCount, Unmanaged::compareTo, ArrayBuffers.uncached(Unmanaged[]::new));
- }
- case APPLY:
- {
- int start = -1 - SortedArrays.binarySearch(pending, 0, pending.length, timestamp, (f, v) -> v.pending == APPLY ? -1 : 1, FAST);
- int end = findPendingApply(pending, timestamp);
- if (end < 0) end = -1 - end;
-
- if (start == end)
- return pending;
-
- for (int i = start; i < end ; ++i)
- removeWaitingOn(safeStore, pending[i].txnId, cfk.key);
-
- Unmanaged[] newPending = new Unmanaged[pending.length - (end - start)];
- System.arraycopy(pending, 0, newPending, 0, start);
- System.arraycopy(pending, end, newPending, start, newPending.length - start);
- return newPending;
- }
- }
- }
-
- private static void updatePendingAsync(SafeCommandStore safeStore, TxnId txnId, Key key)
- {
- PreLoadContext context = PreLoadContext.contextFor(txnId, Keys.of(key), KeyHistory.COMMANDS);
- safeStore.commandStore().execute(context, safeStore0 -> {
- SafeCommandsForKey safeCommandsForKey = safeStore0.get(key);
- CommandsForKey prev = safeCommandsForKey.current();
- Unmanaged addPending = updatePending(safeStore0, safeStore0.get(txnId), prev);
- if (addPending != null)
- {
- Unmanaged[] newPending = SortedArrays.insert(prev.unmanageds, addPending, Unmanaged[]::new);
- safeCommandsForKey.set(new CommandsForKey(prev.key, prev.redundantBefore, prev.txns, newPending));
- }
- }).begin(safeStore.agent());
- }
-
- private static Unmanaged updatePending(SafeCommandStore safeStore, SafeCommand safeCommand, CommandsForKey cfk)
- {
- if (safeCommand.current().hasBeen(Status.Truncated))
- return null;
-
- Command.Committed command = safeCommand.current().asCommitted();
- TxnId waitingTxnId = command.txnId();
- Timestamp waitingExecuteAt = command.executeAt();
-
- SortedRelationList<TxnId> txnIds = command.partialDeps().keyDeps.txnIds(cfk.key);
- int i = txnIds.find(cfk.shardRedundantBefore());
- if (i < 0) i = -1 - i;
-
- if (i < txnIds.size())
- {
- boolean readyToExecute = true;
- Timestamp executesAt = null;
- int j = SortedArrays.binarySearch(cfk.txns, 0, cfk.txns.length, txnIds.get(i), Timestamp::compareTo, FAST);
- if (j < 0) j = -1 -j;
- while (i < txnIds.size() && j < cfk.txns.length)
- {
- int c = txnIds.get(i).compareTo(cfk.txns[j]);
- if (c > 0) ++j;
- else if (c < 0) throw illegalState("Transaction not found: " + cfk.txns[j]);
- else
- {
- TxnInfo txn = cfk.txns[j];
- if (waitingTxnId.kind().awaitsOnlyDeps() || txn.executeAt.compareTo(waitingExecuteAt) < 0)
- {
- readyToExecute &= txn.status.compareTo(APPLIED) >= 0;
- executesAt = Timestamp.nonNullOrMax(executesAt, txn.executeAt);
- }
- ++i;
- ++j;
- }
- }
-
- if (!readyToExecute)
- {
- if (waitingTxnId.kind().awaitsOnlyDeps() && executesAt != null)
- {
- if (executesAt instanceof TxnInfo) executesAt = ((TxnInfo) executesAt).asPlainTxnId();
- if (executesAt.compareTo(command.waitingOn.executeAtLeast(Timestamp.NONE)) > 0)
- {
- Command.WaitingOn.Update waitingOn = new Command.WaitingOn.Update(command.waitingOn);
- waitingOn.updateExecuteAtLeast(executesAt);
- safeCommand.updateWaitingOn(waitingOn);
- }
- }
- return new Unmanaged(APPLY, command.txnId(), executesAt);
- }
- }
-
- removeWaitingOn(safeStore, safeCommand, cfk.key, true);
- return null;
- }
-
- private static int findPendingCommit(Unmanaged[] pending, Timestamp txnId)
- {
- return SortedArrays.binarySearch(pending, 0, pending.length, txnId, (f, v) -> {
- if (v.pending == APPLY) return -1;
- return f.compareTo(v.waitingUntil);
- }, CEIL);
- }
-
- private static int findPendingApply(Unmanaged[] pending, Timestamp txnId)
- {
- return SortedArrays.binarySearch(pending, 0, pending.length, txnId, (f, v) -> {
- if (v.pending == COMMIT) return 1;
- return f.compareTo(v.waitingUntil);
- }, CEIL);
- }
-
- public CommandsForKey registerUnmanaged(SafeCommandStore safeStore, SafeCommand safeCommand)
- {
- if (safeCommand.current().hasBeen(Status.Truncated))
- return this;
-
- Command.Committed command = safeCommand.current().asCommitted();
- TxnId waitingTxnId = command.txnId();
- Timestamp waitingExecuteAt = command.executeAt();
-
- SortedRelationList<TxnId> txnIds = command.partialDeps().keyDeps.txnIds(key);
- TxnId[] missing = cachedTxnIds().get(8);
- int missingCount = 0;
- int i = txnIds.find(shardRedundantBefore());
- if (i < 0) i = -1 - i;
- if (i < txnIds.size())
- {
- boolean readyToExecute = true;
- boolean waitingToApply = true;
- Timestamp executesAt = null;
- int j = SortedArrays.binarySearch(txns, 0, txns.length, txnIds.get(i), Timestamp::compareTo, FAST);
- if (j < 0) j = -1 -j;
- while (i < txnIds.size())
- {
- int c = j == txns.length ? -1 : txnIds.get(i).compareTo(txns[j]);
- if (c == 0)
- {
- TxnInfo txn = txns[j];
- if (txn.status.compareTo(COMMITTED) < 0) readyToExecute = waitingToApply = false;
- else if (waitingTxnId.kind().awaitsOnlyDeps() || txn.executeAt.compareTo(waitingExecuteAt) < 0)
- {
- readyToExecute &= txn.status.compareTo(APPLIED) >= 0;
- executesAt = Timestamp.nonNullOrMax(executesAt, txn.executeAt);
- }
- ++i;
- ++j;
- }
- else if (c > 0) ++j;
- else
- {
- readyToExecute = waitingToApply = false;
- if (missingCount == missing.length)
- missing = cachedTxnIds().resize(missing, missingCount, missingCount * 2);
- missing[missingCount++] = txnIds.get(i++);
- }
- }
-
- if (!readyToExecute)
- {
- TxnInfo[] newTxns = txns;
- if (missingCount > 0)
- {
- newTxns = new TxnInfo[missingCount + txns.length];
- i = 0;
- j = 0;
- int count = 0;
- while (i < missingCount && j < txns.length)
- {
- int c = missing[i].compareTo(txns[j]);
- if (c < 0) newTxns[count++] = TxnInfo.create(missing[i++], TRANSITIVELY_KNOWN);
- else newTxns[count++] = txns[j++];
- }
- while (i < missingCount)
- newTxns[count++] = TxnInfo.create(missing[i++], TRANSITIVELY_KNOWN);
- while (j < txns.length)
- newTxns[count++] = txns[j++];
- }
-
- Unmanaged newPendingRecord;
- if (waitingToApply)
- {
- if (waitingTxnId.kind().awaitsOnlyDeps() && executesAt != null)
- {
- if (executesAt.compareTo(command.waitingOn.executeAtLeast(Timestamp.NONE)) > 0)
- {
- Command.WaitingOn.Update waitingOn = new Command.WaitingOn.Update(command.waitingOn);
- waitingOn.updateExecuteAtLeast(executesAt);
- safeCommand.updateWaitingOn(waitingOn);
- }
- }
- if (executesAt != null)
- if (executesAt instanceof TxnInfo) executesAt = ((TxnInfo) executesAt).asPlainTxnId();
-
- newPendingRecord = new Unmanaged(APPLY, command.txnId(), executesAt);
- }
- else newPendingRecord = new Unmanaged(COMMIT, command.txnId(), txnIds.get(txnIds.size() - 1));
- Unmanaged[] newPending = SortedArrays.insert(unmanageds, newPendingRecord, Unmanaged[]::new);
- return new CommandsForKey(key, redundantBefore, newTxns, newPending);
- }
- }
-
- removeWaitingOn(safeStore, safeCommand, key, true);
- return this;
- }
-
- // minVisitIndex is the index we guarantee to visit from contiguously, i.e. ignoring any jumps backwards due to executeAt
- private void notify(SafeCommandStore safeStore, Kinds kinds, Timestamp from, Timestamp to)
- {
- int start = SortedArrays.binarySearch(committed, 0, committed.length, from, (f, v) -> f.compareTo(v.executeAt), FAST);
- if (start < 0) start = -1 - start;
- int end = SortedArrays.binarySearch(committed, start, committed.length, to, (f, v) -> f.compareTo(v.executeAt), FAST);
- if (end < 0) end = -1 - end;
- else ++end;
-
- notify(safeStore, kinds, start, end);
- }
-
- private void notify(SafeCommandStore safeStore, Kinds kinds, int start, int end)
- {
- Participants<?> asParticipants = null;
- int uncommittedIndex = minUncommitted == null ? txns.length : -1;
- int unappliedReadCount = 0, unappliedWriteCount = 0, unappliedSyncPointCount = 0;
-
- for (int i = start ; i < end ; ++i)
- {
- TxnInfo txn = committed[i];
- Kind kind = txn.kind();
- switch (txn.status)
- {
- case APPLIED:
- case INVALID_OR_TRUNCATED:
- continue;
-
- case COMMITTED:
- // cannot execute as dependencies not stable, so notify progress log to get or decide stable deps
- if (kinds.test(kind))
- {
- if (asParticipants == null)
- asParticipants = Keys.of(key).toParticipants();
- TxnId txnId = new TxnId(txn);
- safeStore.progressLog().waiting(txnId, WaitingToExecute, null, asParticipants);
- }
- break;
-
- case STABLE:
- if (uncommittedIndex < 0)
- uncommittedIndex = SortedArrays.binarySearch(txns, 0, txns.length, minUncommitted, Timestamp::compareTo, FAST);
-
- int nextUncommittedIndex = SortedArrays.exponentialSearch(txns, uncommittedIndex, txns.length, txn.executeAt, Timestamp::compareTo, FAST);
- if (nextUncommittedIndex < 0) nextUncommittedIndex = -1 -nextUncommittedIndex;
- while (uncommittedIndex < nextUncommittedIndex)
- {
- TxnInfo backfillTxn = txns[uncommittedIndex++];
- if (backfillTxn.status.compareTo(COMMITTED) >= 0) continue;
- Kind backfillKind = backfillTxn.kind();
- switch (backfillKind)
- {
- default: throw new AssertionError("Unhandled Txn.Kind: " + backfillKind);
- case LocalOnly:
- case EphemeralRead:
- throw illegalState("Invalid Txn.Kind for CommandsForKey: " + backfillKind);
-
- case Write:
- ++unappliedWriteCount;
- break;
-
- case Read:
- ++unappliedReadCount;
- break;
-
- case SyncPoint:
- case ExclusiveSyncPoint:
- ++unappliedSyncPointCount;
- break;
- }
- }
-
- int expectMissingCount = 0;
- switch (txn.kind())
- {
- default: throw new AssertionError("Unhandled Txn.Kind: " + kind);
- case LocalOnly:
- case EphemeralRead:
- throw illegalState("Invalid Txn.Kind for CommandsForKey: " + kind);
-
- case SyncPoint:
- case ExclusiveSyncPoint:
- expectMissingCount += unappliedSyncPointCount;
-
- case Write:
- expectMissingCount += unappliedReadCount;
-
- case Read:
- expectMissingCount += unappliedWriteCount;
- }
-
- // We remove committed transactions from the missing set, since they no longer need them there
- // So the missing collection represents only those uncommitted transaction ids that a transaction
- // witnesses/conflicts with. So we may simply count all of those we know of with a lower TxnId,
- // and if the count is the same then we are not awaiting any of them for execution and can remove
- // this command's dependency on this key for execution.
- TxnId[] missing = txn.missing();
- int missingCount = missing.length;
- if (missingCount > 0 && minUncommitted != null)
- {
- int missingFrom = SortedArrays.binarySearch(missing, 0, missing.length, minUncommitted, TxnId::compareTo, FAST);
- if (missingFrom < 0) missingFrom = -1 - missingFrom;
- missingCount -= missingFrom;
- }
- if (expectMissingCount == missingCount)
- {
- TxnId txnId = new TxnId(txn);
- removeWaitingOn(safeStore, txnId, key);
- if (asParticipants == null)
- asParticipants = Keys.of(key).toParticipants();
- safeStore.progressLog().waiting(txnId, WaitingToApply, null, asParticipants);
- }
- }
-
- switch (kind)
- {
- default: throw new AssertionError("Unhandled Txn.Kind: " + kind);
- case LocalOnly:
- case EphemeralRead:
- throw illegalState("Invalid Txn.Kind for CommandsForKey: " + kind);
-
- case Write:
- ++unappliedWriteCount;
- break;
-
- case Read:
- ++unappliedReadCount;
- break;
-
- case SyncPoint:
- case ExclusiveSyncPoint:
- ++unappliedSyncPointCount;
- break;
- }
- }
- }
-
- private static void removeWaitingOn(SafeCommandStore safeStore, TxnId txnId, Key key)
- {
- SafeCommand safeCommand = safeStore.ifLoadedAndInitialised(txnId);
- if (safeCommand != null) removeWaitingOn(safeStore, safeCommand, key, true);
- else
- {
- PreLoadContext context = PreLoadContext.contextFor(txnId);
- safeStore.commandStore().execute(context, safeStore0 -> removeWaitingOn(safeStore0, safeStore0.get(txnId), key, false))
- .begin(safeStore.agent());
- }
- }
-
- private static void removeWaitingOn(SafeCommandStore safeStore, SafeCommand safeCommand, Key key, boolean executeAsync)
- {
- Commands.removeWaitingOnKeyAndMaybeExecute(safeStore, safeCommand, key, executeAsync);
- }
-
- public CommandsForKey withRedundantBefore(RedundantBefore.Entry newRedundantBefore)
- {
- Invariants.checkArgument(newRedundantBefore.shardRedundantBefore().compareTo(shardRedundantBefore()) >= 0, "Expect new RedundantBefore.Entry shardAppliedOrInvalidatedBefore to be ahead of existing one");
-
- if (newRedundantBefore.equals(redundantBefore))
- return this;
-
- TxnInfo[] newTxns = txns;
- if (newRedundantBefore.shardRedundantBefore().compareTo(shardRedundantBefore()) >= 0)
- {
- int pos = insertPos(0, newRedundantBefore.shardRedundantBefore());
- if (pos != 0)
- {
- newTxns = Arrays.copyOfRange(txns, pos, txns.length);
- for (int i = 0 ; i < newTxns.length ; ++i)
- {
- TxnInfo txn = newTxns[i];
- TxnId[] missing = txn.missing();
- if (missing == NO_TXNIDS) continue;
- int j = Arrays.binarySearch(missing, newRedundantBefore.shardRedundantBefore());
- if (j < 0) j = -1 - j;
- if (j <= 0) continue;
- missing = j == missing.length ? NO_TXNIDS : Arrays.copyOfRange(missing, j, missing.length);
- newTxns[i] = txn.update(missing);
- }
- }
- }
-
- // TODO (expected): filter pending unmanageds
- return new CommandsForKey(key, newRedundantBefore, newTxns, unmanageds);
- }
-
- public CommandsForKey registerHistorical(TxnId txnId)
- {
- if (txnId.compareTo(shardRedundantBefore()) < 0)
- return this;
-
- int i = Arrays.binarySearch(txns, txnId);
- if (i >= 0)
- return txns[i].status.compareTo(HISTORICAL) >= 0 ? this : update(i, txnId, txns[i], TxnInfo.create(txnId, HISTORICAL, txnId));
-
- return insert(-1 - i, txnId, TxnInfo.create(txnId, HISTORICAL, txnId));
- }
-
- private int insertPos(int min, Timestamp timestamp)
- {
- int i = Arrays.binarySearch(txns, min, txns.length, timestamp);
- if (i < 0) i = -1 -i;
- return i;
- }
-
- public TxnId findFirst()
- {
- return txns.length > 0 ? txns[0] : null;
- }
-
- @Override
- public boolean equals(Object o)
- {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- CommandsForKey that = (CommandsForKey) o;
- return Objects.equals(key, that.key)
- && Arrays.equals(txns, that.txns)
- && Arrays.equals(unmanageds, that.unmanageds);
- }
-
- @Override
- public int hashCode()
- {
- throw new UnsupportedOperationException();
- }
-}
diff --git a/accord-core/src/main/java/accord/local/PreLoadContext.java b/accord-core/src/main/java/accord/local/PreLoadContext.java
index 45de8ff3..25c68efd 100644
--- a/accord-core/src/main/java/accord/local/PreLoadContext.java
+++ b/accord-core/src/main/java/accord/local/PreLoadContext.java
@@ -19,10 +19,13 @@
package accord.local;
import accord.api.Key;
+import accord.local.cfk.CommandsForKey;
import accord.primitives.Keys;
import accord.primitives.Seekables;
import accord.primitives.TxnId;
import com.google.common.collect.Iterators;
+
+import accord.utils.Invariants;
import net.nicoulaj.compilecommand.annotations.Inline;
import com.google.common.collect.Sets;
@@ -134,6 +137,7 @@ public interface PreLoadContext
static PreLoadContext contextFor(TxnId primary, Collection<TxnId> additional, Seekables<?, ?> keys, KeyHistory keyHistory)
{
+ Invariants.checkState(!additional.contains(primary));
return new PreLoadContext()
{
@Override
diff --git a/accord-core/src/main/java/accord/local/RedundantBefore.java b/accord-core/src/main/java/accord/local/RedundantBefore.java
index 0189e74c..dbdfa000 100644
--- a/accord-core/src/main/java/accord/local/RedundantBefore.java
+++ b/accord-core/src/main/java/accord/local/RedundantBefore.java
@@ -109,6 +109,8 @@ public class RedundantBefore extends ReducingRangeMap<RedundantBefore.Entry>
this.shardAppliedOrInvalidatedBefore = shardAppliedOrInvalidatedBefore;
this.bootstrappedAt = bootstrappedAt;
this.staleUntilAtLeast = staleUntilAtLeast;
+ Invariants.checkArgument(locallyAppliedOrInvalidatedBefore.equals(TxnId.NONE) || locallyAppliedOrInvalidatedBefore.domain().isRange());
+ Invariants.checkArgument(shardAppliedOrInvalidatedBefore.equals(TxnId.NONE) || shardAppliedOrInvalidatedBefore.domain().isRange());
}
public static Entry reduce(Entry a, Entry b)
@@ -460,9 +462,9 @@ public class RedundantBefore extends ReducingRangeMap<RedundantBefore.Entry>
return staleRanges;
}
- static class Builder extends AbstractIntervalBuilder<RoutingKey, Entry, RedundantBefore>
+ public static class Builder extends AbstractIntervalBuilder<RoutingKey, Entry, RedundantBefore>
{
- protected Builder(boolean inclusiveEnds, int capacity)
+ public Builder(boolean inclusiveEnds, int capacity)
{
super(inclusiveEnds, capacity);
}
diff --git a/accord-core/src/main/java/accord/local/SafeCommand.java b/accord-core/src/main/java/accord/local/SafeCommand.java
index 0a0cb6e3..dbc4b54b 100644
--- a/accord-core/src/main/java/accord/local/SafeCommand.java
+++ b/accord-core/src/main/java/accord/local/SafeCommand.java
@@ -56,11 +56,7 @@ public abstract class SafeCommand
return txnId;
}
- // TODO (expected): it isn't ideal to pass keysOrRanges for the special case of Accept. We can either:
- // 1 - remove the special case that permits accept without the definition
- // 2 - store some pseudo transaction with only the keys
- // 3 - just come up with something a bit neater
- <C extends Command> C update(SafeCommandStore safeStore, C update)
+ public <C extends Command> C update(SafeCommandStore safeStore, C update)
{
Command prev = current();
if (prev == update)
diff --git a/accord-core/src/main/java/accord/local/SafeCommandStore.java b/accord-core/src/main/java/accord/local/SafeCommandStore.java
index 7aea6790..0cea6eb3 100644
--- a/accord-core/src/main/java/accord/local/SafeCommandStore.java
+++ b/accord-core/src/main/java/accord/local/SafeCommandStore.java
@@ -27,6 +27,8 @@ import accord.api.Key;
import accord.api.ProgressLog;
import accord.api.RoutingKey;
import accord.impl.ErasedSafeCommand;
+import accord.local.cfk.CommandsForKey;
+import accord.local.cfk.SafeCommandsForKey;
import accord.primitives.Deps;
import accord.primitives.EpochSupplier;
import accord.primitives.Keys;
@@ -141,7 +143,7 @@ public abstract class SafeCommandStore
*
* This permits efficient operation when a transaction involved in processing another transaction happens to be in memory.
*/
- public final SafeCommand ifLoadedAndInitialised(TxnId txnId)
+ public SafeCommand ifLoadedAndInitialised(TxnId txnId)
{
SafeCommand safeCommand = getInternalIfLoadedAndInitialised(txnId);
if (safeCommand == null)
@@ -155,6 +157,11 @@ public abstract class SafeCommandStore
return maybeTruncate(safeCommand, safeCommand.current(), null, null);
}
+ public SafeCommand unsafeGet(TxnId txnId)
+ {
+ return get(txnId);
+ }
+
protected SafeCommandsForKey maybeTruncate(SafeCommandsForKey safeCfk)
{
RedundantBefore.Entry entry = commandStore().redundantBefore().get(safeCfk.key().toUnseekable());
@@ -194,11 +201,6 @@ public abstract class SafeCommandStore
protected abstract SafeCommandsForKey getInternalIfLoadedAndInitialised(Key key);
public abstract boolean canExecuteWith(PreLoadContext context);
- public void load(Command loaded)
- {
- update(null, loaded);
- }
-
protected void update(Command prev, Command updated)
{
updateMaxConflicts(prev, updated);
@@ -219,58 +221,64 @@ public abstract class SafeCommandStore
commandStore().updateMaxConflicts(prev, updated);
}
- private void updateCommandsForKey(Command prev, Command updated)
+ private void updateCommandsForKey(Command prev, Command next)
{
- if (!CommandsForKey.needsUpdate(prev, updated))
+ if (!CommandsForKey.needsUpdate(prev, next))
return;
- TxnId txnId = updated.txnId();
- Keys keys;
- if (txnId.domain().isKey() && txnId.kind().isGloballyVisible())
+ TxnId txnId = next.txnId();
+ if (CommandsForKey.manages(txnId)) updateManagedCommandsForKey(this, prev, next);
+ if (!CommandsForKey.managesExecution(txnId) && next.hasBeen(Status.Stable) && !next.hasBeen(Status.Truncated) && !prev.hasBeen(Status.Stable))
+ updateUnmanagedExecutionCommandsForKey(this, next);
+ }
+
+ private static void updateManagedCommandsForKey(SafeCommandStore safeStore, Command prev, Command next)
+ {
+ TxnId txnId = next.txnId();
+ Keys keys = (Keys)next.keysOrRanges();
+ if (keys == null || next.hasBeen(Status.Truncated)) keys = (Keys)prev.keysOrRanges();
+ if (keys == null)
+ return;
+
+ // TODO (required): additionalKeysOrRanges may not be being handled entirely correctly here, though it may not matter.
+ // Once committed without a given key, we should be effectively erasing the command from that CFK
+ PreLoadContext context = PreLoadContext.contextFor(txnId, keys, COMMANDS);
+ // TODO (expected): execute immediately for any keys we already have loaded, and save only those we haven't for later
+ if (safeStore.canExecuteWith(context))
{
- keys = (Keys)updated.keysOrRanges();
- if (keys == null || updated.hasBeen(Status.Truncated)) keys = (Keys)prev.keysOrRanges();
- if (keys == null)
- return;
-
- PreLoadContext context = PreLoadContext.contextFor(txnId, keys, COMMANDS);
- // TODO (expected): execute immediately for any keys we already have loaded, and save only those we haven't for later
- if (canExecuteWith(context))
+ for (Key key : keys)
{
- for (Key key : keys)
- {
- get(key).update(this, prev, updated);
- }
- }
- else
- {
- commandStore().execute(context, safeStore -> safeStore.updateCommandsForKey(prev, updated))
- .begin(commandStore().agent);
+ safeStore.get(key).update(safeStore, next);
}
}
else
{
- if (!updated.hasBeen(Status.Stable) || prev.hasBeen(Status.Stable) || updated.hasBeen(Status.Truncated))
- return;
-
- keys = updated.asCommitted().waitingOn.keys;
- // TODO (required): consider how execution works for transactions that await future deps and where the command store inherits additional keys in execution epoch
- Ranges ranges = ranges().allAt(updated.executeAt());
- PreLoadContext context = PreLoadContext.contextFor(txnId, keys, COMMANDS);
- // TODO (expected): execute immediately for any keys we already have loaded, and save only those we haven't for later
- if (canExecuteWith(context))
- {
- Routables.foldl(keys, ranges, (self, t, key, o, i) -> {
- self.get(key).registerUnmanaged(self, self.get(t));
- return null;
- }, this, txnId, null, i->false);
- }
- else
- {
- commandStore().execute(context, safeStore -> safeStore.updateCommandsForKey(prev, updated))
- .begin(commandStore().agent);
- }
+ safeStore = safeStore; // prevent accidental usage inside lambda
+ safeStore.commandStore().execute(context, safeStore0 -> updateManagedCommandsForKey(safeStore0, prev, next))
+ .begin(safeStore.commandStore().agent);
+ }
+ }
+ private static void updateUnmanagedExecutionCommandsForKey(SafeCommandStore safeStore, Command next)
+ {
+ TxnId txnId = next.txnId();
+ Keys keys = next.asCommitted().waitingOn.keys;
+ // TODO (required): consider how execution works for transactions that await future deps and where the command store inherits additional keys in execution epoch
+ Ranges ranges = safeStore.ranges().allAt(next.executeAt());
+ PreLoadContext context = PreLoadContext.contextFor(txnId, keys, COMMANDS);
+ // TODO (expected): execute immediately for any keys we already have loaded, and save only those we haven't for later
+ if (safeStore.canExecuteWith(context))
+ {
+ Routables.foldl(keys, ranges, (ss, t, key, o, i) -> {
+ ss.get(key).registerUnmanaged(ss, ss.get(t));
+ return null;
+ }, safeStore, txnId, null, i->false);
+ }
+ else
+ {
+ safeStore = safeStore;
+ safeStore.commandStore().execute(context, safeStore0 -> updateUnmanagedExecutionCommandsForKey(safeStore0, next))
+ .begin(safeStore.commandStore().agent);
}
}
diff --git a/accord-core/src/main/java/accord/local/SafeCommandsForKey.java b/accord-core/src/main/java/accord/local/SafeCommandsForKey.java
deleted file mode 100644
index b4e74e4d..00000000
--- a/accord-core/src/main/java/accord/local/SafeCommandsForKey.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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 accord.local;
-
-import accord.api.Key;
-import accord.impl.SafeState;
-import accord.primitives.TxnId;
-
-import static accord.local.CommandsForKey.InternalStatus.HISTORICAL;
-
-public abstract class SafeCommandsForKey implements SafeState<CommandsForKey>
-{
- private final Key key;
-
- public SafeCommandsForKey(Key key)
- {
- this.key = key;
- }
-
- protected abstract void set(CommandsForKey update);
-
- public Key key()
- {
- return key;
- }
-
- CommandsForKey update(SafeCommandStore safeStore, Command prevCommand, Command nextCommand)
- {
- CommandsForKey prevCfk = current();
- CommandsForKey nextCfk = prevCfk.update(prevCommand, nextCommand);
- if (nextCfk == prevCfk)
- return prevCfk;
- set(nextCfk.notifyAndUpdatePending(safeStore, nextCommand, prevCfk));
- return nextCfk;
- }
-
- CommandsForKey update(SafeCommandStore safeStore, Command nextCommand)
- {
- CommandsForKey prevCfk = current();
- CommandsForKey nextCfk = prevCfk.update(null, nextCommand, false);
- if (nextCfk == prevCfk)
- return prevCfk;
- set(nextCfk.notifyAndUpdatePending(safeStore, nextCommand, prevCfk));
- return nextCfk;
- }
-
- CommandsForKey registerUnmanaged(SafeCommandStore safeStore, SafeCommand unmanaged)
- {
- CommandsForKey prev = current();
- CommandsForKey next = prev.registerUnmanaged(safeStore, unmanaged);
- if (next != prev)
- set(next.notifyAndUpdatePending(safeStore, prev));
- return next;
- }
-
- public CommandsForKey registerHistorical(SafeCommandStore safeStore, TxnId txnId)
- {
- CommandsForKey prev = current();
- CommandsForKey next = prev.registerHistorical(txnId);
- if (next != prev)
- set(next.notifyAndUpdatePending(safeStore, txnId, HISTORICAL, txnId, prev));
- return next;
- }
-
- public CommandsForKey updateRedundantBefore(SafeCommandStore safeStore, RedundantBefore.Entry redundantBefore)
- {
- CommandsForKey prev = current();
- CommandsForKey next = prev.withRedundantBefore(redundantBefore);
- if (next != prev)
- set(next.notifyAndUpdatePending(safeStore, prev));
- return next;
- }
-
- public void initialize()
- {
- set(new CommandsForKey(key));
- }
-
- public void refresh(SafeCommandStore safeStore)
- {
- updateRedundantBefore(safeStore, safeStore.commandStore().redundantBefore().get(key));
- }
-}
diff --git a/accord-core/src/main/java/accord/local/Status.java b/accord-core/src/main/java/accord/local/Status.java
index 3719993c..c80918fa 100644
--- a/accord-core/src/main/java/accord/local/Status.java
+++ b/accord-core/src/main/java/accord/local/Status.java
@@ -159,7 +159,7 @@ public enum Status
public Known atLeast(Known that)
{
- // TODO (expected): validate no inconsistent combos
+ Invariants.checkArgument(compatibleWith(that));
KnownRoute maxRoute = route.atLeast(that.route);
Definition maxDefinition = definition.atLeast(that.definition);
KnownExecuteAt maxExecuteAt = executeAt.atLeast(that.executeAt);
@@ -168,8 +168,25 @@ public enum Status
return selectOrCreate(that, maxRoute, maxDefinition, maxExecuteAt, maxDeps, maxOutcome);
}
+ public Known min(Known that)
+ {
+ Invariants.checkArgument(compatibleWith(that));
+ KnownRoute minRoute = min(route, that.route);
+ Definition minDefinition = min(definition, that.definition);
+ KnownExecuteAt minExecuteAt = min(executeAt, that.executeAt);
+ KnownDeps minDeps = min(deps, that.deps);
+ Outcome minOutcome = min(outcome, that.outcome);
+ return selectOrCreate(that, minRoute, minDefinition, minExecuteAt, minDeps, minOutcome);
+ }
+
+ static <E extends Enum<E>> E min(E a, E b)
+ {
+ return a.compareTo(b) <= 0 ? a : b;
+ }
+
public Known reduce(Known that)
{
+ Invariants.checkArgument(compatibleWith(that));
KnownRoute maxRoute = route.reduce(that.route);
Definition minDefinition = definition.reduce(that.definition);
KnownExecuteAt maxExecuteAt = executeAt.reduce(that.executeAt);
@@ -188,6 +205,13 @@ public enum Status
return selectOrCreate(maxRoute, minDefinition, maxExecuteAt, minDeps, maxOutcome);
}
+ boolean compatibleWith(Known that)
+ {
+ return executeAt.compatibleWith(that.executeAt)
+ && deps.compatibleWith(that.deps)
+ && outcome.compatibleWith(that.outcome);
+ }
+
@Nonnull
private Known selectOrCreate(Known with, KnownRoute maxRoute, Definition maxDefinition, KnownExecuteAt maxExecuteAt, KnownDeps maxDeps, Outcome maxOutcome)
{
@@ -320,6 +344,10 @@ public enum Status
return definition.isKnown();
}
+ // TODO (expected): this isn't exactly correct, though it is probably fine:
+ // outcome.isDecided includes Erased and Invalidated; Invalidated is definitely fine because we only use this to infer invalidation
+ // whereas for Erased, we are inaccurate in the safer direction (saying it is Decided to avoid inferring Invalidated when we don't know),
+ // but it would be good to tidy this up - either by clarifying the semantics or the naming
public boolean hasDefinitionBeenKnown()
{
return definition.isKnown() || outcome.isDecided();
@@ -534,6 +562,15 @@ public enum Status
{
return this == ExecuteAtUnknown;
}
+
+ public boolean compatibleWith(KnownExecuteAt that)
+ {
+ if (this == that) return true;
+ int c = compareTo(that);
+ KnownExecuteAt max = c >= 0 ? this : that;
+ KnownExecuteAt min = c <= 0 ? this : that;
+ return max != NoExecuteAt || min != ExecuteAtKnown;
+ }
}
public enum KnownDeps
@@ -633,6 +670,15 @@ public enum Status
{
return this == NoDeps ? NoDeps : DepsUnknown;
}
+
+ public boolean compatibleWith(KnownDeps that)
+ {
+ if (this == that) return true;
+ int c = compareTo(that);
+ KnownDeps max = c >= 0 ? this : that;
+ KnownDeps min = c <= 0 ? this : that;
+ return max != NoDeps || (min != DepsCommitted && min != DepsKnown);
+ }
}
/**
@@ -646,7 +692,7 @@ public enum Status
DefinitionUnknown,
/**
- * The definition was known, but has been erased
+ * The definition was perhaps known previously, but has since been erased
*/
DefinitionErased,
@@ -786,6 +832,15 @@ public enum Status
{
return this != Unknown;
}
+
+ public boolean compatibleWith(Outcome that)
+ {
+ if (this == that) return true;
+ int c = compareTo(that);
+ Outcome max = c >= 0 ? this : that;
+ Outcome min = c <= 0 ? this : that;
+ return max != Invalidated || (min != Apply && min != WasApply);
+ }
}
public enum LogicalEpoch
diff --git a/accord-core/src/main/java/accord/local/cfk/CommandsForKey.java b/accord-core/src/main/java/accord/local/cfk/CommandsForKey.java
new file mode 100644
index 00000000..162b4fe8
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/CommandsForKey.java
@@ -0,0 +1,1471 @@
+/*
+ * 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 accord.local.cfk;
+
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import accord.api.Key;
+
+import accord.api.VisibleForImplementation;
+import accord.impl.CommandsSummary;
+import accord.local.Command;
+import accord.local.RedundantBefore;
+import accord.local.SafeCommand;
+import accord.local.SafeCommandStore;
+import accord.local.SafeCommandStore.CommandFunction;
+import accord.local.SafeCommandStore.TestDep;
+import accord.local.SafeCommandStore.TestStartedAt;
+import accord.local.SafeCommandStore.TestStatus;
+import accord.local.SaveStatus;
+import accord.local.Status;
+import accord.primitives.Ballot;
+import accord.primitives.Keys;
+import accord.primitives.Participants;
+import accord.primitives.Timestamp;
+import accord.primitives.Txn.Kind;
+import accord.primitives.Txn.Kind.Kinds;
+import accord.primitives.TxnId;
+import accord.utils.Invariants;
+import accord.utils.SortedArrays;
+import accord.utils.btree.BTree;
+
+import static accord.local.cfk.CommandsForKey.InternalStatus.ACCEPTED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.APPLIED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.COMMITTED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.PREACCEPTED_OR_ACCEPTED_INVALIDATE;
+import static accord.local.cfk.CommandsForKey.InternalStatus.STABLE;
+import static accord.local.cfk.CommandsForKey.InternalStatus.HISTORICAL;
+import static accord.local.cfk.CommandsForKey.InternalStatus.INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.TRANSITIVELY_KNOWN;
+import static accord.local.cfk.Pruning.isWaitingOnPruned;
+import static accord.local.cfk.Pruning.loadingPrunedFor;
+import static accord.local.cfk.Updating.insertOrUpdate;
+import static accord.local.SafeCommandStore.TestDep.ANY_DEPS;
+import static accord.local.SafeCommandStore.TestDep.WITH;
+import static accord.local.SaveStatus.LocalExecution.WaitingToApply;
+import static accord.local.SaveStatus.LocalExecution.WaitingToExecute;
+import static accord.local.cfk.Utils.removeRedundantMissing;
+import static accord.primitives.Txn.Kind.Kinds.AnyGloballyVisible;
+import static accord.primitives.Txn.Kind.Write;
+import static accord.utils.Invariants.Paranoia.LINEAR;
+import static accord.utils.Invariants.Paranoia.NONE;
+import static accord.utils.Invariants.Paranoia.SUPERLINEAR;
+import static accord.utils.Invariants.ParanoiaCostFactor.LOW;
+import static accord.utils.Invariants.checkNonNegative;
+import static accord.utils.Invariants.illegalState;
+import static accord.utils.Invariants.isParanoid;
+import static accord.utils.Invariants.testParanoia;
+import static accord.utils.SortedArrays.Search.FAST;
+
+/**
+ * <h2>Introduction</h2>
+ * A specialised collection for efficiently representing and querying everything we need for making coordination
+ * and recovery decisions about a key's command conflicts.
+ *
+ * Every command we know about that is not shard-redundant is listed in the {@code byId} collection, which is sorted by {@code TxnId}.
+ * This includes all transitive dependencies we have witnessed via other transactions, but not witnessed directly.
+ *
+ * <h2>Contents</h2>
+ * This collection tracks various transactions differently:
+ * - Range transactions are tracked ONLY as dependencies; if no managed key transactions witness a range transaction
+ * it will not be tracked here. The dependencies of range transactions are not themselves tracked at all.
+ * A range transaction that depends on some key for execution will be registered as an unmanaged transaction
+ * to track when it may be executed.
+ * - Key (Exclusive)?SyncPoints are tracked fully until execution; we fully encode their dependencies, and track their lifecycle.
+ * This permits them to be consulted for recovery. Once they are stable, they will be registered as unmanaged transactions for execution.
+ * - Key Reads and Writes are first class citizens. We fully encode their dependencies, track their lifecycle and also
+ * directly manage their execution.
+ *
+ * <h2>Dependency Encoding</h2>
+ * The byId list implies the contents of the deps of all commands in the collection - that is, it is assumed that in
+ * the normal course of events every transaction will include the full set of {@code TxnId} we know that would be
+ * witnessed by the command. We only encode divergences from this, stored in each command's {@code missing} collection.
+ *
+ * We then go one step further, exploiting the fact that the missing collection exists solely to implement recovery,
+ * and so we elide from this missing collection any {@code TxnId} we have recorded as {@code Committed} or higher.
+ * Any recovery coordinator that contacts this replica will report that the command has been agreed to execute,
+ * and so will not need to decipher any fast-path decisions. So the missing collection is redundant, as no command's deps
+ * will need to be queried for this TxnId's presence/absence.
+ * TODO (expected) this logic applies equally well to Accepted
+ *
+ * The goal with these behaviours is that this missing collection will ordinarily be empty, occupying no space.
+ *
+ * <h2>Garbage Collection</h2>
+ * This collection is trimmed by two mechanisms: pruning applied transactions and removing redundant transactions.
+ *
+ * 1) redundantBefore represents the global lower bound covering the key for transactions all being applied or invalidated,
+ * so we know that nothing with a lower TxnId should ever need to be computed, taken as a dependency or recovered. So
+ * we may simply erase these.
+ *
+ * 2) prunedBefore represents a local bound that permits us to optimistically remove data from the CommandsForKey
+ * that may need to be loaded again later. Specifically, we pick an applied {@code TxnId} that we will retain, and we
+ * remove from the {@code CommandsForKey} any transaction with a lower {@code TxnId} and {@code executeAt} that is also
+ * applied or invalidated.
+ *
+ * [We only do this if there also exists some later transactions we are not pruning that collectively have a superset of
+ * its witnessed collection, so that recovery decisions will be unaffected by the removal of the transaction.]
+ *
+ * The complexity here is that, by virtue of being a local decision point, we cannot guarantee that no coordinator will
+ * contact us in future with either a new TxnId that is lower than this, or a dependency collection containing a TxnId
+ * we have already processed.
+ *
+ * [We pick a TxnId stale by some time bound so that we can expect that any earlier already-applied TxnId will
+ * not be included in a future set of dependencies - we expect that "transitive dependency elision" will ordinarily filter
+ * it; but it might not on all replicas.]
+ *
+ * The difficulty is that we cannot immediately distinguish these two cases, and so on encountering a TxnId that is
+ * less than our {@code prunedBefore} we must load the local command state for the TxnId. If we have not witnessed the
+ * TxnId then we know it is a new transitive dependency. If we have witnessed it, and it is applied, then we load it
+ * into the {@code CommandsForKey} until we next prune to avoid reloading it repeatedly. This is managed with the
+ * {@link #loadingPruned} btree collection.
+ *
+ * <h2>Transitive Dependency Elision</h2>
+ * {@code CommandsForKey} also implements transitive dependency elision.
+ * When evaluating {@code mapReduceActive}, we first establish the last-executing Stable write command (i.e. those whose deps
+ * are considered durably decided, and so must wait for all commands {@code Committed} with a lower {@code executeAt}).
+ * We then elide any {@code Committed} command that has a lower executeAt than this command that would be witnessed by that command.
+ *
+ * Both commands must be known at a majority, but neither might be {@code Committed} at any other replica.
+ * Either command may therefore be recovered.
+ * If the later command is recovered, this replica will report its Stable deps, thereby recovering them.
+ * If this replica is not contacted, some other replica must participate that either has taken the same action as this replica,
+ * or else does not know the later command is Stable, and so will report the earlier command as a dependency again.
+ * If the earlier command is recovered, this replica will report that it is {@code Committed}, and so will not consult
+ * this replica's collection to decipher any fast path decision. Any other replica must either do the same, or else
+ * will correctly record this transaction as present in any relevant deps of later transactions.
+ *
+ * TODO (expected): maintain separate redundantBefore and closedBefore timestamps, latter implied by any exclusivesyncpoint;
+ * advance former based on Applied status of all TxnId before the latter
+ * TODO (desired): track whether a TxnId is a write on this key only for execution (rather than globally)
+ * TODO (expected): merge with TimestampsForKey
+ * TODO (desired): save space by encoding InternalStatus in TxnId.flags(), so that when executeAt==txnId we can save 8 bytes per entry
+ * TODO (expected): remove a command that is committed to not intersect with the key for this store (i.e. if accepted in a later epoch than committed on, so ownership changes)
+ * TODO (expected): avoid updating transactions we don't manage the execution of - perhaps have a dedicated InternalStatus
+ * TODO (expected): minimise repeated notification, either by logic or marking a command as notified once ready-to-execute
+ * TODO (required): linearizability violation detection
+ * TODO (desired): introduce a new status or other fast and simple mechanism for filtering treatment of range or unmanaged transactions
+ */
+public class CommandsForKey extends CommandsForKeyUpdate implements CommandsSummary
+{
+ private static final boolean ELIDE_TRANSITIVE_DEPENDENCIES = true;
+
+ public static final RedundantBefore.Entry NO_REDUNDANT_BEFORE = new RedundantBefore.Entry(null, Long.MIN_VALUE, Long.MAX_VALUE, TxnId.NONE, TxnId.NONE, TxnId.NONE, null);
+ public static final TxnId[] NO_TXNIDS = new TxnId[0];
+ public static final TxnInfo NO_INFO = new TxnInfo(TxnId.NONE, HISTORICAL, TxnId.NONE);
+ public static final TxnInfo[] NO_INFOS = new TxnInfo[0];
+ public static final Unmanaged[] NO_PENDING_UNMANAGED = new Unmanaged[0];
+
+ /**
+ * Transactions that are witnessed by {@code CommandsForKey} for dependency management
+ * (essentially all globally visible key transactions).
+ */
+ public static boolean manages(TxnId txnId)
+ {
+ return txnId.domain().isKey() && txnId.kind().isGloballyVisible();
+ }
+
+ /**
+ * Transactions whose execution will be wholly managed by {@code CommandsForKey} (essentially reads and writes).
+ *
+ * Other transactions that depend on these transactions need only adopt a dependency on the {@code Key} to represent
+ * all of these transactions; the {@code CommandsForKey} will then notify when they have executed.
+ */
+ public static boolean managesExecution(TxnId txnId)
+ {
+ return Write.witnesses(txnId.kind()) && txnId.domain().isKey();
+ }
+
+ public static boolean needsUpdate(Command prev, Command updated)
+ {
+ SaveStatus prevStatus;
+ Ballot prevAcceptedOrCommitted;
+ if (prev == null)
+ {
+ prevStatus = SaveStatus.NotDefined;
+ prevAcceptedOrCommitted = Ballot.ZERO;
+ }
+ else
+ {
+ prevStatus = prev.saveStatus();
+ prevAcceptedOrCommitted = prev.acceptedOrCommitted();
+ }
+
+ return needsUpdate(prevStatus, prevAcceptedOrCommitted, updated.saveStatus(), updated.acceptedOrCommitted());
+ }
+
+ public static boolean needsUpdate(SaveStatus prevStatus, Ballot prevAcceptedOrCommitted, SaveStatus updatedStatus, Ballot updatedAcceptedOrCommitted)
+ {
+ InternalStatus prev = InternalStatus.from(prevStatus);
+ InternalStatus updated = InternalStatus.from(updatedStatus);
+ return updated != prev || (updated != null && updated.hasExecuteAtOrDeps && !prevAcceptedOrCommitted.equals(updatedAcceptedOrCommitted));
+ }
+
+ public static class SerializerSupport
+ {
+ public static CommandsForKey create(Key key, TxnInfo[] txns, Unmanaged[] unmanageds, TxnId prunedBefore)
+ {
+ return new CommandsForKey(key, NO_REDUNDANT_BEFORE, prunedBefore, Pruning.LoadingPruned.empty(), txns, unmanageds);
+ }
+ }
+
+ /**
+ * An object representing the basic CommandsForKey state, extending TxnId to save memory and improve locality.
+ */
+ public static class TxnInfo extends TxnId
+ {
+ public final InternalStatus status;
+ public final Timestamp executeAt;
+
+ TxnInfo(TxnId txnId, InternalStatus status, Timestamp executeAt)
+ {
+ super(txnId);
+ this.status = status;
+ this.executeAt = executeAt == txnId ? this : executeAt;
+ }
+
+ public static TxnInfo create(@Nonnull TxnId txnId, InternalStatus status)
+ {
+ return new TxnInfo(txnId, status, txnId);
+ }
+
+ public static TxnInfo create(@Nonnull TxnId txnId, InternalStatus status, Command command)
+ {
+ Timestamp executeAt = txnId;
+ if (status.hasExecuteAt()) executeAt = command.executeAt();
+ Ballot ballot;
+ if (!status.hasBallot || (ballot = command.acceptedOrCommitted()).equals(Ballot.ZERO))
+ return new TxnInfo(txnId, status, executeAt);
+ return new TxnInfoExtra(txnId, status, executeAt, NO_TXNIDS, ballot);
+ }
+
+ public static TxnInfo create(@Nonnull TxnId txnId, InternalStatus status, @Nonnull Timestamp executeAt, @Nonnull Ballot ballot)
+ {
+ Invariants.checkState(executeAt == txnId || !executeAt.equals(txnId));
+ Invariants.checkState(status.hasExecuteAtOrDeps || executeAt == txnId);
+ Invariants.checkState(status.hasBallot || ballot == Ballot.ZERO);
+ if (!status.hasBallot || ballot.equals(Ballot.ZERO)) return new TxnInfo(txnId, status, executeAt);
+ return new TxnInfoExtra(txnId, status, executeAt, NO_TXNIDS, ballot);
+ }
+
+ public static TxnInfo create(@Nonnull TxnId txnId, InternalStatus status, @Nonnull Timestamp executeAt, @Nonnull TxnId[] missing, @Nonnull Ballot ballot)
+ {
+ Invariants.checkState(executeAt == txnId || !executeAt.equals(txnId));
+ Invariants.checkState(status.hasExecuteAtOrDeps || executeAt == txnId);
+ Invariants.checkState(status.hasBallot || ballot == Ballot.ZERO);
+ Invariants.checkState(status.hasExecuteAtOrDeps || missing == NO_TXNIDS);
+ if (missing == NO_TXNIDS && (!status.hasBallot || ballot == Ballot.ZERO))
+ return new TxnInfo(txnId, status, executeAt);
+ Invariants.checkState(missing.length > 0 || missing == NO_TXNIDS);
+ return new TxnInfoExtra(txnId, status, executeAt, missing, ballot);
+ }
+
+ public static TxnInfo createMock(TxnId txnId, InternalStatus status, @Nullable Timestamp executeAt, @Nullable TxnId[] missing, @Nullable Ballot ballot)
+ {
+ Invariants.checkState(executeAt == null || executeAt == txnId || !executeAt.equals(txnId));
+ Invariants.checkArgument(missing == null || missing == NO_TXNIDS);
+ if (missing == NO_TXNIDS && ballot == Ballot.ZERO) return new TxnInfo(txnId, status, executeAt);
+ return new TxnInfoExtra(txnId, status, executeAt, missing, ballot);
+ }
+
+ Timestamp depsKnownBefore()
+ {
+ return status.depsKnownBefore(this, executeAt);
+ }
+
+ Timestamp witnessedAfter()
+ {
+ return status.witnessedAfter(this, executeAt);
+ }
+
+ public TxnInfo update(TxnId[] newMissing)
+ {
+ Invariants.checkState(status.hasExecuteAtOrDeps);
+ return newMissing == NO_TXNIDS
+ ? new TxnInfo(this, status, executeAt)
+ : new TxnInfoExtra(this, status, executeAt, newMissing, Ballot.ZERO);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TxnInfo info = (TxnInfo) o;
+ return status == info.status
+ && (executeAt == this ? info.executeAt == info : Objects.equals(executeAt, info.executeAt))
+ && Arrays.equals(missing(), info.missing());
+ }
+
+ TxnId plainTxnId()
+ {
+ return new TxnId(this);
+ }
+
+ Timestamp plainExecuteAt()
+ {
+ return executeAt == this ? plainTxnId() : executeAt;
+ }
+
+ /**
+ * Any uncommitted transactions the owning CommandsForKey is aware of, that could have been included in our
+ * dependencies but weren't.
+ * <p>
+ * That is to say, any TxnId < depsKnownBefore() we have otherwise witnessed that were not witnessed by this transaction.
+ */
+ public TxnId[] missing()
+ {
+ return NO_TXNIDS;
+ }
+
+ public Ballot ballot()
+ {
+ return Ballot.ZERO;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Info{" +
+ "txnId=" + toPlainString() +
+ ", status=" + status +
+ ", executeAt=" + plainExecuteAt() +
+ '}';
+ }
+
+ public String toPlainString()
+ {
+ return super.toString();
+ }
+
+ public int compareExecuteAt(TxnInfo that)
+ {
+ return this.executeAt.compareTo(that.executeAt);
+ }
+
+ Timestamp executeAtIfKnownElseTxnId()
+ {
+ return status == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED ? this : executeAt;
+ }
+ }
+
+ public static class TxnInfoExtra extends TxnInfo
+ {
+ /**
+ * {@link TxnInfo#missing()}
+ */
+ public final TxnId[] missing;
+ public final Ballot ballot;
+
+ TxnInfoExtra(TxnId txnId, InternalStatus status, Timestamp executeAt, TxnId[] missing, Ballot ballot)
+ {
+ super(txnId, status, executeAt);
+ this.missing = missing;
+ this.ballot = ballot;
+ }
+
+ /**
+ * {@link TxnInfo#missing()}
+ */
+ @Override
+ public TxnId[] missing()
+ {
+ return missing;
+ }
+
+ @Override
+ public Ballot ballot()
+ {
+ return ballot;
+ }
+
+ public TxnInfo update(TxnId[] newMissing)
+ {
+ if (newMissing == missing)
+ return this;
+
+ return newMissing == NO_TXNIDS && ballot == Ballot.ZERO
+ ? new TxnInfo(this, status, executeAt)
+ : new TxnInfoExtra(this, status, executeAt, newMissing, ballot);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Info{" +
+ "txnId=" + toPlainString() +
+ ", status=" + status +
+ ", executeAt=" + plainExecuteAt() +
+ (ballot != Ballot.ZERO ? ", ballot=" + ballot : "") +
+ ", missing=" + Arrays.toString(missing) +
+ '}';
+ }
+ }
+
+ /**
+ * A transaction whose key-dependencies for execution are not natively managed by this class.
+ * This essentially supports all range commands and key sync points for managing their key execution dependencies.
+ * <p>
+ * We maintain a sorted list of waiting transactions; we gate this by simple TxnId and executeAt bounds in two phases:
+ * 1) pick the highest dependency TxnId on the key; wait for all <= TxnId to commit
+ * 2) pick the highest executeAt dependency on the key that executes before the Unmanaged txn, and wait for it and all earlier txn to Apply
+ *
+ * NOTE: this can create false dependencies on commit phase for earlier TxnId, which could delay transaction execution.
+ * This is probably an acceptable trade-off given this vastly simplifies execution and reduces the state we have to manage;
+ * we mostly use this for sync point execution; and because Commit should always complete promptly.
+ * However, we could improve this in future if we want to, by integrating this execution logic with the execution
+ * of regular reads and writes now we anyway often represent these transactions in the TxnInfo collections.
+ */
+ public static class Unmanaged implements Comparable<Unmanaged>
+ {
+ public enum Pending { COMMIT, APPLY }
+
+ public final Pending pending;
+ public final Timestamp waitingUntil;
+ public final TxnId txnId;
+
+ public Unmanaged(Pending pending, TxnId txnId, Timestamp waitingUntil)
+ {
+ this.pending = pending;
+ this.txnId = txnId;
+ this.waitingUntil = waitingUntil;
+ }
+
+ @Override
+ public int compareTo(Unmanaged that)
+ {
+ if (this.pending != that.pending) return this.pending.compareTo(that.pending);
+ int c = this.waitingUntil.compareTo(that.waitingUntil);
+ if (c == 0) c = this.txnId.compareTo(that.txnId);
+ return c;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Unmanaged unmanaged = (Unmanaged) o;
+ return pending == unmanaged.pending && waitingUntil.equals(unmanaged.waitingUntil) && txnId.equals(unmanaged.txnId);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(pending, waitingUntil, txnId);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Pending{" + txnId + " until:" + waitingUntil + " " + pending + "}";
+ }
+ }
+
+ public enum InternalStatus
+ {
+ TRANSITIVELY_KNOWN(false, false), // (unwitnessed; no need for mapReduce to witness)
+ HISTORICAL(false, false),
+ PREACCEPTED_OR_ACCEPTED_INVALIDATE(false, true),
+ ACCEPTED(true, true),
+ COMMITTED(true, true),
+ STABLE(true, false),
+ APPLIED(true, false),
+ INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED(false, false);
+
+ static final EnumMap<SaveStatus, InternalStatus> convert = new EnumMap<>(SaveStatus.class);
+ static final InternalStatus[] VALUES = values();
+
+ static
+ {
+ convert.put(SaveStatus.PreAccepted, PREACCEPTED_OR_ACCEPTED_INVALIDATE);
+ convert.put(SaveStatus.AcceptedInvalidateWithDefinition, PREACCEPTED_OR_ACCEPTED_INVALIDATE);
+ convert.put(SaveStatus.Accepted, ACCEPTED);
+ convert.put(SaveStatus.AcceptedWithDefinition, ACCEPTED);
+ convert.put(SaveStatus.PreCommittedWithDefinition, PREACCEPTED_OR_ACCEPTED_INVALIDATE);
+ convert.put(SaveStatus.PreCommittedWithAcceptedDeps, ACCEPTED);
+ convert.put(SaveStatus.PreCommittedWithDefinitionAndAcceptedDeps, ACCEPTED);
+ convert.put(SaveStatus.Committed, COMMITTED);
+ convert.put(SaveStatus.Stable, STABLE);
+ convert.put(SaveStatus.ReadyToExecute, STABLE);
+ convert.put(SaveStatus.PreApplied, STABLE);
+ convert.put(SaveStatus.Applying, STABLE);
+ convert.put(SaveStatus.Applied, APPLIED);
+ convert.put(SaveStatus.TruncatedApplyWithDeps, INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED);
+ convert.put(SaveStatus.TruncatedApplyWithOutcome, INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED);
+ convert.put(SaveStatus.TruncatedApply, INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED);
+ convert.put(SaveStatus.ErasedOrInvalidated, INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED);
+ convert.put(SaveStatus.Erased, INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED);
+ convert.put(SaveStatus.Invalidated, INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED);
+ }
+
+ public final boolean hasInfo;
+ public final boolean hasExecuteAtOrDeps;
+ public final boolean hasBallot;
+
+ InternalStatus(boolean hasExecuteAtOrDeps, boolean hasBallot)
+ {
+ this.hasExecuteAtOrDeps = hasExecuteAtOrDeps;
+ this.hasBallot = hasBallot;
+ this.hasInfo = hasExecuteAtOrDeps | hasBallot;
+ }
+
+ boolean hasExecuteAt()
+ {
+ return hasExecuteAtOrDeps;
+ }
+
+ boolean hasDeps()
+ {
+ return hasExecuteAtOrDeps;
+ }
+
+ boolean hasStableDeps()
+ {
+ return this == STABLE || this == APPLIED;
+ }
+
+ public boolean isCommitted()
+ {
+ return this == COMMITTED | this == STABLE | this == APPLIED;
+ }
+
+ public Timestamp depsKnownBefore(TxnId txnId, Timestamp executeAt)
+ {
+ switch (this)
+ {
+ default: throw new AssertionError("Unhandled InternalStatus: " + this);
+ case TRANSITIVELY_KNOWN:
+ case INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED:
+ case HISTORICAL:
+ throw new AssertionError("Invalid InternalStatus to know deps");
+
+ case PREACCEPTED_OR_ACCEPTED_INVALIDATE:
+ case ACCEPTED:
+ return txnId;
+
+ case APPLIED:
+ case STABLE:
+ case COMMITTED:
+ return executeAt;
+ }
+ }
+
+ public Timestamp witnessedAfter(TxnId txnId, Timestamp executeAt)
+ {
+ switch (this)
+ {
+ default: throw new AssertionError("Unhandled InternalStatus: " + this);
+ case INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED:
+ case TRANSITIVELY_KNOWN:
+ case HISTORICAL:
+ case PREACCEPTED_OR_ACCEPTED_INVALIDATE:
+ case ACCEPTED:
+ return txnId;
+
+ case APPLIED:
+ case STABLE:
+ case COMMITTED:
+ return executeAt;
+ }
+ }
+
+ @VisibleForTesting
+ public static InternalStatus from(SaveStatus status)
+ {
+ return convert.get(status);
+ }
+
+ public static InternalStatus get(int ordinal)
+ {
+ return VALUES[ordinal];
+ }
+ }
+
+ final Key key;
+ final RedundantBefore.Entry redundantBefore;
+ final TxnInfo prunedBefore;
+ // a btree keyed by TxnId we have encountered since pruning that occur before prunedBefore;
+ // mapping to those TxnId that had witnessed this potentially-pruned TxnId.
+ final Object[] loadingPruned;
+ // all transactions, sorted by TxnId
+ final TxnInfo[] byId;
+ // reads and writes ONLY that are committed or stable or applied, keyed by executeAt
+ // TODO (required): validate that it is always a prefix that is Applied (i.e. never a gap)
+ // TODO (desired): filter transactions whose execution we don't manage
+ final TxnInfo[] committedByExecuteAt;
+ final int minUndecidedById, maxAppliedWriteByExecuteAt;
+ final Unmanaged[] unmanageds;
+
+ CommandsForKey(Key key, RedundantBefore.Entry redundantBefore, TxnInfo prunedBefore, Object[] loadingPruned, TxnInfo[] byId, TxnInfo[] committedByExecuteAt, int minUndecidedById, int maxAppliedWriteByExecuteAt, Unmanaged[] unmanageds)
+ {
+ this.key = key;
+ this.redundantBefore = Invariants.nonNull(redundantBefore);
+ this.prunedBefore = Invariants.nonNull(prunedBefore);
+ this.loadingPruned = loadingPruned;
+ this.byId = byId;
+ this.committedByExecuteAt = committedByExecuteAt;
+ this.minUndecidedById = minUndecidedById;
+ this.maxAppliedWriteByExecuteAt = maxAppliedWriteByExecuteAt;
+ this.unmanageds = unmanageds;
+ checkIntegrity();
+ }
+
+ CommandsForKey(Key key, RedundantBefore.Entry redundantBefore, TxnId prunedBefore, Object[] loadingPruned, TxnInfo[] byId, Unmanaged[] unmanageds)
+ {
+ this.key = key;
+ this.redundantBefore = Invariants.nonNull(redundantBefore);
+ this.prunedBefore = redundantBefore.shardRedundantBefore().compareTo(prunedBefore) >= 0
+ ? NO_INFO : prunedBefore.equals(TxnId.NONE)
+ ? NO_INFO : Invariants.nonNull(get(prunedBefore, byId));
+ this.loadingPruned = loadingPruned;
+ this.byId = byId;
+ this.unmanageds = unmanageds;
+
+ int countCommitted = 0;
+ int minUndecided = -1;
+ for (int i = 0; i < byId.length ; ++i)
+ {
+ TxnInfo txn = byId[i];
+ if (txn.status == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED) continue;
+ if (txn.status.compareTo(COMMITTED) >= 0) ++countCommitted;
+ else if (minUndecided == -1 && managesExecution(txn)) minUndecided = i;
+ }
+ this.minUndecidedById = minUndecided;
+ this.committedByExecuteAt = new TxnInfo[countCommitted];
+ countCommitted = 0;
+ for (TxnInfo txn : byId)
+ {
+ if (txn.status.compareTo(COMMITTED) >= 0 && txn.status != INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ committedByExecuteAt[countCommitted++] = txn;
+ }
+ Arrays.sort(committedByExecuteAt, TxnInfo::compareExecuteAt);
+ int maxAppliedByExecuteAt = committedByExecuteAt.length;
+ while (--maxAppliedByExecuteAt >= 0)
+ {
+ TxnInfo txn = committedByExecuteAt[maxAppliedByExecuteAt];
+ if (txn.status == APPLIED && txn.kind() == Write)
+ break;
+ }
+ this.maxAppliedWriteByExecuteAt = maxAppliedByExecuteAt;
+
+ checkIntegrity();
+ }
+
+ CommandsForKey(CommandsForKey copy, Object[] loadingPruned, Unmanaged[] unmanageds)
+ {
+ this.key = copy.key;
+ this.redundantBefore = copy.redundantBefore;
+ this.prunedBefore = copy.prunedBefore;
+ this.loadingPruned = loadingPruned;
+ this.byId = copy.byId;
+ this.committedByExecuteAt = copy.committedByExecuteAt;
+ this.minUndecidedById = copy.minUndecidedById;
+ this.maxAppliedWriteByExecuteAt = copy.maxAppliedWriteByExecuteAt;
+ this.unmanageds = unmanageds;
+
+ checkIntegrity();
+ }
+
+ public CommandsForKey(Key key)
+ {
+ this.key = key;
+ this.redundantBefore = NO_REDUNDANT_BEFORE;
+ this.prunedBefore = NO_INFO;
+ this.loadingPruned = Pruning.LoadingPruned.empty();
+ this.byId = NO_INFOS;
+ this.committedByExecuteAt = NO_INFOS;
+ this.minUndecidedById = this.maxAppliedWriteByExecuteAt = -1;
+ this.unmanageds = NO_PENDING_UNMANAGED;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CommandsForKey@" + System.identityHashCode(this) + '{' + key + '}';
+ }
+
+ public Key key()
+ {
+ return key;
+ }
+
+ public int size()
+ {
+ return byId.length;
+ }
+
+ public boolean isLoadingPruned()
+ {
+ return !BTree.isEmpty(loadingPruned);
+ }
+
+ @VisibleForImplementation
+ public int unmanagedCount()
+ {
+ return unmanageds.length;
+ }
+
+ public int indexOf(TxnId txnId)
+ {
+ return Arrays.binarySearch(byId, txnId);
+ }
+
+ public TxnId txnId(int i)
+ {
+ return byId[i];
+ }
+
+ public TxnInfo get(int i)
+ {
+ return byId[i];
+ }
+
+ @VisibleForImplementation
+ public Unmanaged getUnmanaged(int i)
+ {
+ return unmanageds[i];
+ }
+
+ public TxnInfo get(TxnId txnId)
+ {
+ int i = indexOf(txnId);
+ return i < 0 ? null : byId[i];
+ }
+
+ private static TxnInfo get(TxnId txnId, TxnInfo[] txns)
+ {
+ int i = Arrays.binarySearch(txns, txnId);
+ return i < 0 ? null : txns[i];
+ }
+
+ public RedundantBefore.Entry redundantBefore()
+ {
+ return redundantBefore;
+ }
+
+ public TxnId prunedBefore()
+ {
+ return prunedBefore;
+ }
+
+ public TxnId locallyRedundantBefore()
+ {
+ return redundantBefore.locallyRedundantBefore();
+ }
+
+ public TxnId shardRedundantBefore()
+ {
+ return redundantBefore.shardRedundantBefore();
+ }
+
+ /**
+ * All commands before/after (exclusive of) the given timestamp, excluding those that are redundant,
+ * or have locally applied prior to some other command that is stable, will be returned by the collection.
+ *
+ * Note that if the command has already applied locally, this method may erroneously treat the command as
+ * being unwitnessed by some following/dependent command.
+ * <p>
+ * Note that {@code testDep} applies only to commands that MAY have the command in their deps; if specified any
+ * commands that do not know any deps will be ignored, as will any with an executeAt prior to the txnId.
+ * <p>
+ */
+ public <P1, T> T mapReduceFull(TxnId testTxnId,
+ Kinds testKind,
+ TestStartedAt testStartedAt,
+ TestDep testDep,
+ TestStatus testStatus,
+ CommandFunction<P1, T, T> map, P1 p1, T initialValue)
+ {
+ int start, end, loadingIndex = 0;
+ // if this is null the TxnId is known in byId
+ // otherwise, it must be non-null and represents the transactions (if any) that have requested it be loaded due to being pruned
+ TxnId[] loadingFor = null;
+ {
+ int insertPos = Arrays.binarySearch(byId, testTxnId);
+ if (insertPos < 0)
+ {
+ loadingFor = NO_TXNIDS;
+ insertPos = -1 - insertPos;
+ switch (testDep)
+ {
+ default: throw new AssertionError("Unhandled TestDep: " + testDep);
+ case ANY_DEPS:
+ break;
+
+ case WITH:
+ if (testTxnId.compareTo(prunedBefore) >= 0)
+ return initialValue;
+
+ loadingFor = loadingPrunedFor(loadingPruned, testTxnId, NO_TXNIDS);
+ break;
+
+ case WITHOUT:
+ if (testTxnId.compareTo(prunedBefore) < 0)
+ loadingFor = loadingPrunedFor(loadingPruned, testTxnId, NO_TXNIDS);
+ }
+ }
+
+ switch (testStartedAt)
+ {
+ default: throw new AssertionError("Unhandled TestStartedAt: " + testTxnId);
+ case STARTED_BEFORE: start = 0; end = insertPos; break;
+ case STARTED_AFTER: start = insertPos; end = byId.length; break;
+ case ANY: start = 0; end = byId.length;
+ }
+ }
+
+ for (int i = start; i < end ; ++i)
+ {
+ TxnInfo txn = byId[i];
+ if (!testKind.test(txn.kind())) continue;
+ InternalStatus status = txn.status;
+ switch (testStatus)
+ {
+ default: throw new AssertionError("Unhandled TestStatus: " + testStatus);
+ case IS_PROPOSED:
+ if (status == ACCEPTED || status == COMMITTED) break;
+ else continue;
+ case IS_STABLE:
+ if (status.compareTo(STABLE) >= 0 && status.compareTo(INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED) < 0) break;
+ else continue;
+ case ANY_STATUS:
+ if (status == TRANSITIVELY_KNOWN)
+ continue;
+ }
+
+ Timestamp executeAt = txn.executeAt;
+ if (testDep != ANY_DEPS)
+ {
+ if (!status.hasExecuteAtOrDeps)
+ continue;
+
+ if (executeAt.compareTo(testTxnId) <= 0)
+ continue;
+
+ boolean hasAsDep;
+ if (loadingFor == null)
+ {
+ TxnId[] missing = txn.missing();
+ hasAsDep = missing == NO_TXNIDS || Arrays.binarySearch(txn.missing(), testTxnId) < 0;
+ }
+ else if (loadingFor == NO_TXNIDS)
+ {
+ hasAsDep = false;
+ }
+ else
+ {
+ // we could use expontentialSearch and moving index for improved algorithmic complexity,
+ // but since should be rarely taken path probably not worth code complexity
+ loadingIndex = SortedArrays.exponentialSearch(loadingFor, loadingIndex, loadingFor.length, txn);
+ if (hasAsDep = (loadingIndex >= 0)) ++loadingIndex;
+ else loadingIndex = -1 - loadingIndex;
+ }
+
+ if (hasAsDep != (testDep == WITH))
+ continue;
+ }
+
+ initialValue = map.apply(p1, key, txn.plainTxnId(), executeAt, initialValue);
+ }
+ return initialValue;
+ }
+
+ public <P1, T> T mapReduceActive(Timestamp startedBefore,
+ Kinds testKind,
+ CommandFunction<P1, T, T> map, P1 p1, T initialValue)
+ {
+ int end = insertPos(startedBefore);
+ Timestamp maxCommittedWriteBefore;
+ {
+ int from = 0, to = committedByExecuteAt.length;
+ if (maxAppliedWriteByExecuteAt >= 0)
+ {
+ if (committedByExecuteAt[maxAppliedWriteByExecuteAt].executeAt.compareTo(startedBefore) <= 0) from = maxAppliedWriteByExecuteAt;
+ else to = maxAppliedWriteByExecuteAt;
+ }
+ int i = SortedArrays.binarySearch(committedByExecuteAt, from, to, startedBefore, (f, v) -> f.compareTo(v.executeAt), FAST);
+ if (i < 0) i = -2 - i;
+ else --i;
+ while (i >= 0 && !committedByExecuteAt[i].kind().isWrite()) --i;
+ maxCommittedWriteBefore = i < 0 ? null : committedByExecuteAt[i].executeAt;
+ }
+
+ for (int i = 0; i < end ; ++i)
+ {
+ TxnInfo txn = byId[i];
+ if (!testKind.test(txn.kind()))
+ continue;
+
+ switch (txn.status)
+ {
+ case COMMITTED:
+ case STABLE:
+ case APPLIED:
+ // TODO (expected): prove the correctness of this approach
+ if (!ELIDE_TRANSITIVE_DEPENDENCIES || maxCommittedWriteBefore == null || txn.executeAt.compareTo(maxCommittedWriteBefore) >= 0 || !Write.witnesses(txn))
+ break;
+ case TRANSITIVELY_KNOWN:
+ case INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED:
+ continue;
+ }
+
+ initialValue = map.apply(p1, key, txn.plainTxnId(), txn.executeAt, initialValue);
+ }
+
+ if (startedBefore.compareTo(prunedBefore) <= 0)
+ {
+ // in the event we have pruned transactions that may execute before us, we take the earliest future dependency we can in their place.
+ // in practice this only has an effect on ExclusiveSyncPoints because they do not agree an execution time
+ // and only take dependencies on TxnId lower than them. Other transactions will propose a timestamp that
+ // occurs after any dependencies witnessed here, or will be invalidated, or have already been agreed
+ // and their dependencies are known.
+ int i = SortedArrays.binarySearch(committedByExecuteAt, 0, maxAppliedWriteByExecuteAt, startedBefore, (f, v) -> f.compareTo(v.executeAt), FAST);
+ if (i < 0) i = -1 - i;
+ while (!committedByExecuteAt[i].kind().isWrite())
+ ++i;
+
+ initialValue = map.apply(p1, key, committedByExecuteAt[i].plainTxnId(), committedByExecuteAt[i].executeAt, initialValue);
+ }
+
+ return initialValue;
+ }
+
+ // NOTE: prev MAY NOT be the version that last updated us due to various possible race conditions
+ @VisibleForTesting
+ public CommandsForKeyUpdate update(Command next)
+ {
+ Invariants.checkState(manages(next.txnId()));
+ InternalStatus newStatus = InternalStatus.from(next.saveStatus());
+ if (newStatus == null)
+ return this;
+
+ return update(newStatus, next, false);
+ }
+
+ CommandsForKeyUpdate updatePruned(Command next)
+ {
+ InternalStatus newStatus = InternalStatus.from(next.saveStatus());
+ if (newStatus == null)
+ newStatus = TRANSITIVELY_KNOWN;
+ if (!manages(next.txnId()))
+ newStatus = newStatus.compareTo(COMMITTED) < 0 ? TRANSITIVELY_KNOWN : INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED;
+ return update(newStatus, next, true);
+ }
+
+ private CommandsForKeyUpdate update(InternalStatus newStatus, Command next, boolean wasPruned)
+ {
+ TxnId txnId = next.txnId();
+ Invariants.checkArgument(wasPruned || manages(txnId));
+
+ if (txnId.compareTo(redundantBefore.shardRedundantBefore()) < 0)
+ return this;
+
+ TxnId[] loadingAsPrunedFor = loadingPrunedFor(loadingPruned, txnId, null); // we default to null to distinguish between no match, and a match with NO_TXNIDS
+ wasPruned |= loadingAsPrunedFor != null;
+
+ int pos = Arrays.binarySearch(byId, txnId);
+ CommandsForKeyUpdate result;
+ if (pos < 0)
+ {
+ pos = -1 - pos;
+ if (newStatus.hasExecuteAtOrDeps && !wasPruned && txnId.domain().isKey()) result = insert(pos, txnId, newStatus, next);
+ else result = insert(pos, txnId, TxnInfo.create(txnId, newStatus, next), wasPruned, loadingAsPrunedFor);
+ }
+ else
+ {
+ // update
+ TxnInfo cur = byId[pos];
+
+ if (cur != null)
+ {
+ int c = newStatus.compareTo(cur.status);
+ if (c <= 0)
+ {
+ if (c < 0)
+ {
+ if (!(newStatus == PREACCEPTED_OR_ACCEPTED_INVALIDATE && cur.status == ACCEPTED && next.acceptedOrCommitted().compareTo(cur.ballot()) > 0))
+ return this;
+ }
+ else
+ {
+ if (!newStatus.hasInfo)
+ return this;
+
+ if (next.acceptedOrCommitted().compareTo(cur.ballot()) <= 0)
+ return this;
+ }
+ }
+ }
+
+ if (newStatus.hasExecuteAtOrDeps && !wasPruned) result = update(pos, txnId, cur, newStatus, next);
+ else result = update(pos, txnId, cur, TxnInfo.create(txnId, newStatus, next), wasPruned, loadingAsPrunedFor);
+ }
+
+ return result;
+ }
+
+ private CommandsForKeyUpdate insert(int insertPos, TxnId plainTxnId, InternalStatus newStatus, Command command)
+ {
+ return insertOrUpdate(this, insertPos, -1, plainTxnId, null, newStatus, command);
+ }
+
+ private CommandsForKeyUpdate update(int updatePos, TxnId plainTxnId, TxnInfo curInfo, InternalStatus newStatus, Command command)
+ {
+ return insertOrUpdate(this, updatePos, updatePos, plainTxnId, curInfo, newStatus, command);
+ }
+
+ CommandsForKeyUpdate update(int pos, TxnId plainTxnId, TxnInfo curInfo, TxnInfo newInfo, boolean wasPruned, TxnId[] loadingAsPrunedFor)
+ {
+ return insertOrUpdate(this, pos, plainTxnId, curInfo, newInfo, wasPruned, loadingAsPrunedFor);
+ }
+
+ /**
+ * Insert a new txnId and info
+ */
+ CommandsForKeyUpdate insert(int pos, TxnId plainTxnId, TxnInfo newInfo, boolean wasPruned, TxnId[] loadingAsPrunedFor)
+ {
+ return insertOrUpdate(this, pos, plainTxnId, null, newInfo, wasPruned, loadingAsPrunedFor);
+ }
+
+ // TODO (required): additional linearizability violation detection, based on expectation of presence in missing set
+
+ CommandsForKeyUpdate update(TxnInfo[] newById, int newMinUndecidedById, TxnInfo[] newCommittedByExecuteAt, int newMaxAppliedWriteByExecuteAt, Object[] newLoadingPruned, TxnId plainTxnId, @Nullable TxnInfo curInfo, @Nonnull TxnInfo newInfo)
+ {
+ return new CommandsForKey(key, redundantBefore, prunedBefore, newLoadingPruned, newById, newCommittedByExecuteAt, newMinUndecidedById, newMaxAppliedWriteByExecuteAt, unmanageds)
+ .notifyUnmanaged(curInfo, newInfo);
+ }
+
+ CommandsForKey update(Unmanaged[] newUnmanageds)
+ {
+ return new CommandsForKey(key, redundantBefore, prunedBefore, loadingPruned, byId, committedByExecuteAt, minUndecidedById, maxAppliedWriteByExecuteAt, newUnmanageds);
+ }
+
+ CommandsForKey update(Object[] newLoadingPruned)
+ {
+ return new CommandsForKey(key, redundantBefore, prunedBefore, newLoadingPruned, byId, committedByExecuteAt, minUndecidedById, maxAppliedWriteByExecuteAt, unmanageds);
+ }
+
+ private void notifyWaitingOnCommit(SafeCommandStore safeStore, TxnInfo uncommitted, NotifySink notifySink)
+ {
+ if (redundantBefore.endEpoch > uncommitted.epoch())
+ notifySink.waitingOnCommit(safeStore, uncommitted, key);
+ }
+
+ CommandsForKeyUpdate registerUnmanaged(SafeCommandStore safeStore, SafeCommand safeCommand)
+ {
+ return registerUnmanaged(safeStore, safeCommand, NotifySink.DefaultNotifySink.INSTANCE);
+ }
+
+ CommandsForKeyUpdate registerUnmanaged(SafeCommandStore safeStore, SafeCommand safeCommand, NotifySink notifySink)
+ {
+ return Updating.updateUnmanaged(this, safeStore, safeCommand, notifySink, true, null);
+ }
+
+ private CommandsForKeyUpdate notifyUnmanaged(@Nullable TxnInfo curInfo, TxnInfo newInfo)
+ {
+ return PostProcess.notifyUnmanaged(this, curInfo, newInfo);
+ }
+
+ void postProcess(SafeCommandStore safeStore, CommandsForKey prevCfk, @Nullable Command command, NotifySink notifySink)
+ {
+ TxnInfo minUndecided = minUndecided();
+ if (minUndecided != null && !minUndecided.equals(prevCfk.minUndecided()))
+ notifyWaitingOnCommit(safeStore, minUndecided, notifySink);
+
+ if (command == null || !command.hasBeen(Status.Committed) || !managesExecution(command.txnId()))
+ return;
+
+ /*
+ * Make sure transactions are notified they can execute, without repeatedly notifying.
+ *
+ * Basic logic:
+ * - If there is any uncommitted transaction, notify the minimum txnId that we are expecting it to commit
+ * - If a nextWrite is known, then:
+ * - if it is next to execute we notify it;
+ * - otherwise we try to notify any transactions that execute before it, but none after it
+ * - If no nextWrite is known, then we attempt to notify any transaction as it is decided
+ *
+ * Note: If we have any uncommitted transactions that were declared before the next decided transaction,
+ * we do not set next or nextWrite, and so do not notify them
+ */
+
+ TxnId updatedTxnId = command.txnId();
+ TxnInfo newInfo = get(updatedTxnId);
+ InternalStatus newStatus = newInfo.status;
+
+ TxnInfo prevInfo = prevCfk.get(updatedTxnId);
+ InternalStatus prevStatus = prevInfo == null ? TRANSITIVELY_KNOWN : prevInfo.status;
+
+ int byExecuteAtIndex = newStatus == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED ? -1 : checkNonNegative(Arrays.binarySearch(committedByExecuteAt, newInfo, TxnInfo::compareExecuteAt));
+
+ Kinds mayExecuteKinds;
+ int mayExecuteToIndex, mayNotExecuteBeforeIndex = 0;
+ int mayExecuteAnyAtIndex = -1;
+ if (prevStatus.compareTo(COMMITTED) < 0)
+ {
+ mayExecuteKinds = updatedTxnId.kind().witnessedBy();
+ switch (newInfo.status)
+ {
+ default: throw new AssertionError("Unhandled InternalStatus: " + newInfo.status);
+ case ACCEPTED:
+ case HISTORICAL:
+ case PREACCEPTED_OR_ACCEPTED_INVALIDATE:
+ case TRANSITIVELY_KNOWN:
+ throw illegalState("Invalid status: command has been committed but we have InternalStatus " + newInfo.status);
+
+ case INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED:
+ case APPLIED:
+ mayExecuteToIndex = committedByExecuteAt.length;
+ break;
+
+ case COMMITTED:
+ mayExecuteToIndex = byExecuteAtIndex;
+ break;
+
+ case STABLE:
+ mayExecuteToIndex = byExecuteAtIndex + 1;
+ mayExecuteAnyAtIndex = byExecuteAtIndex;
+ break;
+ }
+ }
+ else if (newStatus == APPLIED)
+ {
+ mayExecuteKinds = updatedTxnId.kind().witnessedBy();
+ mayExecuteToIndex = committedByExecuteAt.length;
+ }
+ else if (newStatus == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED && prevStatus != INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ {
+ // rare case of us erasing a command that has been committed, so simply try to execute the next executable thing
+ mayExecuteKinds = AnyGloballyVisible;
+ mayExecuteToIndex = committedByExecuteAt.length;
+ }
+ else
+ {
+ // we only permit to execute the updated transaction itself in this case, as there's no new information for the execution of other transactions
+ if (newStatus != STABLE)
+ return;
+
+ mayExecuteAnyAtIndex = byExecuteAtIndex;
+ mayExecuteKinds = AnyGloballyVisible;
+ mayExecuteToIndex = byExecuteAtIndex + 1;
+ }
+
+ notifyManaged(safeStore, mayExecuteKinds, mayNotExecuteBeforeIndex, mayExecuteToIndex, mayExecuteAnyAtIndex, notifySink);
+ }
+
+ private void notifyManaged(SafeCommandStore safeStore, Kinds kinds, int mayNotExecuteBeforeIndex, int mayExecuteToIndex, int mayExecuteAny, NotifySink notifySink)
+ {
+ Participants<?> asParticipants = null;
+ int undecidedIndex = minUndecidedById < 0 ? byId.length : minUndecidedById;
+ long unappliedCounters = 0L;
+ TxnInfo minUndecided = minUndecided();
+
+ for (int i = maxAppliedWriteByExecuteAt + 1; i < mayExecuteToIndex ; ++i)
+ {
+ TxnInfo txn = committedByExecuteAt[i];
+ if (txn.status == APPLIED || !managesExecution(txn))
+ continue;
+
+ Kind kind = txn.kind();
+ if (i >= mayNotExecuteBeforeIndex && (kinds.test(kind) || i == mayExecuteAny) && !isWaitingOnPruned(loadingPruned, txn, txn.executeAt))
+ {
+ switch (txn.status)
+ {
+ case COMMITTED:
+ {
+ // cannot execute as dependencies not stable, so notify progress log to get or decide stable deps
+ if (asParticipants == null)
+ asParticipants = Keys.of(key).toParticipants();
+ safeStore.progressLog().waiting(txn.plainTxnId(), WaitingToExecute, null, asParticipants);
+ break;
+ }
+
+ case STABLE:
+ {
+ if (undecidedIndex < byId.length)
+ {
+ int nextUndecidedIndex = SortedArrays.exponentialSearch(byId, undecidedIndex, byId.length, txn.executeAt, Timestamp::compareTo, FAST);
+ if (nextUndecidedIndex < 0) nextUndecidedIndex = -1 -nextUndecidedIndex;
+ while (undecidedIndex < nextUndecidedIndex)
+ {
+ TxnInfo backfillTxn = byId[undecidedIndex++];
+ if (backfillTxn.status.compareTo(COMMITTED) >= 0 || !managesExecution(backfillTxn)) continue;
+ unappliedCounters += unappliedCountersDelta(backfillTxn.kind());
+ }
+ }
+
+ int expectMissingCount = unappliedCount(unappliedCounters, kind);
+
+ // We remove committed transactions from the missing set, since they no longer need them there
+ // So the missing collection represents only those uncommitted transaction ids that a transaction
+ // witnesses/conflicts with. So we may simply count all of those we know of with a lower TxnId,
+ // and if the count is the same then we are not awaiting any of them for execution and can remove
+ // this command's dependency on this key for execution.
+ TxnId[] missing = txn.missing();
+ int missingCount = missing.length;
+ if (missingCount > 0)
+ {
+ int missingFrom = 0;
+ if (minUndecided != null)
+ {
+ missingFrom = SortedArrays.binarySearch(missing, 0, missing.length, minUndecided, TxnId::compareTo, FAST);
+ if (missingFrom < 0) missingFrom = -1 - missingFrom;
+ missingCount -= missingFrom;
+ }
+ for (int j = missingFrom ; j < missing.length ; ++j)
+ {
+ if (!managesExecution(missing[j]))
+ --missingCount;
+ }
+ }
+ if (expectMissingCount == missingCount)
+ {
+ TxnId txnId = txn.plainTxnId();
+ notifySink.notWaiting(safeStore, txnId, key);
+ if (asParticipants == null)
+ asParticipants = Keys.of(key).toParticipants();
+ safeStore.progressLog().waiting(txnId, WaitingToApply, null, asParticipants);
+ }
+ }
+ }
+ }
+
+ unappliedCounters += unappliedCountersDelta(kind);
+ if (kind == Kind.Write)
+ return; // the minimum execute index occurs after the next write, so nothing to do yet
+ }
+ }
+
+ private static long unappliedCountersDelta(Kind kind)
+ {
+ switch (kind)
+ {
+ default: throw new AssertionError("Unhandled Txn.Kind: " + kind);
+ case LocalOnly:
+ case EphemeralRead:
+ throw illegalState("Invalid Txn.Kind for CommandsForKey: " + kind);
+
+ case ExclusiveSyncPoint:
+ case SyncPoint:
+ return 0L;
+
+ case Write:
+ return (1L << 32) + 1L;
+
+ case Read:
+ return 1L;
+ }
+ }
+
+ private static int unappliedCount(long unappliedCounters, Kind kind)
+ {
+ switch (kind)
+ {
+ default: throw new AssertionError("Unhandled Txn.Kind: " + kind);
+ case LocalOnly:
+ case EphemeralRead:
+ case ExclusiveSyncPoint:
+ throw illegalState("Invalid Txn.Kind for CommandsForKey: " + kind);
+
+ case SyncPoint:
+ case Write:
+ return (int)unappliedCounters;
+
+ case Read:
+ return (int) (unappliedCounters >>> 32);
+ }
+
+ }
+
+ public CommandsForKey withRedundantBeforeAtLeast(RedundantBefore.Entry newRedundantBefore)
+ {
+ Invariants.checkArgument(newRedundantBefore.shardRedundantBefore().compareTo(shardRedundantBefore()) >= 0, "Expect new RedundantBefore.Entry shardAppliedOrInvalidatedBefore to be ahead of existing one");
+
+ if (newRedundantBefore.equals(redundantBefore))
+ return this;
+
+ TxnInfo[] newInfos = byId;
+ int pos = insertPos(newRedundantBefore.shardRedundantBefore());
+ if (pos != 0)
+ {
+ newInfos = Arrays.copyOfRange(byId, pos, byId.length);
+ for (int i = 0 ; i < newInfos.length ; ++i)
+ {
+ TxnInfo txn = newInfos[i];
+ TxnId[] missing = txn.missing();
+ if (missing == NO_TXNIDS) continue;
+ missing = removeRedundantMissing(missing, newRedundantBefore.shardRedundantBefore());
+ newInfos[i] = txn.update(missing);
+ }
+ }
+
+ // TODO (expected): filter pending unmanageds
+ return new CommandsForKey(key, newRedundantBefore, prunedBefore, loadingPruned, newInfos, unmanageds);
+ }
+
+ /**
+ * Remove transitively redundant applied or invalidated commands
+ * @param pruneInterval the number of committed commands we must have prior to the first prune point candidate to trigger a prune attempt
+ * @param minHlcDelta do not prune any commands with an HLC within this distance of the prune point candidate
+ */
+ public CommandsForKey maybePrune(int pruneInterval, long minHlcDelta)
+ {
+ return Pruning.maybePrune(this, pruneInterval, minHlcDelta);
+ }
+
+ public CommandsForKeyUpdate registerHistorical(TxnId txnId)
+ {
+ return Updating.registerHistorical(this, txnId);
+ }
+
+ int insertPos(Timestamp timestamp)
+ {
+ int i = Arrays.binarySearch(byId, 0, byId.length, timestamp);
+ if (i < 0) i = -1 -i;
+ return i;
+ }
+
+ public TxnId findFirst()
+ {
+ return byId.length > 0 ? byId[0] : null;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CommandsForKey that = (CommandsForKey) o;
+ return Objects.equals(key, that.key)
+ && Arrays.equals(byId, that.byId)
+ && Arrays.equals(unmanageds, that.unmanageds);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CommandsForKey cfk()
+ {
+ return this;
+ }
+
+ @Override
+ PostProcess postProcess()
+ {
+ return null;
+ }
+
+ TxnInfo minUndecided()
+ {
+ return minUndecidedById < 0 ? null : byId[minUndecidedById];
+ }
+
+ int maxContiguousManagedAppliedIndex()
+ {
+ int i = maxAppliedWriteByExecuteAt + 1;
+ while (i < committedByExecuteAt.length)
+ {
+ TxnInfo txn = committedByExecuteAt[i];
+ if (txn.status != APPLIED && managesExecution(txn))
+ break;
+ ++i;
+ }
+ return i - 1;
+ }
+
+ TxnInfo maxContiguousManagedApplied()
+ {
+ int i = maxContiguousManagedAppliedIndex();
+ return i < 0 ? null : committedByExecuteAt[i];
+ }
+
+ private void checkIntegrity()
+ {
+ if (isParanoid())
+ {
+ Invariants.checkState(byId.length == 0 || byId[0].compareTo(shardRedundantBefore()) >= 0);
+ Invariants.checkState(prunedBefore == NO_INFO || (prunedBefore.status == APPLIED && prunedBefore.kind().isWrite()));
+ Invariants.checkState(minUndecidedById < 0 || byId[minUndecidedById].status.compareTo(COMMITTED) < 0 && managesExecution(byId[minUndecidedById]));
+ if (maxAppliedWriteByExecuteAt >= 0)
+ {
+ Invariants.checkState(committedByExecuteAt[maxAppliedWriteByExecuteAt].kind() == Write);
+ Invariants.checkState(committedByExecuteAt[maxAppliedWriteByExecuteAt].status == APPLIED);
+ }
+
+ if (testParanoia(LINEAR, NONE, LOW))
+ {
+ Invariants.checkArgument(SortedArrays.isSortedUnique(byId));
+ Invariants.checkArgument(SortedArrays.isSortedUnique(committedByExecuteAt, TxnInfo::compareExecuteAt));
+
+ if (minUndecidedById >= 0) for (int i = 0 ; i < minUndecidedById ; ++i) Invariants.checkState(byId[i].status.compareTo(COMMITTED) >= 0 || !managesExecution(byId[i]));
+ else for (TxnInfo txn : byId) Invariants.checkState(txn.status.compareTo(COMMITTED) >= 0 || !managesExecution(txn));
+
+ if (maxAppliedWriteByExecuteAt >= 0)
+ {
+ for (int i = maxAppliedWriteByExecuteAt + 1; i < committedByExecuteAt.length ; ++i)
+ Invariants.checkState(committedByExecuteAt[i].kind() != Kind.Write || committedByExecuteAt[i].status.compareTo(APPLIED) < 0);
+ }
+ else
+ {
+ for (TxnInfo txn : committedByExecuteAt)
+ Invariants.checkState(txn.kind() != Kind.Write || txn.status.compareTo(APPLIED) < 0);
+ }
+ }
+ if (testParanoia(SUPERLINEAR, NONE, LOW))
+ {
+ for (TxnInfo txn : committedByExecuteAt)
+ {
+ Invariants.checkState(txn == get(txn, byId));
+ }
+ for (TxnInfo txn : byId)
+ {
+ for (TxnId missingId : txn.missing())
+ {
+ Invariants.checkState(txn.kind().witnesses(missingId));
+ TxnInfo missingInfo = get(missingId, byId);
+ Invariants.checkState(missingInfo.status.compareTo(COMMITTED) < 0);
+ Invariants.checkState(txn.depsKnownBefore().compareTo(missingInfo.witnessedAfter()) >= 0);
+ }
+ if (txn.status.isCommitted())
+ Invariants.checkState(txn == committedByExecuteAt[Arrays.binarySearch(committedByExecuteAt, 0, committedByExecuteAt.length, txn, TxnInfo::compareExecuteAt)]);
+ }
+ for (Pruning.LoadingPruned txn : BTree.<Pruning.LoadingPruned>iterable(loadingPruned))
+ {
+ Invariants.checkState(indexOf(txn) < 0);
+ }
+ }
+ }
+ }
+}
diff --git a/accord-core/src/main/java/accord/local/cfk/CommandsForKeyUpdate.java b/accord-core/src/main/java/accord/local/cfk/CommandsForKeyUpdate.java
new file mode 100644
index 00000000..5d990141
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/CommandsForKeyUpdate.java
@@ -0,0 +1,72 @@
+/*
+ * 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 accord.local.cfk;
+
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import accord.local.Command;
+import accord.local.SafeCommandStore;
+
+/**
+ * Encapsulates an updated {@code CommandsForKey} and any follow-up notification work that cannot be performed as part
+ * of the {@code CommandsForKey} update because it can trigger updates that may lead to additional updates to the
+ * {@code CommandsForKey}, which would still be being constructed. We therefore first update the {@code CommandsForKey},
+ * then notify any work that might itself need to update the {@code CommandsForKey}.
+ */
+public abstract class CommandsForKeyUpdate
+{
+ @VisibleForTesting
+ public abstract CommandsForKey cfk();
+ abstract PostProcess postProcess();
+ abstract void postProcess(SafeCommandStore safeStore, @Nullable CommandsForKey prevCfk, @Nullable Command command, NotifySink notifySink);
+
+ static class CommandsForKeyUpdateWithNotifier extends CommandsForKeyUpdate
+ {
+ final CommandsForKey cfk;
+ final PostProcess postProcess;
+
+ CommandsForKeyUpdateWithNotifier(CommandsForKey cfk, PostProcess postProcess)
+ {
+ this.cfk = cfk;
+ this.postProcess = postProcess;
+ }
+
+ @Override
+ public CommandsForKey cfk()
+ {
+ return cfk;
+ }
+
+ @Override
+ PostProcess postProcess()
+ {
+ return postProcess;
+ }
+
+ @Override
+ void postProcess(SafeCommandStore safeStore, @Nullable CommandsForKey prevCfk, @Nullable Command command, NotifySink notifySink)
+ {
+ cfk.postProcess(safeStore, prevCfk, command, notifySink);
+ postProcess.postProcess(safeStore, cfk.key(), notifySink);
+ }
+ }
+}
+
diff --git a/accord-core/src/main/java/accord/local/cfk/NotifySink.java b/accord-core/src/main/java/accord/local/cfk/NotifySink.java
new file mode 100644
index 00000000..be8b1ca4
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/NotifySink.java
@@ -0,0 +1,113 @@
+/*
+ * 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 accord.local.cfk;
+
+import accord.api.Key;
+import accord.local.Command;
+import accord.local.Commands;
+import accord.local.CommonAttributes;
+import accord.local.PreLoadContext;
+import accord.local.SafeCommand;
+import accord.local.SafeCommandStore;
+import accord.local.Status;
+import accord.local.cfk.CommandsForKey.TxnInfo;
+import accord.primitives.Keys;
+import accord.primitives.Seekables;
+import accord.primitives.TxnId;
+
+import static accord.local.KeyHistory.COMMANDS;
+import static accord.local.SaveStatus.LocalExecution.WaitingToExecute;
+import static accord.local.cfk.CommandsForKey.InternalStatus.PREACCEPTED_OR_ACCEPTED_INVALIDATE;
+
+interface NotifySink
+{
+ void notWaiting(SafeCommandStore safeStore, TxnId txnId, Key key);
+
+ void notWaiting(SafeCommandStore safeStore, SafeCommand safeCommand, Key key);
+
+ void waitingOnCommit(SafeCommandStore safeStore, TxnInfo uncommitted, Key key);
+
+ class DefaultNotifySink implements NotifySink
+ {
+ static final DefaultNotifySink INSTANCE = new DefaultNotifySink();
+
+ @Override
+ public void notWaiting(SafeCommandStore safeStore, TxnId txnId, Key key)
+ {
+ SafeCommand safeCommand = safeStore.ifLoadedAndInitialised(txnId);
+ if (safeCommand != null) notWaiting(safeStore, safeCommand, key);
+ else
+ {
+ PreLoadContext context = PreLoadContext.contextFor(txnId);
+ safeStore.commandStore().execute(context, safeStore0 -> notWaiting(safeStore0, safeStore0.unsafeGet(txnId), key))
+ .begin(safeStore.agent());
+ }
+ }
+
+ @Override
+ public void notWaiting(SafeCommandStore safeStore, SafeCommand safeCommand, Key key)
+ {
+ Commands.removeWaitingOnKeyAndMaybeExecute(safeStore, safeCommand, key);
+ }
+
+ @Override
+ public void waitingOnCommit(SafeCommandStore safeStore, TxnInfo uncommitted, Key key)
+ {
+ TxnId txnId = uncommitted.plainTxnId();
+ if (uncommitted.status.compareTo(PREACCEPTED_OR_ACCEPTED_INVALIDATE) < 0)
+ {
+ Keys keys = Keys.of(key);
+ PreLoadContext context = PreLoadContext.contextFor(txnId, keys);
+ if (safeStore.canExecuteWith(context)) doNotifyMaybeInvalidWaitingOnCommit(safeStore, txnId, key, keys);
+ else
+ safeStore.commandStore().execute(PreLoadContext.contextFor(txnId, keys, COMMANDS), safeStore0 -> doNotifyMaybeInvalidWaitingOnCommit(safeStore0, txnId, key, keys)).begin(safeStore.agent());
+ }
+ else
+ {
+ safeStore.progressLog().waiting(txnId, WaitingToExecute, null, Keys.of(key).toParticipants());
+ }
+ }
+
+ private void doNotifyMaybeInvalidWaitingOnCommit(SafeCommandStore safeStore, TxnId txnId, Key key, Keys keys)
+ {
+ SafeCommand safeCommand = safeStore.unsafeGet(txnId);
+ safeCommand.initialise();
+ Command command = safeCommand.current();
+ Seekables<?, ?> keysOrRanges = command.keysOrRanges();
+ if (keysOrRanges == null || !keysOrRanges.contains(key))
+ {
+ CommonAttributes.Mutable attrs = command.mutable();
+ if (command.additionalKeysOrRanges() == null) attrs.additionalKeysOrRanges(keys);
+ else attrs.additionalKeysOrRanges(keys.with((Keys) command.additionalKeysOrRanges()));
+ safeCommand.update(safeStore, command.updateAttributes(attrs));
+ }
+ if (command.hasBeen(Status.Committed))
+ {
+ // if we're committed but not invalidated, that means EITHER we have raced with a commit+
+ // OR we adopted as a dependency a
+ safeStore.get(key).update(safeStore, safeCommand.current());
+ }
+ else
+ {
+ // TODO (desired): we could complicate our state machine to replicate PreCommitted here, so we can simply wait for ReadyToExclude
+ safeStore.progressLog().waiting(txnId, WaitingToExecute, null, keys.toParticipants());
+ }
+ }
+ }
+}
diff --git a/accord-core/src/main/java/accord/local/cfk/PostProcess.java b/accord-core/src/main/java/accord/local/cfk/PostProcess.java
new file mode 100644
index 00000000..a44fe8dc
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/PostProcess.java
@@ -0,0 +1,245 @@
+/*
+ * 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 accord.local.cfk;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import accord.api.Key;
+import accord.local.PreLoadContext;
+import accord.local.SafeCommand;
+import accord.local.SafeCommandStore;
+import accord.local.cfk.CommandsForKey.TxnInfo;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.utils.ArrayBuffers;
+import accord.utils.Invariants;
+import accord.utils.SortedArrays;
+import accord.utils.btree.BTree;
+
+import static accord.local.KeyHistory.COMMANDS;
+import static accord.local.cfk.CommandsForKey.InternalStatus.APPLIED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED;
+import static accord.local.cfk.CommandsForKey.Unmanaged.Pending.APPLY;
+import static accord.local.cfk.Updating.updateUnmanaged;
+import static accord.local.cfk.Updating.updateUnmanagedAsync;
+import static accord.local.cfk.Utils.findApply;
+import static accord.local.cfk.Utils.findCommit;
+import static accord.local.cfk.Utils.findFirstApply;
+import static accord.local.cfk.Utils.removeUnmanaged;
+import static accord.local.cfk.Utils.selectUnmanaged;
+
+abstract class PostProcess
+{
+ final PostProcess prev;
+
+ protected PostProcess(PostProcess prev)
+ {
+ this.prev = prev;
+ }
+
+ void postProcess(SafeCommandStore safeStore, Key key, NotifySink notifySink)
+ {
+ doNotify(safeStore, key, notifySink);
+ if (prev != null)
+ prev.postProcess(safeStore, key, notifySink);
+ }
+
+ abstract void doNotify(SafeCommandStore safeStore, Key key, NotifySink notifySink);
+
+ static class LoadPruned extends PostProcess
+ {
+ final TxnId[] load;
+
+ LoadPruned(PostProcess prev, TxnId[] load)
+ {
+ super(prev);
+ this.load = load;
+ }
+
+ void doNotify(SafeCommandStore safeStore, Key key, NotifySink notifySink)
+ {
+ SafeCommandsForKey safeCfk = safeStore.get(key);
+ for (TxnId txnId : load)
+ {
+ safeStore = safeStore; // make it unsafe for use in lambda
+ SafeCommand safeCommand = safeStore.ifLoadedAndInitialised(txnId);
+ if (safeCommand != null) load(safeStore, safeCommand, safeCfk, notifySink);
+ else
+ safeStore.commandStore().execute(PreLoadContext.contextFor(txnId, Keys.of(key), COMMANDS), safeStore0 -> {
+ load(safeStore0, safeStore0.unsafeGet(txnId), safeStore0.get(key), notifySink);
+ }).begin(safeStore.agent());
+ }
+ }
+
+ static void load(SafeCommandStore safeStore, SafeCommand safeCommand, SafeCommandsForKey safeCfk, NotifySink notifySink)
+ {
+ safeCfk.updatePruned(safeStore, safeCommand.current(), notifySink);
+ }
+
+ static CommandsForKeyUpdate load(TxnId[] txnIds, CommandsForKeyUpdate result)
+ {
+ if (txnIds.length == 0)
+ return result;
+ return new CommandsForKeyUpdate.CommandsForKeyUpdateWithNotifier(result.cfk(), new LoadPruned(result.postProcess(), txnIds));
+ }
+ }
+
+ static class NotifyNotWaiting extends PostProcess
+ {
+ final TxnId[] notify;
+
+ NotifyNotWaiting(PostProcess prev, TxnId[] notify)
+ {
+ super(prev);
+ this.notify = notify;
+ }
+
+ void doNotify(SafeCommandStore safeStore, Key key, NotifySink notifySink)
+ {
+ for (TxnId txnId : notify)
+ notifySink.notWaiting(safeStore, txnId, key);
+ }
+ }
+
+ static class NotifyUnmanagedOfCommit extends PostProcess
+ {
+ final TxnId[] notify;
+
+ NotifyUnmanagedOfCommit(PostProcess prev, TxnId[] notify)
+ {
+ super(prev);
+ this.notify = notify;
+ }
+
+ void doNotify(SafeCommandStore safeStore, Key key, NotifySink notifySink)
+ {
+ SafeCommandsForKey safeCfk = safeStore.get(key);
+ CommandsForKey cfk = safeCfk.current();
+ List<CommandsForKey.Unmanaged> addUnmanageds = new ArrayList<>();
+ for (TxnId txnId : notify)
+ {
+ SafeCommand safeCommand = safeStore.ifLoadedAndInitialised(txnId);
+ if (safeCommand != null)
+ {
+ Invariants.checkState(cfk == updateUnmanaged(cfk, safeStore, safeCommand, notifySink, false, addUnmanageds));
+ }
+ else
+ {
+ updateUnmanagedAsync(safeStore.commandStore(), txnId, key, notifySink);
+ }
+ }
+
+ if (!addUnmanageds.isEmpty())
+ {
+ CommandsForKey cur = safeCfk.current();
+ addUnmanageds.sort(CommandsForKey.Unmanaged::compareTo);
+ CommandsForKey.Unmanaged[] newUnmanageds = addUnmanageds.toArray(new CommandsForKey.Unmanaged[0]);
+ newUnmanageds = SortedArrays.linearUnion(cur.unmanageds, 0, cur.unmanageds.length, newUnmanageds, 0, newUnmanageds.length, CommandsForKey.Unmanaged::compareTo, ArrayBuffers.uncached(CommandsForKey.Unmanaged[]::new));
+ safeCfk.set(cur.update(newUnmanageds));
+ }
+ }
+ }
+
+ static CommandsForKeyUpdate notifyUnmanaged(CommandsForKey cfk, @Nullable TxnInfo curInfo, TxnInfo newInfo)
+ {
+ PostProcess notifier = null;
+ CommandsForKey.Unmanaged[] unmanageds = cfk.unmanageds;
+ {
+ // notify commit uses exclusive bounds, as we use minUndecided
+ Timestamp minUndecided = cfk.minUndecidedById < 0 ? Timestamp.MAX : cfk.byId[cfk.minUndecidedById];
+ if (!BTree.isEmpty(cfk.loadingPruned)) minUndecided = Timestamp.min(minUndecided, BTree.<Pruning.LoadingPruned>findByIndex(cfk.loadingPruned, 0));
+ int end = findCommit(unmanageds, minUndecided);
+ if (end > 0)
+ {
+ TxnId[] notifyUnmanaged = new TxnId[end];
+ for (int i = 0 ; i < end ; ++i)
+ notifyUnmanaged[i] = unmanageds[i].txnId;
+
+ unmanageds = Arrays.copyOfRange(unmanageds, end, unmanageds.length);
+ notifier = new PostProcess.NotifyUnmanagedOfCommit(null, notifyUnmanaged);
+ }
+ }
+
+ if (newInfo.status.compareTo(APPLIED) >= 0)
+ {
+ TxnInfo maxContiguousApplied = cfk.maxContiguousManagedApplied();
+ if (maxContiguousApplied != null && maxContiguousApplied.compareExecuteAt(newInfo) < 0)
+ maxContiguousApplied = null;
+
+ if (maxContiguousApplied != null)
+ {
+ int start = findFirstApply(unmanageds);
+ int end = findApply(unmanageds, start, maxContiguousApplied.executeAt);
+ if (start != end)
+ {
+ TxnId[] notifyNotWaiting = selectUnmanaged(unmanageds, start, end);
+ unmanageds = removeUnmanaged(unmanageds, start, end);
+ notifier = new PostProcess.NotifyNotWaiting(notifier, notifyNotWaiting);
+ }
+ }
+ }
+ if (newInfo.status == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED && curInfo != null && curInfo.status.isCommitted())
+ {
+ TxnInfo[] committedByExecuteAt = cfk.committedByExecuteAt;
+ // this is a rare edge case, but we might have unmanaged transactions waiting on this command we must re-schedule or notify
+ int start = findFirstApply(unmanageds);
+ int end = start;
+ while (end < unmanageds.length && unmanageds[end].waitingUntil.equals(curInfo.executeAt))
+ ++end;
+
+ if (start != end)
+ {
+ // find committed predecessor, if any
+ int predecessor = -2 - Arrays.binarySearch(committedByExecuteAt, curInfo, TxnInfo::compareExecuteAt);
+
+ if (predecessor >= 0)
+ {
+ int maxContiguousApplied = cfk.maxContiguousManagedAppliedIndex();
+ if (maxContiguousApplied >= predecessor)
+ predecessor = -1;
+ }
+
+ if (predecessor >= 0)
+ {
+ Timestamp waitingUntil = committedByExecuteAt[predecessor].plainExecuteAt();
+ unmanageds = unmanageds.clone();
+ for (int i = start ; i < end ; ++i)
+ unmanageds[i] = new CommandsForKey.Unmanaged(APPLY, unmanageds[i].txnId, waitingUntil);
+ }
+ else
+ {
+ TxnId[] notifyNotWaiting = selectUnmanaged(unmanageds, start, end);
+ unmanageds = removeUnmanaged(unmanageds, start, end);
+ notifier = new PostProcess.NotifyNotWaiting(notifier, notifyNotWaiting);
+ }
+ }
+ }
+
+ if (notifier == null)
+ return cfk;
+
+ return new CommandsForKeyUpdate.CommandsForKeyUpdateWithNotifier(new CommandsForKey(cfk, cfk.loadingPruned, unmanageds), notifier);
+ }
+
+}
diff --git a/accord-core/src/main/java/accord/local/cfk/Pruning.java b/accord-core/src/main/java/accord/local/cfk/Pruning.java
new file mode 100644
index 00000000..86d3f0e1
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/Pruning.java
@@ -0,0 +1,332 @@
+/*
+ * 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 accord.local.cfk;
+
+import java.util.Arrays;
+
+import accord.local.cfk.CommandsForKey.TxnInfo;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.utils.ArrayBuffers;
+import accord.utils.Invariants;
+import accord.utils.SortedArrays;
+import accord.utils.btree.BTree;
+import accord.utils.btree.BTreeRemoval;
+import accord.utils.btree.UpdateFunction;
+
+import static accord.local.cfk.CommandsForKey.InternalStatus.APPLIED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED;
+import static accord.local.cfk.CommandsForKey.NO_TXNIDS;
+import static accord.local.cfk.CommandsForKey.managesExecution;
+import static accord.local.cfk.Pruning.LoadingPruned.LOADINGF;
+import static accord.utils.ArrayBuffers.cachedTimestamps;
+import static accord.utils.ArrayBuffers.cachedTxnIds;
+
+public class Pruning
+{
+ /**
+ * A TxnId that we have witnessed as a dependency that predates {@link #prunedBefore}, so we must load its
+ * Command state to determine if this is a new transaction to track, or if it is an already-applied transaction
+ * we have pruned.
+ *
+ * Intended to be stored in a BTree, and includes static methods for managing the BTree.
+ */
+ static class LoadingPruned extends TxnId
+ {
+ static final UpdateFunction.Simple<LoadingPruned> LOADINGF = UpdateFunction.Simple.of(LoadingPruned::merge);
+
+ /**
+ * Transactions that had witnessed this pre-pruned TxnId and are therefore waiting for the load to complete
+ */
+ final TxnId[] witnessedBy;
+
+ public LoadingPruned(TxnId copy, TxnId[] witnessedBy)
+ {
+ super(copy);
+ this.witnessedBy = witnessedBy;
+ }
+
+ LoadingPruned merge(LoadingPruned that)
+ {
+ return new LoadingPruned(this, SortedArrays.linearUnion(witnessedBy, that.witnessedBy, cachedTxnIds()));
+ }
+
+ static Object[] empty()
+ {
+ return BTree.empty();
+ }
+ }
+
+ /**
+ * Updating {@code loadingPruned} to register that each element of {@code toLoad} is being loaded for {@code loadingFor}
+ */
+ static Object[] loadPruned(Object[] loadingPruned, TxnId[] toLoad, TxnId loadingFor)
+ {
+ return loadPruned(loadingPruned, toLoad, new TxnId[]{ loadingFor });
+ }
+
+ static Object[] loadPruned(Object[] loadingPruned, TxnId[] toLoad, TxnId[] loadingForAsList)
+ {
+ Object[] toLoadAsTree;
+ try (BTree.FastBuilder<LoadingPruned> fastBuilder = BTree.fastBuilder())
+ {
+ for (TxnId txnId : toLoad)
+ fastBuilder.add(new LoadingPruned(txnId, loadingForAsList));
+ toLoadAsTree = fastBuilder.build();
+ }
+ return BTree.update(loadingPruned, toLoadAsTree, LoadingPruned::compareTo, LOADINGF);
+ }
+
+ /**
+ * Find the list of TxnId that are waiting for {@code find} to load
+ */
+ static TxnId[] loadingPrunedFor(Object[] loadingPruned, TxnId find, TxnId[] ifNoMatch)
+ {
+ LoadingPruned obj = (LoadingPruned) BTree.find(loadingPruned, TxnId::compareTo, find);
+ if (obj == null)
+ return ifNoMatch;
+
+ return obj.witnessedBy;
+ }
+
+ /**
+ * Updating {@code loadingPruned} to remove {@code find}, as it has been loaded
+ */
+ static Object[] removeLoadingPruned(Object[] loadingPruned, TxnId find)
+ {
+ return BTreeRemoval.remove(loadingPruned, TxnId::compareTo, find);
+ }
+
+ /**
+ * Return true if {@code waitingId} is waiting for any transaction with a lower TxnId than waitingExecuteAt
+ */
+ static boolean isWaitingOnPruned(Object[] loadingPruned, TxnId waitingId, Timestamp waitingExecuteAt)
+ {
+ if (BTree.isEmpty(loadingPruned))
+ return false;
+
+ int ceilIndex = BTree.ceilIndex(loadingPruned, Timestamp::compareTo, waitingExecuteAt);
+ // TODO (desired): this is O(n.lg n), whereas we could import the accumulate function and perform in O(max(m, lg n))
+ for (int i = 0; i < ceilIndex; ++i)
+ {
+ LoadingPruned loading = BTree.findByIndex(loadingPruned, i);
+ if (!managesExecution(loading)) continue;
+ if (Arrays.binarySearch(loading.witnessedBy, waitingId) >= 0)
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return true if {@code waitingId} is waiting for any transaction with a lower TxnId than waitingExecuteAt
+ */
+ static boolean isAnyPredecessorWaitingOnPruned(Object[] loadingPruned, TxnId waitingId)
+ {
+ if (BTree.isEmpty(loadingPruned))
+ return false;
+
+ int ceilIndex = BTree.ceilIndex(loadingPruned, Timestamp::compareTo, waitingId);
+ // TODO (desired): this is O(n.lg n), whereas we could import the accumulate function and perform in O(max(m, lg n))
+ for (int i = 0; i < ceilIndex; ++i)
+ {
+ LoadingPruned loading = BTree.findByIndex(loadingPruned, i);
+ if (!managesExecution(loading)) continue;
+ // if we exactly match any, or if we sort after the first element then we're waiting for some txnId <= waitingId
+ if (-1 != Arrays.binarySearch(loading.witnessedBy, waitingId))
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Remove transitively redundant applied or invalidated commands
+ * @param pruneInterval the number of committed commands we must have prior to the first prune point candidate to trigger a prune attempt
+ * @param minHlcDelta do not prune any commands with an HLC within this distance of the prune point candidate
+ */
+ static CommandsForKey maybePrune(CommandsForKey cfk, int pruneInterval, long minHlcDelta)
+ {
+ TxnInfo newPrunedBefore;
+ {
+ if (cfk.maxAppliedWriteByExecuteAt < pruneInterval)
+ return cfk;
+
+ int i = cfk.maxAppliedWriteByExecuteAt;
+ long maxPruneHlc = cfk.committedByExecuteAt[i].executeAt.hlc() - minHlcDelta;
+ while (--i >= 0)
+ {
+ TxnInfo txn = cfk.committedByExecuteAt[i];
+ if (txn.kind().isWrite() && txn.executeAt.hlc() <= maxPruneHlc && txn.status == APPLIED)
+ break;
+ }
+
+ if (i < 0)
+ return cfk;
+
+ newPrunedBefore = cfk.committedByExecuteAt[i];
+ if (newPrunedBefore.compareTo(cfk.prunedBefore) <= 0)
+ return cfk;
+ }
+
+ int pos = cfk.insertPos(newPrunedBefore);
+ if (pos == 0)
+ return cfk;
+
+ return pruneBefore(cfk, newPrunedBefore, pos);
+ }
+
+ /**
+ * We can prune anything transitively applied where some later stable command replicates each of its missing array entries.
+ * These later commands can durably stand in for any recovery or dependency calculations.
+ */
+ static CommandsForKey pruneBefore(CommandsForKey cfk, TxnInfo newPrunedBefore, int pos)
+ {
+ Invariants.checkArgument(newPrunedBefore.compareTo(cfk.prunedBefore) >= 0, "Expect new prunedBefore to be ahead of existing one");
+
+ TxnInfo[] byId = cfk.byId;
+ int minUndecidedById = -1;
+ int retainCount = 0, removedCommittedCount = 0;
+ Timestamp[] removedExecuteAts = NO_TXNIDS;
+ int removedExecuteAtCount = 0;
+ TxnInfo[] newInfos;
+ {
+ ArrayBuffers.RecursiveObjectBuffers<TxnId> missingBuffers = new ArrayBuffers.RecursiveObjectBuffers<>(cachedTxnIds());
+ TxnId[] mergedMissing = newPrunedBefore.missing();
+ int mergedMissingCount = mergedMissing.length;
+ for (int i = 0 ; i < pos ; ++i)
+ {
+ TxnInfo txn = byId[i];
+ switch (txn.status)
+ {
+ default: throw new AssertionError("Unhandled status: " + txn.status);
+ case COMMITTED:
+ case STABLE:
+ ++retainCount;
+ break;
+
+ case HISTORICAL:
+ case TRANSITIVELY_KNOWN:
+ case PREACCEPTED_OR_ACCEPTED_INVALIDATE:
+ case ACCEPTED:
+ if (minUndecidedById < 0 && managesExecution(txn))
+ minUndecidedById = retainCount;
+ ++retainCount;
+ break;
+
+ case APPLIED:
+ if (txn.executeAt.compareTo(newPrunedBefore.executeAt) < 0)
+ {
+ TxnId[] missing = txn.missing();
+ if (missing == NO_TXNIDS || SortedArrays.isSubset(missing, 0, missing.length, mergedMissing, 0, mergedMissingCount))
+ {
+ if (missing != NO_TXNIDS)
+ {
+ if (removedExecuteAtCount == removedExecuteAts.length)
+ removedExecuteAts = cachedTimestamps().resize(removedExecuteAts, removedExecuteAtCount, Math.max(8, removedExecuteAtCount + (removedExecuteAtCount >> 1)));
+ removedExecuteAts[removedExecuteAtCount++] = txn.executeAt;
+ }
+ ++removedCommittedCount;
+ continue;
+ }
+ if (txn.executeAt == txn)
+ {
+ mergedMissing = SortedArrays.linearUnion(missing, missing.length, mergedMissing, mergedMissingCount, missingBuffers);
+ mergedMissingCount = missingBuffers.sizeOfLast(mergedMissing);
+ }
+ }
+ ++retainCount;
+
+ case INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED:
+ break;
+ }
+ }
+
+ if (pos == retainCount)
+ return cfk;
+
+ int removedByIdCount = pos - retainCount;
+ newInfos = new TxnInfo[byId.length - removedByIdCount];
+ if (minUndecidedById < 0 && cfk.minUndecidedById >= 0) // must occur later, so deduct all removals
+ minUndecidedById = cfk.minUndecidedById - removedByIdCount;
+ missingBuffers.discardBuffers();
+ }
+
+ { // copy to new byTxnId array
+ int insertPos = retainCount;
+ int removedExecuteAtPos = removedExecuteAtCount - 1;
+ for (int i = pos - 1; i >= 0 ; --i)
+ {
+ TxnInfo txn = byId[i];
+ if (txn.status == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ continue;
+
+ if (txn.status == APPLIED && txn.executeAt.compareTo(newPrunedBefore.executeAt) < 0)
+ {
+ TxnId[] missing = txn.missing();
+ if (missing == NO_TXNIDS)
+ continue;
+
+ if (removedExecuteAtPos >= 0 && removedExecuteAts[removedExecuteAtPos] == txn.executeAt)
+ {
+ --removedExecuteAtPos;
+ continue;
+ }
+ }
+
+ newInfos[--insertPos] = txn;
+ }
+ Invariants.checkState(retainCount + byId.length - pos == newInfos.length);
+ System.arraycopy(byId, pos, newInfos, retainCount, byId.length - pos);
+ }
+
+ TxnInfo[] committedByExecuteAt = cfk.committedByExecuteAt;
+ TxnInfo[] newCommittedByExecuteAt;
+ { // copy to new committedByExecuteAt array
+ Arrays.sort(removedExecuteAts, 0, removedExecuteAtCount);
+ newCommittedByExecuteAt = new TxnInfo[committedByExecuteAt.length - removedCommittedCount];
+ int sourcePos = Arrays.binarySearch(committedByExecuteAt, newPrunedBefore, TxnInfo::compareExecuteAt);
+ int insertPos = sourcePos - removedCommittedCount;
+ int removedExecuteAtPos = removedExecuteAtCount - 1;
+ for (int i = sourcePos - 1; i >= 0 ; --i)
+ {
+ TxnInfo txn = committedByExecuteAt[i];
+ if (txn.status == APPLIED && txn.compareTo(newPrunedBefore) < 0)
+ {
+ TxnId[] missing = txn.missing();
+ if (missing == NO_TXNIDS)
+ continue;
+
+ if (removedExecuteAtPos >= 0 && removedExecuteAts[removedExecuteAtPos] == txn.executeAt)
+ {
+ --removedExecuteAtPos;
+ continue;
+ }
+ }
+
+ newCommittedByExecuteAt[--insertPos] = txn;
+ }
+ System.arraycopy(committedByExecuteAt, sourcePos, newCommittedByExecuteAt, sourcePos - removedCommittedCount, committedByExecuteAt.length - sourcePos);
+ }
+
+ cachedTimestamps().forceDiscard(removedExecuteAts, removedExecuteAtCount);
+ int newMaxAppliedWriteByExecuteAt = cfk.maxAppliedWriteByExecuteAt - removedCommittedCount;
+ return new CommandsForKey(cfk.key, cfk.redundantBefore, newPrunedBefore, cfk.loadingPruned, newInfos, newCommittedByExecuteAt, minUndecidedById, newMaxAppliedWriteByExecuteAt, cfk.unmanageds);
+ }
+}
diff --git a/accord-core/src/main/java/accord/local/cfk/SafeCommandsForKey.java b/accord-core/src/main/java/accord/local/cfk/SafeCommandsForKey.java
new file mode 100644
index 00000000..46946c07
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/SafeCommandsForKey.java
@@ -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 accord.local.cfk;
+
+import javax.annotation.Nullable;
+
+import accord.api.Agent;
+import accord.api.Key;
+import accord.impl.SafeState;
+import accord.local.Command;
+import accord.local.RedundantBefore;
+import accord.local.SafeCommand;
+import accord.local.SafeCommandStore;
+import accord.local.Status;
+import accord.primitives.TxnId;
+
+public abstract class SafeCommandsForKey implements SafeState<CommandsForKey>
+{
+ private final Key key;
+
+ public SafeCommandsForKey(Key key)
+ {
+ this.key = key;
+ }
+
+ protected abstract void set(CommandsForKey update);
+
+ public Key key()
+ {
+ return key;
+ }
+
+ void updatePruned(SafeCommandStore safeStore, Command nextCommand, NotifySink notifySink)
+ {
+ CommandsForKey prevCfk = current();
+ update(safeStore, nextCommand, prevCfk, prevCfk.updatePruned(nextCommand), notifySink);
+ }
+
+ public void update(SafeCommandStore safeStore, Command nextCommand)
+ {
+ CommandsForKey prevCfk = current();
+ update(safeStore, nextCommand, prevCfk, prevCfk.update(nextCommand));
+ }
+
+ private void update(SafeCommandStore safeStore, @Nullable Command command, CommandsForKey prevCfk, CommandsForKeyUpdate updateCfk)
+ {
+ update(safeStore, command, prevCfk, updateCfk, NotifySink.DefaultNotifySink.INSTANCE);
+ }
+
+ private void update(SafeCommandStore safeStore, @Nullable Command command, CommandsForKey prevCfk, CommandsForKeyUpdate updateCfk, NotifySink notifySink)
+ {
+ if (updateCfk == prevCfk)
+ return;
+
+ CommandsForKey nextCfk = updateCfk.cfk();
+ if (nextCfk != prevCfk)
+ {
+ if (command != null && command.hasBeen(Status.Applied))
+ {
+ Agent agent = safeStore.agent();
+ nextCfk = nextCfk.maybePrune(agent.cfkPruneInterval(), agent.cfkHlcPruneDelta());
+ }
+ set(nextCfk);
+ }
+
+ updateCfk.postProcess(safeStore, prevCfk, command, notifySink);
+ }
+
+ public void registerUnmanaged(SafeCommandStore safeStore, SafeCommand unmanaged)
+ {
+ CommandsForKey prevCfk = current();
+ update(safeStore, null, prevCfk, prevCfk.registerUnmanaged(safeStore, unmanaged));
+ }
+
+ public void registerHistorical(SafeCommandStore safeStore, TxnId txnId)
+ {
+ CommandsForKey prevCfk = current();
+ update(safeStore, null, prevCfk, prevCfk.registerHistorical(txnId));
+ }
+
+ public void updateRedundantBefore(SafeCommandStore safeStore, RedundantBefore.Entry redundantBefore)
+ {
+ CommandsForKey prevCfk = current();
+ update(safeStore, null, prevCfk, prevCfk.withRedundantBeforeAtLeast(redundantBefore));
+ }
+
+ public void initialize()
+ {
+ set(new CommandsForKey(key));
+ }
+
+ public void refresh(SafeCommandStore safeStore)
+ {
+ updateRedundantBefore(safeStore, safeStore.commandStore().redundantBefore().get(key));
+ }
+}
diff --git a/accord-core/src/main/java/accord/local/cfk/Updating.java b/accord-core/src/main/java/accord/local/cfk/Updating.java
new file mode 100644
index 00000000..4b674f67
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/Updating.java
@@ -0,0 +1,898 @@
+/*
+ * 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 accord.local.cfk;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.Key;
+import accord.local.Command;
+import accord.local.CommandStore;
+import accord.local.PreLoadContext;
+import accord.local.SafeCommand;
+import accord.local.SafeCommandStore;
+import accord.local.Status;
+import accord.local.cfk.CommandsForKey.TxnInfo;
+import accord.primitives.Ballot;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import accord.primitives.Txn;
+import accord.primitives.TxnId;
+import accord.utils.Invariants;
+import accord.utils.RelationMultiMap;
+import accord.utils.SortedArrays;
+import accord.utils.SortedCursor;
+
+import static accord.local.KeyHistory.COMMANDS;
+import static accord.local.cfk.CommandsForKey.InternalStatus.APPLIED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.COMMITTED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.HISTORICAL;
+import static accord.local.cfk.CommandsForKey.InternalStatus.INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.TRANSITIVELY_KNOWN;
+import static accord.local.cfk.CommandsForKey.NO_TXNIDS;
+import static accord.local.cfk.CommandsForKey.managesExecution;
+import static accord.local.cfk.CommandsForKey.Unmanaged.Pending.APPLY;
+import static accord.local.cfk.CommandsForKey.Unmanaged.Pending.COMMIT;
+import static accord.local.cfk.Pruning.loadPruned;
+import static accord.local.cfk.Pruning.loadingPrunedFor;
+import static accord.local.cfk.Utils.insertMissing;
+import static accord.local.cfk.Utils.mergeAndFilterMissing;
+import static accord.local.cfk.Utils.missingTo;
+import static accord.local.cfk.Utils.removeOneMissing;
+import static accord.local.cfk.Utils.removePrunedAdditions;
+import static accord.local.cfk.Utils.validateMissing;
+import static accord.primitives.Txn.Kind.EphemeralRead;
+import static accord.primitives.Txn.Kind.ExclusiveSyncPoint;
+import static accord.primitives.Txn.Kind.Write;
+import static accord.utils.ArrayBuffers.cachedTxnIds;
+import static accord.utils.Invariants.Paranoia.NONE;
+import static accord.utils.Invariants.Paranoia.SUPERLINEAR;
+import static accord.utils.Invariants.ParanoiaCostFactor.LOW;
+import static accord.utils.Invariants.illegalState;
+import static accord.utils.Invariants.testParanoia;
+import static accord.utils.SortedArrays.Search.FAST;
+
+class Updating
+{
+ private static final Logger logger = LoggerFactory.getLogger(CommandsForKey.class);
+
+ /**
+ * A TxnInfo to insert alongside any dependencies we did not already know about
+ */
+ static class InfoWithAdditions
+ {
+ final TxnInfo info;
+ // a cachedTxnIds() array that should be returned once done
+ final TxnId[] additions;
+ final int additionCount;
+
+ InfoWithAdditions(TxnInfo info, TxnId[] additions, int additionCount)
+ {
+ this.info = info;
+ this.additions = additions;
+ this.additionCount = additionCount;
+ }
+ }
+
+ static CommandsForKeyUpdate insertOrUpdate(CommandsForKey cfk, int insertPos, int updatePos, TxnId plainTxnId, TxnInfo curInfo, CommandsForKey.InternalStatus newStatus, Command command)
+ {
+ // TODO (now): do not calculate any deps or additions if we're transitioning from Stable to Applied; wasted effort and might trigger LoadPruned
+ Object newInfoObj = computeInfoAndAdditions(cfk, insertPos, updatePos, plainTxnId, newStatus, command);
+ if (newInfoObj.getClass() != InfoWithAdditions.class)
+ return insertOrUpdate(cfk, insertPos, plainTxnId, curInfo, (TxnInfo)newInfoObj, false, null);
+
+ InfoWithAdditions newInfoWithAdditions = (InfoWithAdditions) newInfoObj;
+ TxnId[] additions = newInfoWithAdditions.additions;
+ int additionCount = newInfoWithAdditions.additionCount;
+ TxnInfo newInfo = newInfoWithAdditions.info;
+
+ TxnId[] prunedIds = removePrunedAdditions(additions, additionCount, cfk.prunedBefore());
+ Object[] newLoadingPruned = cfk.loadingPruned;
+ if (prunedIds != NO_TXNIDS)
+ {
+ additionCount -= prunedIds.length;
+ newLoadingPruned = Pruning.loadPruned(cfk.loadingPruned, prunedIds, plainTxnId);
+ }
+
+ int committedByExecuteAtUpdatePos = committedByExecuteAtUpdatePos(cfk.committedByExecuteAt, curInfo, newInfo);
+ TxnInfo[] newCommittedByExecuteAt = updateCommittedByExecuteAt(cfk, committedByExecuteAtUpdatePos, newInfo, false);
+ int newMaxAppliedWriteByExecuteAt = updateMaxAppliedWriteByExecuteAt(cfk, committedByExecuteAtUpdatePos, newInfo, newCommittedByExecuteAt, false);
+
+ TxnInfo[] byId = cfk.byId;
+ TxnInfo[] newById = new TxnInfo[byId.length + additionCount + (updatePos < 0 ? 1 : 0)];
+ insertOrUpdateWithAdditions(byId, insertPos, updatePos, plainTxnId, newInfo, additions, additionCount, newById, newCommittedByExecuteAt, NO_TXNIDS);
+ if (testParanoia(SUPERLINEAR, NONE, LOW))
+ validateMissing(newById, additions, additionCount, curInfo, newInfo, NO_TXNIDS);
+
+ int newMinUndecidedById = cfk.minUndecidedById;
+ {
+ TxnInfo curMinUndecided = cfk.minUndecided();
+ int additionsBeforeOldUndecided = additionCount;
+ if (curMinUndecided != null && additionCount > 0)
+ {
+ additionsBeforeOldUndecided = Arrays.binarySearch(additions, 0, additionCount, curMinUndecided);
+ if (additionsBeforeOldUndecided < 0)
+ additionsBeforeOldUndecided = -1 - additionsBeforeOldUndecided;
+ }
+
+ TxnId newMinUndecided = null;
+ for (int i = 0 ; i < additionsBeforeOldUndecided ; ++i)
+ {
+ TxnId addition = additions[i];
+ if (!managesExecution(addition))
+ continue;
+
+ newMinUndecided = addition;
+ break;
+ }
+
+ if ((newMinUndecided == null || newMinUndecided.compareTo(newInfo) > 0) && newInfo.status.compareTo(COMMITTED) < 0 && managesExecution(newInfo))
+ newMinUndecided = newInfo;
+
+ if (newMinUndecided != null && curMinUndecided != null && curMinUndecided.compareTo(newMinUndecided) < 0)
+ newMinUndecided = null;
+
+ if (updatePos < 0 && newMinUndecided == null && curMinUndecided != null && newInfo.compareTo(curMinUndecided) < 0)
+ ++additionsBeforeOldUndecided;
+
+ if (newMinUndecided == null && curMinUndecided == curInfo)
+ newMinUndecidedById = nextUndecided(newById, insertPos + 1);
+ else if (newMinUndecided == null && newMinUndecidedById >= 0)
+ newMinUndecidedById += additionsBeforeOldUndecided;
+ else if (newMinUndecided == newInfo)
+ newMinUndecidedById = insertPos + -1 - Arrays.binarySearch(additions, 0, additionCount, newInfo);
+ else if (newMinUndecided != null)
+ newMinUndecidedById = Arrays.binarySearch(newById, 0, newById.length, newMinUndecided);
+ }
+
+ cachedTxnIds().forceDiscard(additions, additionCount + prunedIds.length);
+ return PostProcess.LoadPruned.load(prunedIds, cfk.update(newById, newMinUndecidedById, newCommittedByExecuteAt, newMaxAppliedWriteByExecuteAt, newLoadingPruned, plainTxnId, curInfo, newInfo));
+ }
+
+ static Object computeInfoAndAdditions(CommandsForKey cfk, int insertPos, int updatePos, TxnId txnId, CommandsForKey.InternalStatus newStatus, Command command)
+ {
+ Invariants.checkState(newStatus.hasExecuteAtOrDeps);
+ Timestamp executeAt = command.executeAt();
+ if (executeAt.equals(txnId)) executeAt = txnId;
+ Ballot ballot = Ballot.ZERO;
+ if (newStatus.hasBallot)
+ ballot = command.acceptedOrCommitted();
+
+ Timestamp depsKnownBefore = newStatus.depsKnownBefore(txnId, executeAt);
+ SortedCursor<TxnId> deps = command.partialDeps().txnIds(cfk.key());
+ deps.find(cfk.redundantBefore().shardRedundantBefore());
+
+ return Updating.computeInfoAndAdditions(cfk.byId, insertPos, updatePos, txnId, newStatus, ballot, executeAt, depsKnownBefore, deps);
+ }
+
+ /**
+ * We return an Object here to avoid wasting allocations; most of the time we expect a new TxnInfo to be returned,
+ * but if we have transitive dependencies to insert we return an InfoWithAdditions
+ */
+ static Object computeInfoAndAdditions(TxnInfo[] byId, int insertPos, int updatePos, TxnId plainTxnId, CommandsForKey.InternalStatus newStatus, Ballot ballot, Timestamp executeAt, Timestamp depsKnownBefore, SortedCursor<TxnId> deps)
+ {
+ TxnId[] additions = NO_TXNIDS, missing = NO_TXNIDS;
+ int additionCount = 0, missingCount = 0;
+
+ // the position until which we should have witnessed transactions, i.e. for computing the missing collection
+ // *NOT* to be used for terminating *inserts* from deps parameter, as this may see the future
+ // (due to pruning sometimes including a later transaction where it cannot include all earlier ones)
+ int depsKnownBeforePos = insertPos;
+ if (depsKnownBefore != plainTxnId)
+ {
+ depsKnownBeforePos = Arrays.binarySearch(byId, insertPos, byId.length, depsKnownBefore);
+ Invariants.checkState(depsKnownBeforePos < 0);
+ depsKnownBeforePos = -1 - depsKnownBeforePos;
+ }
+
+ int txnIdsIndex = 0;
+ while (txnIdsIndex < byId.length && deps.hasCur())
+ {
+ TxnInfo t = byId[txnIdsIndex];
+ TxnId d = deps.cur();
+ int c = t.compareTo(d);
+ if (c == 0)
+ {
+ ++txnIdsIndex;
+ deps.advance();
+ }
+ else if (c < 0)
+ {
+ // we expect to be missing ourselves
+ // we also permit any transaction we have recorded as COMMITTED or later to be missing, as recovery will not need to consult our information
+ if (txnIdsIndex != updatePos && txnIdsIndex < depsKnownBeforePos && t.status.compareTo(COMMITTED) < 0 && plainTxnId.kind().witnesses(t))
+ {
+ if (missingCount == missing.length)
+ missing = cachedTxnIds().resize(missing, missingCount, Math.max(8, missingCount * 2));
+ missing[missingCount++] = t.plainTxnId();
+ }
+ txnIdsIndex++;
+ }
+ else
+ {
+ if (plainTxnId.kind().witnesses(d))
+ {
+ if (additionCount >= additions.length)
+ additions = cachedTxnIds().resize(additions, additionCount, Math.max(8, additionCount * 2));
+
+ additions[additionCount++] = d;
+ }
+ else
+ {
+ // we can take dependencies on ExclusiveSyncPoints to represent a GC point in the log
+ // if we don't ordinarily witness a transaction it is meaningless to include it as a dependency
+ // as we will not logically be able to work with it (the missing collection will not correctly represent it anyway)
+ Invariants.checkState(d.kind() == ExclusiveSyncPoint);
+ }
+ deps.advance();
+ }
+ }
+
+ if (deps.hasCur())
+ {
+ do
+ {
+ if (additionCount >= additions.length)
+ additions = cachedTxnIds().resize(additions, additionCount, Math.max(8, additionCount * 2));
+ additions[additionCount++] = deps.cur();
+ deps.advance();
+ }
+ while (deps.hasCur());
+ }
+ else if (txnIdsIndex < byId.length)
+ {
+ while (txnIdsIndex < depsKnownBeforePos)
+ {
+ if (txnIdsIndex != updatePos && byId[txnIdsIndex].status.compareTo(COMMITTED) < 0)
+ {
+ TxnId txnId = byId[txnIdsIndex].plainTxnId();
+ if ((plainTxnId.kind().witnesses(txnId)))
+ {
+ if (missingCount == missing.length)
+ missing = cachedTxnIds().resize(missing, missingCount, Math.max(8, missingCount * 2));
+ missing[missingCount++] = txnId;
+ }
+ }
+ txnIdsIndex++;
+ }
+ }
+
+ TxnInfo info = TxnInfo.create(plainTxnId, newStatus, executeAt, cachedTxnIds().completeAndDiscard(missing, missingCount), ballot);
+ if (additionCount == 0)
+ return info;
+
+ return new InfoWithAdditions(info, additions, additionCount);
+ }
+
+ static CommandsForKeyUpdate insertOrUpdate(CommandsForKey cfk, int pos, TxnId plainTxnId, TxnInfo curInfo, TxnInfo newInfo, boolean wasPruned, @Nullable TxnId[] loadingAsPrunedFor)
+ {
+ if (curInfo == newInfo)
+ return cfk;
+
+ Object[] newLoadingPruned = cfk.loadingPruned;
+ if (loadingAsPrunedFor != null) newLoadingPruned = Pruning.removeLoadingPruned(newLoadingPruned, plainTxnId);
+
+ int committedByExecuteAtUpdatePos = committedByExecuteAtUpdatePos(cfk.committedByExecuteAt, curInfo, newInfo);
+ TxnInfo[] newCommittedByExecuteAt = updateCommittedByExecuteAt(cfk, committedByExecuteAtUpdatePos, newInfo, wasPruned);
+ int newMaxAppliedWriteByExecuteAt = updateMaxAppliedWriteByExecuteAt(cfk, committedByExecuteAtUpdatePos, newInfo, newCommittedByExecuteAt, wasPruned);
+
+ int newMinUndecidedById = cfk.minUndecidedById;
+ TxnInfo[] byId = cfk.byId;
+ TxnInfo[] newById;
+ if (curInfo == null)
+ {
+ newById = new TxnInfo[byId.length + 1];
+ System.arraycopy(byId, 0, newById, 0, pos);
+ newById[pos] = newInfo;
+ System.arraycopy(byId, pos, newById, pos + 1, byId.length - pos);
+ if (managesExecution(newInfo))
+ {
+ if (newInfo.status.compareTo(COMMITTED) >= 0)
+ {
+ if (newMinUndecidedById < 0 || pos <= newMinUndecidedById)
+ {
+ if (pos < newMinUndecidedById) ++newMinUndecidedById;
+ else newMinUndecidedById = nextUndecided(newById, pos + 1);
+ }
+ }
+ else
+ {
+ if (newMinUndecidedById < 0 || pos < newMinUndecidedById)
+ newMinUndecidedById = pos;
+ }
+ }
+ else if (pos <= newMinUndecidedById)
+ ++newMinUndecidedById;
+ }
+ else
+ {
+ newById = byId.clone();
+ newById[pos] = newInfo;
+ if (pos == newMinUndecidedById && curInfo.status.compareTo(COMMITTED) < 0 && newInfo.status.compareTo(COMMITTED) >= 0)
+ newMinUndecidedById = nextUndecided(newById, pos + 1);
+ }
+
+ if (loadingAsPrunedFor == null)
+ loadingAsPrunedFor = NO_TXNIDS;
+
+ if (newCommittedByExecuteAt.length > cfk.committedByExecuteAt.length)
+ {
+ Utils.removeFromMissingArrays(newById, newCommittedByExecuteAt, plainTxnId);
+ }
+ else if (curInfo != null && curInfo.status.compareTo(COMMITTED) < 0 && newInfo.status == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ {
+ Utils.removeFromMissingArrays(newById, newCommittedByExecuteAt, plainTxnId);
+ }
+ else if (curInfo == null && newInfo.status != INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ {
+ // TODO (desired): for consistency, move this to insertOrUpdate (without additions), while maintaining the efficiency
+ Utils.addToMissingArrays(newById, newCommittedByExecuteAt, newInfo, plainTxnId, loadingAsPrunedFor);
+ }
+
+ if (testParanoia(SUPERLINEAR, NONE, LOW) && curInfo == null && newInfo.status.compareTo(COMMITTED) < 0)
+ validateMissing(newById, NO_TXNIDS, 0, curInfo, newInfo, loadingAsPrunedFor);
+
+ return cfk.update(newById, newMinUndecidedById, newCommittedByExecuteAt, newMaxAppliedWriteByExecuteAt, newLoadingPruned, plainTxnId, curInfo, newInfo);
+ }
+
+ /**
+ * Update newById to insert or update newInfo; insert any additions, and update any relevant missing arrays in
+ * both newById and newCommittedByExecuteAt (for both deletions and insertions).
+ */
+ static void insertOrUpdateWithAdditions(TxnInfo[] byId, int sourceInsertPos, int sourceUpdatePos, TxnId plainTxnId, TxnInfo newInfo, TxnId[] additions, int additionCount, TxnInfo[] newById, TxnInfo[] newCommittedByExecuteAt, @Nonnull TxnId[] witnessedBy)
+ {
+ int additionInsertPos = Arrays.binarySearch(additions, 0, additionCount, plainTxnId);
+ additionInsertPos = Invariants.checkArgument(-1 - additionInsertPos, additionInsertPos < 0);
+ int targetInsertPos = sourceInsertPos + additionInsertPos;
+
+ // we may need to insert or remove ourselves, depending on the new and prior status
+ boolean insertSelfMissing = sourceUpdatePos < 0 && newInfo.status.compareTo(COMMITTED) < 0;
+ boolean removeSelfMissing = sourceUpdatePos >= 0 && newInfo.status.compareTo(COMMITTED) >= 0 && byId[sourceUpdatePos].status.compareTo(COMMITTED) < 0;
+ // missingSource starts as additions, but if we insertSelfMissing at the relevant moment it becomes the merge of additions and plainTxnId
+ TxnId[] missingSource = additions;
+
+ // the most recently constructed pure insert missing array, so that it may be reused if possible
+ int i = 0, j = 0, missingCount = 0, missingLimit = additionCount, count = 0;
+ int minByExecuteAtSearchIndex = 0;
+ while (i < byId.length)
+ {
+ if (count == targetInsertPos)
+ {
+ newById[count] = newInfo;
+ if (i == sourceUpdatePos) ++i;
+ else if (insertSelfMissing) ++missingCount;
+ ++count;
+ continue;
+ }
+
+ int c = j == additionCount ? -1 : byId[i].compareTo(additions[j]);
+ if (c < 0)
+ {
+ TxnInfo txn = byId[i];
+ if (i == sourceUpdatePos)
+ {
+ txn = newInfo;
+ }
+ else if (txn.status.hasDeps())
+ {
+ Timestamp depsKnownBefore = txn.depsKnownBefore();
+ if (insertSelfMissing && missingSource == additions && (missingCount != j || (depsKnownBefore != txn && depsKnownBefore.compareTo(plainTxnId) > 0)))
+ {
+ // add ourselves to the missing collection
+ missingSource = insertMissing(additions, additionCount, plainTxnId, additionInsertPos);
+ ++missingLimit;
+ }
+
+ int to = missingTo(txn, depsKnownBefore, missingSource, missingCount, missingLimit);
+ if (to > 0 || removeSelfMissing)
+ {
+ TxnId[] curMissing = txn.missing();
+ TxnId[] newMissing = curMissing;
+ if (to > 0)
+ {
+ TxnId skipInsertMissing = null;
+ if (Arrays.binarySearch(witnessedBy, plainTxnId) >= 0)
+ skipInsertMissing = plainTxnId;
+
+ newMissing = mergeAndFilterMissing(txn, curMissing, missingSource, to, skipInsertMissing);
+ }
+
+ if (removeSelfMissing)
+ newMissing = removeOneMissing(newMissing, plainTxnId);
+
+ if (newMissing != curMissing)
+ {
+ TxnInfo newTxn = txn.update(newMissing);
+
+ if (txn.status.isCommitted())
+ {
+ // update newCommittedByExecuteAt
+ int ci;
+ if (txn.executeAt == txn)
+ {
+ ci = SortedArrays.exponentialSearch(newCommittedByExecuteAt, minByExecuteAtSearchIndex, newCommittedByExecuteAt.length, txn, TxnInfo::compareExecuteAt, FAST);
+ minByExecuteAtSearchIndex = 1 + ci;
+ }
+ else
+ {
+ ci = Arrays.binarySearch(newCommittedByExecuteAt, minByExecuteAtSearchIndex, newCommittedByExecuteAt.length, txn, TxnInfo::compareExecuteAt);
+ }
+ Invariants.checkState(newCommittedByExecuteAt[ci] == txn);
+ newCommittedByExecuteAt[ci] = newTxn;
+ }
+
+ txn = newTxn;
+ }
+ }
+ }
+ newById[count] = txn;
+ i++;
+ }
+ else if (c > 0)
+ {
+ TxnId txnId = additions[j++];
+ newById[count] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId, Ballot.ZERO);
+ ++missingCount;
+ }
+ else
+ {
+ throw illegalState(byId[i] + " should be an insertion, but found match when merging with origin");
+ }
+ count++;
+ }
+
+ if (j < additionCount)
+ {
+ if (count <= targetInsertPos)
+ {
+ while (count < targetInsertPos)
+ {
+ TxnId txnId = additions[j++];
+ newById[count++] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId, Ballot.ZERO);
+ }
+ newById[targetInsertPos] = newInfo;
+ count = targetInsertPos + 1;
+ }
+ while (j < additionCount)
+ {
+ TxnId txnId = additions[j++];
+ newById[count++] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId, Ballot.ZERO);
+ }
+ }
+ else if (count == targetInsertPos)
+ {
+ newById[targetInsertPos] = newInfo;
+ }
+ }
+
+ /**
+ * Similar to insertOrUpdateWithAdditions, but when we only need to insert some additions (i.e. when calling registerUnmanaged)
+ */
+ static TxnInfo[] insertAdditionsOnly(TxnInfo[] byId, TxnInfo[] committedByExecuteAt, TxnInfo[] newInfos, TxnId[] additions, int additionCount)
+ {
+ // the most recently constructed pure insert missing array, so that it may be reused if possible
+ int minByExecuteAtSearchIndex = 0;
+ int i = 0, j = 0, missingCount = 0, count = 0;
+ TxnInfo[] newCommittedByExecuteAt = null;
+ while (i < byId.length)
+ {
+ int c = j == additionCount ? -1 : byId[i].compareTo(additions[j]);
+ if (c < 0)
+ {
+ TxnInfo txn = byId[i];
+ if (txn.status.hasDeps())
+ {
+ Timestamp depsKnownBefore = txn.depsKnownBefore();
+
+ int to = missingTo(txn, depsKnownBefore, additions, missingCount, additionCount);
+ if (to > 0)
+ {
+ TxnId[] prevMissing = txn.missing();
+ TxnId[] newMissing = mergeAndFilterMissing(txn, prevMissing, additions, to, null);
+ if (newMissing != prevMissing)
+ {
+ TxnInfo newTxn = txn.update(newMissing);
+ if (txn.status.isCommitted())
+ {
+ if (newCommittedByExecuteAt == null)
+ newCommittedByExecuteAt = committedByExecuteAt.clone();
+
+ int ci;
+ if (txn.executeAt == txn)
+ {
+ ci = SortedArrays.exponentialSearch(newCommittedByExecuteAt, minByExecuteAtSearchIndex, newCommittedByExecuteAt.length, txn, TxnInfo::compareExecuteAt, FAST);
+ minByExecuteAtSearchIndex = 1 + ci;
+ }
+ else
+ {
+ ci = Arrays.binarySearch(newCommittedByExecuteAt, minByExecuteAtSearchIndex, newCommittedByExecuteAt.length, txn, TxnInfo::compareExecuteAt);
+ }
+ Invariants.checkState(newCommittedByExecuteAt[ci] == txn);
+ newCommittedByExecuteAt[ci] = newTxn;
+ }
+ txn = newTxn;
+ }
+ }
+ }
+ newInfos[count] = txn;
+ i++;
+ }
+ else if (c > 0)
+ {
+ TxnId txnId = additions[j++];
+ newInfos[count] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId, Ballot.ZERO);
+ ++missingCount;
+ }
+ else
+ {
+ throw illegalState(byId[i] + " should be an insertion, but found match when merging with origin");
+ }
+ count++;
+ }
+
+ while (j < additionCount)
+ {
+ TxnId txnId = additions[j++];
+ newInfos[count++] = TxnInfo.create(txnId, TRANSITIVELY_KNOWN, txnId, Ballot.ZERO);
+ }
+
+ if (newCommittedByExecuteAt == null)
+ newCommittedByExecuteAt = committedByExecuteAt;
+ return newCommittedByExecuteAt;
+ }
+
+ static int committedByExecuteAtUpdatePos(TxnInfo[] committedByExecuteAt, @Nullable TxnInfo curInfo, TxnInfo newInfo)
+ {
+ if (newInfo.status.isCommitted())
+ {
+ return Arrays.binarySearch(committedByExecuteAt, 0, committedByExecuteAt.length, newInfo, TxnInfo::compareExecuteAt);
+ }
+ else if (curInfo != null && curInfo.status.isCommitted() && newInfo.status == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ {
+ return Arrays.binarySearch(committedByExecuteAt, 0, committedByExecuteAt.length, curInfo, TxnInfo::compareExecuteAt);
+ }
+ return committedByExecuteAt.length;
+ }
+
+ private static TxnInfo[] updateCommittedByExecuteAt(CommandsForKey cfk, int pos, TxnInfo newInfo, boolean wasPruned)
+ {
+ TxnInfo[] committedByExecuteAt = cfk.committedByExecuteAt;
+ if (pos == committedByExecuteAt.length)
+ return committedByExecuteAt;
+
+ if (newInfo.status != INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ {
+ if (pos >= 0 && committedByExecuteAt[pos].equals(newInfo))
+ {
+ TxnInfo[] result = committedByExecuteAt.clone();
+ result[pos] = newInfo;
+ return result;
+ }
+ else
+ {
+ if (pos >= 0) logger.error("Execution timestamp clash on key {}: {} and {} both have executeAt {}", cfk.key(), newInfo.plainTxnId(), committedByExecuteAt[pos].plainTxnId(), newInfo.executeAt);
+ else pos = -1 - pos;
+
+ TxnInfo[] result = new TxnInfo[committedByExecuteAt.length + 1];
+ System.arraycopy(committedByExecuteAt, 0, result, 0, pos);
+ result[pos] = newInfo;
+ System.arraycopy(committedByExecuteAt, pos, result, pos + 1, committedByExecuteAt.length - pos);
+
+ int maxAppliedWriteByExecuteAt = cfk.maxAppliedWriteByExecuteAt;
+ if (pos <= maxAppliedWriteByExecuteAt)
+ {
+ if (pos < maxAppliedWriteByExecuteAt && !wasPruned && cfk.redundantBefore.bootstrappedAt.compareTo(newInfo) < 0)
+ {
+ for (int i = pos; i <= maxAppliedWriteByExecuteAt; ++i)
+ {
+ if (committedByExecuteAt[pos].kind().witnesses(newInfo))
+ logger.error("Linearizability violation on key {}: {} is committed to execute (at {}) before {} that should witness it but has already applied (at {})", cfk.key, newInfo.plainTxnId(), newInfo.plainExecuteAt(), committedByExecuteAt[i].plainTxnId(), committedByExecuteAt[i].plainExecuteAt());
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+ else
+ {
+ // we can transition from COMMITTED, STABLE or APPLIED to INVALID_OR_TRUNCATED if the local command store ERASES a command
+ // in this case, we not only need to update committedByExecuteAt, we also need to update any unmanaged transactions that
+ // might have been waiting for this command's execution, to either wait for the preceding committed command or else to stop waiting
+ TxnInfo[] newInfos = new TxnInfo[committedByExecuteAt.length - 1];
+ System.arraycopy(committedByExecuteAt, 0, newInfos, 0, pos);
+ System.arraycopy(committedByExecuteAt, pos + 1, newInfos, pos, newInfos.length - pos);
+ return newInfos;
+ }
+ }
+
+ private static int updateMaxAppliedWriteByExecuteAt(CommandsForKey cfk, int pos, TxnInfo newInfo, TxnInfo[] newCommittedByExecuteAt, boolean wasPruned)
+ {
+ int maxAppliedWriteByExecuteAt = cfk.maxAppliedWriteByExecuteAt;
+ if (pos == cfk.committedByExecuteAt.length)
+ return maxAppliedWriteByExecuteAt;
+
+ boolean inserted = pos < 0;
+ if (inserted) pos = -1 - pos;
+ if (pos <= maxAppliedWriteByExecuteAt)
+ {
+ if (inserted)
+ {
+ return maxAppliedWriteByExecuteAt + 1;
+ }
+ else if (newInfo.status == INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED)
+ {
+ // deleted
+ int newMaxAppliedWriteByExecuteAt = maxAppliedWriteByExecuteAt - 1;
+ while (newMaxAppliedWriteByExecuteAt >= 0 && newCommittedByExecuteAt[newMaxAppliedWriteByExecuteAt].kind() != Write)
+ --newMaxAppliedWriteByExecuteAt;
+ return newMaxAppliedWriteByExecuteAt;
+ }
+ }
+ else if (newInfo.status == APPLIED)
+ {
+ return maybeUpdateMaxAppliedAndCheckForLinearizabilityViolations(cfk, pos, newInfo.kind(), newInfo, wasPruned);
+ }
+ return maxAppliedWriteByExecuteAt;
+ }
+
+ private static int maybeUpdateMaxAppliedAndCheckForLinearizabilityViolations(CommandsForKey cfk, int appliedPos, Txn.Kind appliedKind, TxnInfo applied, boolean wasPruned)
+ {
+ if (!wasPruned && cfk.redundantBefore.bootstrappedAt.compareTo(applied) < 0)
+ {
+ TxnInfo[] committedByExecuteAt = cfk.committedByExecuteAt;
+ for (int i = cfk.maxAppliedWriteByExecuteAt + 1; i < appliedPos ; ++i)
+ {
+ if (committedByExecuteAt[i].status != APPLIED && appliedKind.witnesses(committedByExecuteAt[i]))
+ logger.error("Linearizability violation on key {}: {} is committed to execute (at {}) before {} that should witness it but has already applied (at {})", cfk.key, committedByExecuteAt[i].plainTxnId(), committedByExecuteAt[i].plainExecuteAt(), applied.plainTxnId(), applied.plainExecuteAt());
+ }
+ }
+
+ return appliedKind == Txn.Kind.Write ? appliedPos : cfk.maxAppliedWriteByExecuteAt;
+ }
+
+ static int nextUndecided(TxnInfo[] infos, int pos)
+ {
+ while (true)
+ {
+ if (pos == infos.length)
+ return -1;
+
+ if (infos[pos].status.compareTo(COMMITTED) < 0 && managesExecution(infos[pos]))
+ return pos;
+
+ ++pos;
+ }
+ }
+
+ static void updateUnmanagedAsync(CommandStore commandStore, TxnId txnId, Key key, NotifySink notifySink)
+ {
+ PreLoadContext context = PreLoadContext.contextFor(txnId, Keys.of(key), COMMANDS);
+ commandStore.execute(context, safeStore -> {
+ SafeCommandsForKey safeCommandsForKey = safeStore.get(key);
+ CommandsForKey cur = safeCommandsForKey.current();
+ CommandsForKey next = Updating.updateUnmanaged(cur, safeStore, safeStore.unsafeGet(txnId), notifySink);
+ if (cur != next)
+ safeCommandsForKey.set(next);
+ }).begin(commandStore.agent());
+ }
+
+ static CommandsForKey updateUnmanaged(CommandsForKey cfk, SafeCommandStore safeStore, SafeCommand safeCommand, NotifySink notifySink)
+ {
+ CommandsForKeyUpdate upd = Updating.updateUnmanaged(cfk, safeStore, safeCommand, notifySink, false, null);
+ Invariants.checkState(upd instanceof CommandsForKey);
+ return (CommandsForKey) upd;
+ }
+
+ /**
+ * Three modes of operation:
+ * - {@code register}: inserts any missing dependencies from safeCommand into the collection; may return CommandsForKeyUpdate
+ * - {@code !register, update == null}: fails if any dependencies are missing; always returns a CommandsForKey
+ * - {@code !register && update != null}: fails if any dependencies are missing; always returns the original CommandsForKey, and maybe adds a new Unmanaged to {@code update}
+ */
+ static CommandsForKeyUpdate updateUnmanaged(CommandsForKey cfk, SafeCommandStore safeStore, SafeCommand safeCommand, NotifySink notifySink, boolean register, @Nullable List<CommandsForKey.Unmanaged> update)
+ {
+ Invariants.checkArgument(!register || update == null);
+ if (safeCommand.current().hasBeen(Status.Truncated))
+ return cfk;
+
+ Command.Committed command = safeCommand.current().asCommitted();
+ TxnId waitingTxnId = command.txnId();
+ Timestamp waitingExecuteAt = command.executeAt();
+
+ TxnInfo[] byId = cfk.byId;
+ RelationMultiMap.SortedRelationList<TxnId> txnIds = command.partialDeps().keyDeps.txnIds(cfk.key());
+ TxnId[] missing = NO_TXNIDS;
+ int missingCount = 0;
+ int i = txnIds.find(cfk.shardRedundantBefore());
+ if (i < 0) i = -1 - i;
+ if (i < txnIds.size())
+ {
+ Txn.Kind waitingKind = waitingTxnId.kind();
+ boolean readyToApply = true; // our dependencies have applied, so we are ready to apply
+ boolean waitingToApply = true; // our dependencies have committed, so we know when we execute and are waiting
+ Timestamp executesAt = null;
+ int j = SortedArrays.binarySearch(byId, 0, byId.length, txnIds.get(i), Timestamp::compareTo, FAST);
+
+ if (j < 0) j = -1 -j;
+ while (i < txnIds.size())
+ {
+ int c = j == byId.length ? -1 : txnIds.get(i).compareTo(byId[j]);
+ if (c == 0)
+ {
+ TxnInfo txn = byId[j];
+ if (txn.status.compareTo(COMMITTED) < 0) waitingToApply = readyToApply = false;
+ else if (txn.status != INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED
+ && (txn.executeAt.compareTo(waitingExecuteAt) < 0
+ || waitingKind == EphemeralRead
+ || (waitingKind == ExclusiveSyncPoint && txn.compareTo(waitingTxnId) < 0)))
+ {
+ readyToApply &= txn.status == APPLIED;
+ executesAt = Timestamp.nonNullOrMax(executesAt, txn.executeAt);
+ }
+ ++i;
+ ++j;
+ }
+ else if (c > 0)
+ {
+ if (waitingKind.isSyncPoint())
+ {
+ // we special-case sync points to handle pruning of transactions they should have witnessed as dependencies.
+ // We require all earlier TxnId to be decided before we can execute; we must be informed of all the necessary TxnId
+ // transitively by the "future" dependency provided for this purpose in mapReduceActive, so simply ensuring
+ // all earlier TxnId are committed is sufficient to compute the correct executeAt.
+ TxnInfo txn = byId[j];
+ if (managesExecution(txn))
+ {
+ if (txn.status.compareTo(COMMITTED) < 0) readyToApply = waitingToApply = false;
+ else if (txn.status != INVALID_OR_TRUNCATED_OR_UNMANAGED_COMMITTED
+ && (txn.executeAt.compareTo(waitingExecuteAt) < 0
+ || waitingKind == EphemeralRead
+ || (waitingKind == ExclusiveSyncPoint && txn.compareTo(waitingTxnId) < 0)))
+ {
+ readyToApply &= txn.status == APPLIED;
+ executesAt = Timestamp.nonNullOrMax(executesAt, txn.executeAt);
+ }
+ }
+ }
+ ++j;
+ }
+ else if (!managesExecution(txnIds.get(i))) ++i;
+ else if (register)
+ {
+ readyToApply = waitingToApply = false;
+ if (missingCount == missing.length)
+ missing = cachedTxnIds().resize(missing, missingCount, Math.max(8, missingCount + missingCount/2));
+ missing[missingCount++] = txnIds.get(i++);
+ }
+ else
+ {
+ Invariants.checkState(txnIds.get(i++).compareTo(cfk.prunedBefore) < 0);
+ }
+ }
+
+ if (waitingKind.isSyncPoint() && Pruning.isAnyPredecessorWaitingOnPruned(cfk.loadingPruned, waitingTxnId))
+ readyToApply = waitingToApply = false;
+
+ if (!readyToApply)
+ {
+ TxnInfo[] newById = byId, newCommittedByExecuteAt = cfk.committedByExecuteAt;
+ int newMinUndecidedById = cfk.minUndecidedById;
+ Object[] newLoadingPruned = cfk.loadingPruned;
+ TxnId[] loadPruned = NO_TXNIDS;
+ int clearMissingCount = missingCount;
+ if (missingCount > 0)
+ {
+ int prunedIndex = Arrays.binarySearch(missing, 0, missingCount, cfk.prunedBefore());
+ if (prunedIndex < 0) prunedIndex = -1 - prunedIndex;
+ if (prunedIndex > 0)
+ {
+ loadPruned = Arrays.copyOf(missing, prunedIndex);
+ newLoadingPruned = Pruning.loadPruned(cfk.loadingPruned, loadPruned, waitingTxnId);
+ }
+
+ if (prunedIndex != missingCount)
+ {
+ missingCount -= prunedIndex;
+ System.arraycopy(missing, prunedIndex, missing, 0, missingCount);
+ newById = new TxnInfo[byId.length + missingCount];
+ newCommittedByExecuteAt = insertAdditionsOnly(byId, cfk.committedByExecuteAt, newById, missing, missingCount);
+ // we can safely use missing[prunedIndex] here because we only fill missing with transactions for which we manage execution
+ newMinUndecidedById = Arrays.binarySearch(newById, TxnId.nonNullOrMin(missing[0], cfk.minUndecided()));
+ }
+ }
+ cachedTxnIds().discard(missing, clearMissingCount);
+
+ CommandsForKey.Unmanaged newPendingRecord;
+ if (waitingToApply)
+ {
+ if (executesAt instanceof TxnInfo)
+ executesAt = ((TxnInfo) executesAt).plainExecuteAt();
+
+ if (waitingTxnId.kind().awaitsOnlyDeps() && executesAt != null)
+ {
+ if (executesAt.compareTo(command.waitingOn.executeAtLeast(Timestamp.NONE)) > 0)
+ {
+ Command.WaitingOn.Update waitingOn = new Command.WaitingOn.Update(command.waitingOn);
+ waitingOn.updateExecuteAtLeast(executesAt);
+ safeCommand.updateWaitingOn(waitingOn);
+ }
+ }
+
+ newPendingRecord = new CommandsForKey.Unmanaged(APPLY, command.txnId(), executesAt);
+ }
+ else newPendingRecord = new CommandsForKey.Unmanaged(COMMIT, command.txnId(), txnIds.get(txnIds.size() - 1));
+
+ if (update != null)
+ {
+ update.add(newPendingRecord);
+ return cfk;
+ }
+
+ CommandsForKey.Unmanaged[] newUnmanaged = SortedArrays.insert(cfk.unmanageds, newPendingRecord, CommandsForKey.Unmanaged[]::new);
+
+ CommandsForKey result;
+ if (newById == byId) result = new CommandsForKey(cfk, newLoadingPruned, newUnmanaged);
+ else result = new CommandsForKey(cfk.key(), cfk.redundantBefore(), cfk.prunedBefore, newLoadingPruned, newById, newCommittedByExecuteAt, newMinUndecidedById, cfk.maxAppliedWriteByExecuteAt, newUnmanaged);
+
+ if (loadPruned == NO_TXNIDS)
+ return result;
+
+ return new CommandsForKeyUpdate.CommandsForKeyUpdateWithNotifier(result, new PostProcess.LoadPruned(null, loadPruned));
+ }
+ }
+
+ notifySink.notWaiting(safeStore, safeCommand, cfk.key());
+ return cfk;
+ }
+
+ static CommandsForKeyUpdate registerHistorical(CommandsForKey cfk, TxnId txnId)
+ {
+ if (txnId.compareTo(cfk.shardRedundantBefore()) < 0)
+ return cfk;
+
+ int i = Arrays.binarySearch(cfk.byId, txnId);
+ if (i >= 0)
+ {
+ if (cfk.byId[i].status.compareTo(HISTORICAL) >= 0)
+ return cfk;
+ return cfk.update(i, txnId, cfk.byId[i], TxnInfo.create(txnId, HISTORICAL, txnId, Ballot.ZERO), false, null);
+ }
+ else if (txnId.compareTo(cfk.prunedBefore) >= 0)
+ {
+ return cfk.insert(-1 - i, txnId, TxnInfo.create(txnId, HISTORICAL, txnId, Ballot.ZERO), false, null);
+ }
+ else
+ {
+ TxnId[] loadingPrunedFor = loadingPrunedFor(cfk.loadingPruned, txnId, null);
+ if (loadingPrunedFor != null && Arrays.binarySearch(loadingPrunedFor, txnId) >= 0)
+ return cfk;
+
+ TxnId[] txnIdArray = new TxnId[] { txnId };
+ Object[] newLoadingPruned = loadPruned(cfk.loadingPruned, txnIdArray, NO_TXNIDS);
+ return PostProcess.LoadPruned.load(txnIdArray, cfk.update(newLoadingPruned));
+ }
+ }
+}
diff --git a/accord-core/src/main/java/accord/local/cfk/Utils.java b/accord-core/src/main/java/accord/local/cfk/Utils.java
new file mode 100644
index 00000000..109e684f
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/cfk/Utils.java
@@ -0,0 +1,393 @@
+/*
+ * 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 accord.local.cfk;
+
+import java.util.Arrays;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import accord.local.cfk.CommandsForKey.TxnInfo;
+import accord.primitives.Timestamp;
+import accord.primitives.Txn;
+import accord.primitives.TxnId;
+import accord.utils.Invariants;
+import accord.utils.SortedArrays;
+import net.nicoulaj.compilecommand.annotations.Inline;
+
+import static accord.local.cfk.CommandsForKey.InternalStatus.ACCEPTED;
+import static accord.local.cfk.CommandsForKey.InternalStatus.COMMITTED;
+import static accord.local.cfk.CommandsForKey.NO_TXNIDS;
+import static accord.local.cfk.CommandsForKey.Unmanaged.Pending.COMMIT;
+import static accord.utils.ArrayBuffers.cachedTxnIds;
+import static accord.utils.SortedArrays.Search.FAST;
+
+class Utils
+{
+ static void validateMissing(TxnInfo[] byId, TxnId[] additions, int additionCount, TxnInfo curInfo, TxnInfo newInfo, @Nonnull TxnId[] shouldNotHaveMissing)
+ {
+ for (TxnInfo txn : byId)
+ {
+ if (txn == newInfo) continue;
+ if (!txn.status.hasDeps()) continue;
+ int additionIndex = Arrays.binarySearch(additions, 0, additionCount, txn.depsKnownBefore());
+ if (additionIndex < 0) additionIndex = -1 - additionIndex;
+ TxnId[] missing = txn.missing();
+ int j = 0;
+ for (int i = 0 ; i < additionIndex ; ++i)
+ {
+ if (!txn.kind().witnesses(additions[i])) continue;
+ j = SortedArrays.exponentialSearch(missing, j, missing.length, additions[i]);
+ if (shouldNotHaveMissing != NO_TXNIDS && Arrays.binarySearch(shouldNotHaveMissing, txn) >= 0) Invariants.checkState(j < 0);
+ else Invariants.checkState(j >= 0);
+ }
+ if (curInfo == null && newInfo.status.compareTo(COMMITTED) < 0 && txn.kind().witnesses(newInfo) && txn.depsKnownBefore().compareTo(newInfo) > 0 && (shouldNotHaveMissing == NO_TXNIDS || Arrays.binarySearch(shouldNotHaveMissing, txn) < 0))
+ Invariants.checkState(Arrays.binarySearch(missing, newInfo) >= 0);
+ }
+ }
+
+ /**
+ * {@code removeTxnId} no longer needs to be tracked in missing arrays;
+ * remove it from byId and committedByExecuteAt, ensuring both arrays still reference the same TxnInfo where updated
+ */
+ static void removeFromMissingArrays(TxnInfo[] byId, TxnInfo[] committedByExecuteAt, TxnId removeTxnId)
+ {
+ int startIndex = SortedArrays.binarySearch(committedByExecuteAt, 0, committedByExecuteAt.length, removeTxnId, (id, info) -> id.compareTo(info.executeAt), FAST);
+ if (startIndex < 0) startIndex = -1 - startIndex;
+ else ++startIndex;
+
+ int minSearchIndex = Arrays.binarySearch(byId, removeTxnId) + 1;
+ for (int i = startIndex ; i < committedByExecuteAt.length ; ++i)
+ {
+ int newMinSearchIndex;
+ {
+ TxnInfo txn = committedByExecuteAt[i];
+ if (txn.getClass() == TxnInfo.class) continue;
+ if (!txn.kind().witnesses(removeTxnId)) continue;
+
+ TxnId[] missing = txn.missing();
+ TxnId[] newMissing = removeOneMissing(missing, removeTxnId);
+ if (missing == newMissing) continue;
+
+ newMinSearchIndex = updateInfoArraysByExecuteAt(i, txn, txn.update(newMissing), minSearchIndex, byId, committedByExecuteAt);
+ }
+
+ minSearchIndex = removeFromMissingArraysById(byId, minSearchIndex, newMinSearchIndex, removeTxnId);
+ }
+
+ removeFromMissingArraysById(byId, minSearchIndex, byId.length, removeTxnId);
+ }
+
+ /**
+ * {@code removeTxnId} no longer needs to be tracked in missing arrays;
+ * remove it from a range of byId ACCEPTED status entries only, that could not be tracked via committedByExecuteAt
+ */
+ static int removeFromMissingArraysById(TxnInfo[] byId, int from, int to, TxnId removeTxnId)
+ {
+ for (int i = from ; i < to ; ++i)
+ {
+ TxnInfo txn = byId[i];
+ if (txn.getClass() == TxnInfo.class) continue;
+ if (!txn.status.hasExecuteAtOrDeps) continue;
+ if (!txn.kind().witnesses(removeTxnId)) continue;
+ if (txn.status != ACCEPTED) continue;
+
+ TxnId[] missing = txn.missing();
+ TxnId[] newMissing = removeOneMissing(missing, removeTxnId);
+ if (missing == newMissing) continue;
+ byId[i] = txn.update(newMissing);
+ }
+ return to;
+ }
+
+ /**
+ * {@code insertTxnId} needs to be tracked in missing arrays;
+ * add it to byId and committedByExecuteAt, ensuring both arrays still reference the same TxnInfo where updated
+ * Do not insert it into any members of {@code doNotInsert} as these are known to have witnessed {@code insertTxnId}
+ */
+ static void addToMissingArrays(TxnInfo[] byId, TxnInfo[] committedByExecuteAt, TxnInfo newInfo, TxnId insertTxnId, @Nonnull TxnId[] doNotInsert)
+ {
+ TxnId[] oneMissing = null;
+
+ int startIndex = SortedArrays.binarySearch(committedByExecuteAt, 0, committedByExecuteAt.length, insertTxnId, (id, info) -> id.compareTo(info.executeAt), FAST);
+ if (startIndex < 0) startIndex = -1 - startIndex;
+ else ++startIndex;
+
+ int minByIdSearchIndex = Arrays.binarySearch(byId, insertTxnId) + 1;
+ int minDoNotInsertSearchIndex = 0;
+ for (int i = startIndex ; i < committedByExecuteAt.length ; ++i)
+ {
+ int newMinSearchIndex;
+ {
+ TxnInfo txn = committedByExecuteAt[i];
+ if (txn == newInfo) continue;
+ if (!txn.kind().witnesses(insertTxnId)) continue;
+
+ if (doNotInsert != NO_TXNIDS)
+ {
+ if (txn.executeAt == txn)
+ {
+ int j = SortedArrays.exponentialSearch(doNotInsert, 0, doNotInsert.length, txn);
+ if (j >= 0)
+ {
+ minDoNotInsertSearchIndex = j;
+ continue;
+ }
+ minDoNotInsertSearchIndex = -1 -j;
+ }
+ else
+ {
+ if (Arrays.binarySearch(doNotInsert, txn) >= 0)
+ continue;
+ }
+ }
+
+ TxnId[] missing = txn.missing();
+ if (missing == NO_TXNIDS) missing = oneMissing = ensureOneMissing(insertTxnId, oneMissing);
+ else missing = SortedArrays.insert(missing, insertTxnId, TxnId[]::new);
+
+ newMinSearchIndex = updateInfoArraysByExecuteAt(i, txn, txn.update(missing), minByIdSearchIndex, byId, committedByExecuteAt);
+ }
+
+ for (; minByIdSearchIndex < newMinSearchIndex ; ++minByIdSearchIndex)
+ {
+ TxnInfo txn = byId[minByIdSearchIndex];
+ if (txn == newInfo) continue;
+ if (!txn.status.hasExecuteAtOrDeps) continue;
+ if (!txn.kind().witnesses(insertTxnId)) continue;
+ if (txn.status != ACCEPTED) continue;
+ if (minDoNotInsertSearchIndex < doNotInsert.length && doNotInsert[minDoNotInsertSearchIndex].equals(txn))
+ {
+ ++minDoNotInsertSearchIndex;
+ continue;
+ }
+
+ TxnId[] missing = txn.missing();
+ if (missing == NO_TXNIDS) missing = oneMissing = ensureOneMissing(insertTxnId, oneMissing);
+ else missing = SortedArrays.insert(missing, insertTxnId, TxnId[]::new);
+ byId[minByIdSearchIndex] = txn.update(missing);
+ }
+ }
+
+ for (; minByIdSearchIndex < byId.length ; ++minByIdSearchIndex)
+ {
+ TxnInfo txn = byId[minByIdSearchIndex];
+ if (txn == newInfo) continue;
+ if (!txn.status.hasExecuteAtOrDeps) continue;
+ if (!txn.kind().witnesses(insertTxnId)) continue;
+ if (txn.status != ACCEPTED) continue;
+ if (minDoNotInsertSearchIndex < doNotInsert.length && doNotInsert[minDoNotInsertSearchIndex].equals(txn))
+ {
+ ++minDoNotInsertSearchIndex;
+ continue;
+ }
+
+ TxnId[] missing = txn.missing();
+ if (missing == NO_TXNIDS) missing = oneMissing = ensureOneMissing(insertTxnId, oneMissing);
+ else missing = SortedArrays.insert(missing, insertTxnId, TxnId[]::new);
+ byId[minByIdSearchIndex] = txn.update(missing);
+ }
+ }
+
+ /**
+ * Take an index in {@code committedByExecuteAt}, find the companion entry in {@code byId}, and update both of them.
+ * Return the updated minSearchIndex used for querying {@code byId} - this will be updated only if txnId==executeAt
+ */
+ @Inline
+ static int updateInfoArraysByExecuteAt(int i, TxnInfo prevTxn, TxnInfo newTxn, int minSearchIndex, TxnInfo[] byId, TxnInfo[] committedByExecuteAt)
+ {
+ int j;
+ if (prevTxn.executeAt == prevTxn)
+ {
+ j = SortedArrays.exponentialSearch(byId, minSearchIndex, byId.length, prevTxn, TxnInfo::compareTo, FAST);
+ minSearchIndex = 1 + j;
+ }
+ else
+ {
+ j = Arrays.binarySearch(byId, prevTxn);
+ }
+ Invariants.checkState(byId[j] == prevTxn);
+ byId[j] = committedByExecuteAt[i] = newTxn;
+ return minSearchIndex;
+ }
+
+ static TxnId[] removePrunedAdditions(TxnId[] additions, int additionCount, TxnId prunedBefore)
+ {
+ if (additions[0].compareTo(prunedBefore) >= 0)
+ return NO_TXNIDS;
+
+ int prunedIndex = Arrays.binarySearch(additions, 1, additionCount, prunedBefore);
+ if (prunedIndex < 0) prunedIndex = -1 - prunedIndex;
+ if (prunedIndex == 0)
+ return NO_TXNIDS;
+
+ TxnId[] prunedIds = new TxnId[prunedIndex];
+ System.arraycopy(additions, 0, prunedIds, 0, prunedIndex);
+ System.arraycopy(additions, prunedIndex, additions, 0, additionCount - prunedIndex);
+ return prunedIds;
+ }
+
+ /**
+ * If a {@code missing} contains {@code removeTxnId}, return a new array without it (or NO_TXNIDS if the only entry)
+ */
+ static TxnId[] removeOneMissing(TxnId[] missing, TxnId removeTxnId)
+ {
+ if (missing == NO_TXNIDS) return NO_TXNIDS;
+
+ int j = Arrays.binarySearch(missing, removeTxnId);
+ if (j < 0) return missing;
+
+ if (missing.length == 1)
+ return NO_TXNIDS;
+
+ int length = missing.length;
+ TxnId[] newMissing = new TxnId[length - 1];
+ System.arraycopy(missing, 0, newMissing, 0, j);
+ System.arraycopy(missing, j + 1, newMissing, j, length - (1 + j));
+ return newMissing;
+ }
+
+ static TxnId[] removeRedundantMissing(TxnId[] missing, TxnId removeBefore)
+ {
+ if (missing == NO_TXNIDS)
+ return NO_TXNIDS;
+
+ int j = Arrays.binarySearch(missing, removeBefore);
+ if (j < 0) j = -1 - j;
+ if (j <= 0) return missing;
+ if (j == missing.length) return NO_TXNIDS;
+ return Arrays.copyOfRange(missing, j, missing.length);
+ }
+
+ static TxnId[] ensureOneMissing(TxnId txnId, TxnId[] oneMissing)
+ {
+ return oneMissing != null ? oneMissing : new TxnId[] { txnId };
+ }
+
+ static TxnId[] insertMissing(TxnId[] additions, int additionCount, TxnId updateTxnId, int additionInsertPos)
+ {
+ TxnId[] result = new TxnId[additionCount + 1];
+ System.arraycopy(additions, 0, result, 0, additionInsertPos);
+ System.arraycopy(additions, additionInsertPos, result, additionInsertPos + 1, additionCount - additionInsertPos);
+ result[additionInsertPos] = updateTxnId;
+ return result;
+ }
+
+ static int missingTo(TxnId txnId, Timestamp depsKnownBefore, TxnId[] missingSource, int missingCount, int missingLimit)
+ {
+ if (depsKnownBefore == txnId) return missingCount;
+ int to = Arrays.binarySearch(missingSource, 0, missingLimit, depsKnownBefore);
+ if (to < 0) to = -1 - to;
+ return to;
+ }
+
+ /**
+ * Insert the contents of {@code additions} up to {@code additionCount} into {@code current}, ignoring {@code skipAddition} if not null.
+ * Only insert entries that would be witnessed by {@code owner}.
+ */
+ static TxnId[] mergeAndFilterMissing(TxnId owner, TxnId[] current, TxnId[] additions, int additionCount, @Nullable TxnId skipAddition)
+ {
+ Txn.Kind.Kinds kinds = owner.kind().witnesses();
+ int additionLength = additionCount;
+ for (int i = additionCount - 1 ; i >= 0 ; --i)
+ {
+ if (!kinds.test(additions[i].kind()))
+ --additionCount;
+ }
+
+ if (additionCount == (skipAddition == null ? 0 : 1))
+ return current;
+
+ TxnId[] buffer = cachedTxnIds().get(current.length + (skipAddition == null ? additionCount : additionCount - 1));
+ int i = 0, j = 0, count = 0;
+ while (i < additionLength && j < current.length)
+ {
+ if (kinds.test(additions[i].kind()))
+ {
+ int c = additions[i].compareTo(current[j]);
+ if (c < 0)
+ {
+ TxnId addition = additions[i++];
+ if (addition != skipAddition)
+ buffer[count++] = addition;
+ }
+ else
+ {
+ buffer[count++] = current[j++];
+ }
+ }
+ else i++;
+ }
+ while (i < additionLength)
+ {
+ if (kinds.test(additions[i].kind()))
+ {
+ TxnId addition = additions[i];
+ if (addition != skipAddition)
+ buffer[count++] = addition;
+ }
+ i++;
+ }
+ while (j < current.length)
+ {
+ buffer[count++] = current[j++];
+ }
+ Invariants.checkState(count == additionCount + current.length);
+ return cachedTxnIds().completeAndDiscard(buffer, count);
+ }
+
+ static TxnId[] selectUnmanaged(CommandsForKey.Unmanaged[] unmanageds, int start, int end)
+ {
+ TxnId[] notifyNotWaiting = new TxnId[end - start];
+ for (int i = start ; i < end ; ++i)
+ {
+ CommandsForKey.Unmanaged unmanaged = unmanageds[i];
+ TxnId txnId = unmanaged.txnId;
+ notifyNotWaiting[i - start] = txnId;
+ }
+ return notifyNotWaiting;
+ }
+
+ static CommandsForKey.Unmanaged[] removeUnmanaged(CommandsForKey.Unmanaged[] unmanageds, int start, int end)
+ {
+ CommandsForKey.Unmanaged[] newUnmanageds = new CommandsForKey.Unmanaged[unmanageds.length - (end - start)];
+ System.arraycopy(unmanageds, 0, newUnmanageds, 0, start);
+ System.arraycopy(unmanageds, end, newUnmanageds, start, unmanageds.length - end);
+ return newUnmanageds;
+ }
+
+ static int findCommit(CommandsForKey.Unmanaged[] unmanageds, Timestamp exclusive)
+ {
+ return -1 - SortedArrays.exponentialSearch(unmanageds, 0, unmanageds.length, exclusive, (f, v) -> {
+ if (v.pending != COMMIT) return -1;
+ return f.compareTo(v.waitingUntil) > 0 ? 1 : -1;
+ }, FAST);
+ }
+
+ static int findFirstApply(CommandsForKey.Unmanaged[] unmanageds)
+ {
+ return -1 - SortedArrays.binarySearch(unmanageds, 0, unmanageds.length, null, (f, v) -> v.pending == COMMIT ? 1 : -1, FAST);
+ }
+
+ static int findApply(CommandsForKey.Unmanaged[] unmanageds, int start, Timestamp inclusive)
+ {
+ return -1 - SortedArrays.binarySearch(unmanageds, start, unmanageds.length, inclusive, (f, v) -> {
+ return f.compareTo(v.waitingUntil) >= 0 ? 1 : -1;
+ }, FAST);
+ }
+}
diff --git a/accord-core/src/main/java/accord/messages/BeginRecovery.java b/accord-core/src/main/java/accord/messages/BeginRecovery.java
index c20c6f3d..f04e0ac9 100644
--- a/accord-core/src/main/java/accord/messages/BeginRecovery.java
+++ b/accord-core/src/main/java/accord/messages/BeginRecovery.java
@@ -130,12 +130,7 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
}
else
{
- // TODO (expected): if we can combine these with the earlierAcceptedNoWitness we can avoid persisting deps on Accept
- // the problem is we need some way to ensure liveness. If we were to store witnessedAt as a separate register
- // we could return these and filter them by whatever the max witnessedAt is that we discover, OR we could
- // filter on replicas to exclude any that are started after anything that is committed, since they will have to adopt
- // them as a dependency (but we have to make sure we consider dependency rules, so if there's no write and only reads)
- // we might still have new transactions block our execution.
+ // TODO (expected): modify the mapReduce API to perform this check in a single pass
Ranges ranges = safeStore.ranges().allAt(txnId);
rejectsFastPath = hasAcceptedOrCommittedStartedAfterWithoutWitnessing(safeStore, txnId, ranges, partialTxn.keys());
if (!rejectsFastPath)
@@ -363,7 +358,7 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
* TODO (required): consider carefully how _adding_ ranges to a CommandStore affects this
*/
return safeStore.mapReduceFull(keys, ranges, startedAfter, startedAfter.kind().witnessedBy(), STARTED_AFTER, WITHOUT, IS_PROPOSED,
- (p1, keyOrRange, txnId, executeAt, prev) -> true, null, false);
+ (p1, keyOrRange, txnId, executeAt, prev) -> true, null, false);
}
private static boolean hasStableExecutesAfterWithoutWitnessing(SafeCommandStore safeStore, TxnId executesAfter, Ranges ranges, Seekables<?, ?> keys)
diff --git a/accord-core/src/main/java/accord/messages/CheckStatus.java b/accord-core/src/main/java/accord/messages/CheckStatus.java
index c2446b70..a6cf86cf 100644
--- a/accord-core/src/main/java/accord/messages/CheckStatus.java
+++ b/accord-core/src/main/java/accord/messages/CheckStatus.java
@@ -157,7 +157,7 @@ public class CheckStatus extends AbstractEpochRequest<CheckStatus.CheckStatusRep
if (!command.has(Known.DefinitionOnly) && Route.isRoute(query) && safeStore.ranges().allAt(txnId.epoch()).contains(Route.castToRoute(query).homeKey()))
Commands.informHome(safeStore, safeCommand, Route.castToRoute(query));
- InvalidIfNot invalidIfNotAtLeast = invalidIfNot(safeStore, command);
+ InvalidIfNot invalidIfNotAtLeast = invalidIfNot(safeStore, command, command.route());
boolean isCoordinating = isCoordinating(node, command);
Durability durability = command.durability();
Route<?> route = command.route();
@@ -202,11 +202,14 @@ public class CheckStatus extends AbstractEpochRequest<CheckStatus.CheckStatusRep
else node.reply(replyTo, replyContext, ok, null);
}
- private InvalidIfNot invalidIfNot(SafeCommandStore safeStore, Command command)
+ private InvalidIfNot invalidIfNot(SafeCommandStore safeStore, Command command, @Nullable Route<?> route)
{
if (command.known().isDecidedToExecute())
return NotKnownToBeInvalid;
- return Infer.invalidIfNot(safeStore, txnId, query);
+ InvalidIfNot invalidIfNot = Infer.invalidIfNot(safeStore, txnId, query);
+ if (route != null)
+ invalidIfNot = invalidIfNot.atLeast(Infer.invalidIfNot(safeStore, txnId, route));
+ return invalidIfNot;
}
public interface CheckStatusReply extends Reply
diff --git a/accord-core/src/main/java/accord/messages/Commit.java b/accord-core/src/main/java/accord/messages/Commit.java
index 25f083c4..dd7a392b 100644
--- a/accord-core/src/main/java/accord/messages/Commit.java
+++ b/accord-core/src/main/java/accord/messages/Commit.java
@@ -161,6 +161,7 @@ public class Commit extends TxnRequest<CommitOrReadNack>
public static void commitMinimal(Node node, Topologies coordinateEpochOnly, Ballot ballot, TxnId txnId, Txn txn, FullRoute<?> route, Timestamp executeAt, Deps unstableDeps, Callback<ReadReply> callback)
{
+ Invariants.checkArgument(coordinateEpochOnly.size() == 1, "Invalid coordinate epochs: %s", coordinateEpochOnly);
// we want to send to everyone, and we want to include all of the relevant data, but we stabilise on the coordination epoch replica responses
Topology coordinates = coordinateEpochOnly.forEpoch(txnId.epoch());
Topologies all = coordinateEpochOnly;
diff --git a/accord-core/src/main/java/accord/messages/Propagate.java b/accord-core/src/main/java/accord/messages/Propagate.java
index a6d209e0..cb23080e 100644
--- a/accord-core/src/main/java/accord/messages/Propagate.java
+++ b/accord-core/src/main/java/accord/messages/Propagate.java
@@ -138,7 +138,7 @@ public class Propagate implements EpochSupplier, LocalRequest<Status.Known>, Pre
on.mapReduceConsumeLocal(this, route, txnId.epoch(), toEpoch, this);
}
- @SuppressWarnings({"rawtypes", "unchecked"})
+ @SuppressWarnings({"rawtypes"})
public static void propagate(Node node, TxnId txnId, long sourceEpoch, WithQuorum withQuorum, Route route, @Nullable Status.Known target, CheckStatus.CheckStatusOkFull full, BiConsumer<Status.Known, Throwable> callback)
{
propagate(node, txnId, sourceEpoch, sourceEpoch, withQuorum, route, target, full, callback);
diff --git a/accord-core/src/main/java/accord/messages/ReadData.java b/accord-core/src/main/java/accord/messages/ReadData.java
index 2746b6d0..f5921613 100644
--- a/accord-core/src/main/java/accord/messages/ReadData.java
+++ b/accord-core/src/main/java/accord/messages/ReadData.java
@@ -148,7 +148,7 @@ public abstract class ReadData extends AbstractEpochRequest<ReadData.CommitOrRea
@Override
public PreLoadContext listenerPreLoadContext(TxnId caller)
{
- return PreLoadContext.contextFor(txnId, caller, keys());
+ return PreLoadContext.contextFor(txnId, keys());
}
@Override
@@ -192,6 +192,7 @@ public abstract class ReadData extends AbstractEpochRequest<ReadData.CommitOrRea
++waitingOnCount;
safeCommand.addListener(this);
safeStore.progressLog().waiting(safeCommand, executeOn().min.execution, null, readScope);
+ beginWaiting(safeStore, false);
return status.compareTo(SaveStatus.Stable) >= 0 ? null : Insufficient;
case OBSOLETE:
@@ -202,6 +203,7 @@ public abstract class ReadData extends AbstractEpochRequest<ReadData.CommitOrRea
waitingOn.set(safeStore.commandStore().id());
reading.set(safeStore.commandStore().id());
++waitingOnCount;
+ beginWaiting(safeStore, true);
safeCommand.addListener(this);
read(safeStore, safeCommand.current());
return null;
@@ -351,31 +353,37 @@ public abstract class ReadData extends AbstractEpochRequest<ReadData.CommitOrRea
}
}
- void cancel(@Nullable SafeCommandStore safeStore)
+ void beginCancel(@Nullable SafeCommandStore safeStore)
{
if (safeStore != null)
{
- SafeCommand safeCommand = safeStore.ifInitialised(txnId);
- if (safeCommand != null) safeCommand.removeListener(this);
- waitingOn.clear(safeStore.commandStore().id());
+ int id = safeStore.commandStore().id();
+ cancelWaiting(safeStore);
+ waitingOn.clear(id);
}
// TODO (expected): efficient unsubscribe mechanism
- if (waitingOn != null)
- node.commandStores().mapReduceConsume(this, waitingOn.stream(), forEach(in -> {
- SafeCommand safeCommand = in.ifInitialised(txnId);
- if (safeCommand != null) safeCommand.removeListener(this);
- }, node.agent()));
+ node.commandStores().mapReduceConsume(this, waitingOn.stream(), forEach(safeStore0 -> cancelWaiting(safeStore0), node.agent()));
state = State.OBSOLETE;
- waitingOn = null;
+ waitingOn.clear();
reading = null;
data = null;
unavailable = null;
}
+ protected void beginWaiting(SafeCommandStore safeStore, boolean isExecuting)
+ {
+ }
+
+ protected void cancelWaiting(SafeCommandStore safeStore)
+ {
+ SafeCommand safeCommand = safeStore.ifInitialised(txnId);
+ if (safeCommand != null) safeCommand.removeListener(this);
+ }
+
void onFailure(@Nullable SafeCommandStore safeStore, CommitOrReadNack failReply, Throwable throwable)
{
- cancel(safeStore);
+ beginCancel(safeStore);
if (throwable != null)
{
node.reply(replyTo, replyContext, null, throwable);
diff --git a/accord-core/src/main/java/accord/messages/ReadEphemeralTxnData.java b/accord-core/src/main/java/accord/messages/ReadEphemeralTxnData.java
index 75c21262..e170c313 100644
--- a/accord-core/src/main/java/accord/messages/ReadEphemeralTxnData.java
+++ b/accord-core/src/main/java/accord/messages/ReadEphemeralTxnData.java
@@ -108,7 +108,7 @@ public class ReadEphemeralTxnData extends ReadData
if (retryInLaterEpoch > 0)
{
// TODO (expected): wait for all stores' results and report only the ranges that execute later to be retried
- cancel(safeStore);
+ beginCancel(safeStore);
node.reply(replyTo, replyContext, new ReadOkWithFutureEpoch(null, null, retryInLaterEpoch), null);
}
super.read(safeStore, command);
diff --git a/accord-core/src/main/java/accord/messages/WaitOnCommit.java b/accord-core/src/main/java/accord/messages/WaitOnCommit.java
index 69353977..c79f0e24 100644
--- a/accord-core/src/main/java/accord/messages/WaitOnCommit.java
+++ b/accord-core/src/main/java/accord/messages/WaitOnCommit.java
@@ -187,7 +187,7 @@ public class WaitOnCommit implements Request, MapReduceConsume<SafeCommandStore,
@Override
public PreLoadContext listenerPreLoadContext(TxnId caller)
{
- return PreLoadContext.contextFor(txnId, caller, keys());
+ return PreLoadContext.contextFor(txnId, keys());
}
@Override
diff --git a/accord-core/src/main/java/accord/primitives/AbstractKeys.java b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
index 3d9548e7..59ead203 100644
--- a/accord-core/src/main/java/accord/primitives/AbstractKeys.java
+++ b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
@@ -29,6 +29,7 @@ import java.util.stream.Stream;
import accord.api.RoutingKey;
import accord.utils.ArrayBuffers.ObjectBuffers;
+import accord.utils.IndexedBiConsumer;
import accord.utils.IndexedFold;
import accord.utils.IndexedFoldToLong;
import accord.utils.IndexedTriFold;
@@ -258,6 +259,15 @@ public abstract class AbstractKeys<K extends RoutableKey> implements Iterable<K>
Routables.foldl(this, rs, (k, consumer, i) -> { consumer.accept(k); return consumer; }, forEach);
}
+ @Inline
+ public final <P1> void forEach(Ranges rs, IndexedBiConsumer<P1, ? super K> forEach, P1 p1)
+ {
+ Routables.foldl(this, rs, (p, ignore, k, consumer, i) -> {
+ consumer.accept(p, k, i);
+ return consumer;
+ }, p1, null, forEach, i -> false);
+ }
+
@Inline
public final long foldl(Ranges rs, IndexedFoldToLong<? super K> fold, long param, long initialValue, long terminalValue)
{
@@ -276,6 +286,12 @@ public abstract class AbstractKeys<K extends RoutableKey> implements Iterable<K>
return initialValue;
}
+ public void forEach(Consumer<? super K> forEach)
+ {
+ for (K key : keys)
+ forEach.accept(key);
+ }
+
public final FullKeyRoute toRoute(RoutingKey homeKey)
{
if (isEmpty())
diff --git a/accord-core/src/main/java/accord/primitives/Ballot.java b/accord-core/src/main/java/accord/primitives/Ballot.java
index fa940314..5dbb5bda 100644
--- a/accord-core/src/main/java/accord/primitives/Ballot.java
+++ b/accord-core/src/main/java/accord/primitives/Ballot.java
@@ -24,6 +24,9 @@ public class Ballot extends Timestamp
{
public static Ballot fromBits(long msb, long lsb, Id node)
{
+ if (msb == 0 && lsb == 0 && node.equals(Id.NONE))
+ return Ballot.ZERO;
+
return new Ballot(msb, lsb, node);
}
@@ -34,6 +37,9 @@ public class Ballot extends Timestamp
public static Ballot fromValues(long epoch, long hlc, int flags, Id node)
{
+ if (epoch == 0 && hlc == 0 && flags == 0 && node.equals(Id.NONE))
+ return ZERO;
+
return new Ballot(epoch, hlc, flags, node);
}
diff --git a/accord-core/src/main/java/accord/primitives/Deps.java b/accord-core/src/main/java/accord/primitives/Deps.java
index d8e134d0..2286dca4 100644
--- a/accord-core/src/main/java/accord/primitives/Deps.java
+++ b/accord-core/src/main/java/accord/primitives/Deps.java
@@ -19,11 +19,13 @@
package accord.primitives;
import accord.api.Key;
+import accord.local.cfk.CommandsForKey;
import accord.utils.Invariants;
+import accord.utils.MergeFewDisjointSortedListsCursor;
+import accord.utils.SortedCursor;
+import accord.utils.SortedList;
import java.util.AbstractList;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -31,11 +33,32 @@ import java.util.function.Predicate;
import javax.annotation.Nullable;
/**
- * A collection of transaction dependencies, keyed by the key or range on which they were adopted
+ * A collection of transaction dependencies, keyed by the key or range on which they were adopted.
+ *
+ * Dependencies are used in the following phases:
+ *
+ * - PreAccept calculates dependencies that contain TxnId < the proposed TxnId
+ * - if the fast path is taken, these dependencies are used immediately for execution
+ * - if the slow path is taken, these dependencies are proposed in the Accept phase
+ * - Accept phase calculates dependencies that contain TxnId < the proposed executeAt; these are merged with those found during the PreAccept phase
+ * - Commit phase simply makes these dependencies durable
+ *
+ * Dependencies are mostly calculated by CommandsForKey, which has some efficiency tricks, including pruning both itself
+ * and transitive dependencies in any calculation. This means that
+ * - Dependencies will omit any transaction that is known to be a durable dependency of another transaction that has been included.
+ * This mechanism is performed in isolation by each replica, so a final set of Deps may contain redundant transactions.
+ * - CommandsForKey may prune already-applied transactions. This means we can in some circumstances try to calculate
+ * dependencies for a timestamp that occurs before the prune point. If this happens, we insert a future dependency,
+ * so it is possible for Deps to contain TxnId > their intended bound.
+ * - This can occur for regular transactions with old TxnId during PreAccept; in this case we always propose an executeAt that is greater than this inserted Dep
+ * - This can occur for SyncPoint and ExclusiveSyncPoint because they are invisible to other transactions and only
+ * create happens-before edges. So they may have their dependencies calculated at any point after pruning may happen.
+ * In this case, CommandsForKey uses this future dependency to ensure its earlier dependencies are up-to-date before
+ * deciding the execution sequence for the SyncPoint/ExclusiveSyncPoint.
*/
public class Deps
{
- public static final Deps NONE = new Deps(KeyDeps.NONE, RangeDeps.NONE);
+ public static final Deps NONE = new Deps(KeyDeps.NONE, RangeDeps.NONE, KeyDeps.NONE);
public static Builder builder()
{
@@ -47,6 +70,7 @@ public class Deps
{
final KeyDeps.Builder keyBuilder;
RangeDeps.Builder rangeBuilder;
+ KeyDeps.Builder directKeyBuilder;
AbstractBuilder()
{
@@ -55,12 +79,23 @@ public class Deps
public AbstractBuilder<T> add(Seekable keyOrRange, TxnId txnId)
{
- switch (keyOrRange.domain())
+ Invariants.checkArgument(keyOrRange.domain() == txnId.domain(), keyOrRange + " is not same domain as " + txnId);
+ switch (txnId.domain())
{
default: throw new AssertionError();
case Key:
- keyBuilder.add(keyOrRange.asKey(), txnId);
+ if (CommandsForKey.managesExecution(txnId))
+ {
+ keyBuilder.add(keyOrRange.asKey(), txnId);
+ }
+ else
+ {
+ if (directKeyBuilder == null)
+ directKeyBuilder = KeyDeps.builder();
+ directKeyBuilder.add(keyOrRange.asKey(), txnId);
+ }
break;
+
case Range:
if (rangeBuilder == null)
rangeBuilder = RangeDeps.builder();
@@ -78,6 +113,8 @@ public class Deps
keyBuilder.close();
if (rangeBuilder != null)
rangeBuilder.close();
+ if (directKeyBuilder != null)
+ directKeyBuilder.close();
}
}
@@ -91,37 +128,88 @@ public class Deps
@Override
public Deps build()
{
- return new Deps(keyBuilder.build(), rangeBuilder == null ? RangeDeps.NONE : rangeBuilder.build());
+ return new Deps(keyBuilder.build(),
+ rangeBuilder == null ? RangeDeps.NONE : rangeBuilder.build(),
+ directKeyBuilder == null ? KeyDeps.NONE : directKeyBuilder.build());
}
}
+ /**
+ * key dependencies where the execution will be managed by {@link CommandsForKey}, so {@link accord.local.Command.WaitingOn} will wait only on the Key.
+ * This is essentially plain reads and writes.
+ *
+ * i.e. where {@code CommandsForKey.managesExecution}
+ */
public final KeyDeps keyDeps;
+
+ /**
+ * Range transaction dependencies. The execution of these is equivalent to {@code directKeyDeps}, in that they are
+ * managed via direct relationships in {@link accord.local.Command.WaitingOn}.
+ */
public final RangeDeps rangeDeps;
- public Deps(KeyDeps keyDeps, RangeDeps rangeDeps)
+ /**
+ * key dependencies where the execution will be managed by direct dependency relationships, so {@link accord.local.Command.WaitingOn} will wait on the {@code TxnId} directly
+ * i.e. where {@code !CommandsForKey.managesExecution}
+ */
+ public final KeyDeps directKeyDeps;
+
+ public Deps(KeyDeps keyDeps, RangeDeps rangeDeps, KeyDeps directKeyDeps)
{
this.keyDeps = keyDeps;
+ this.directKeyDeps = directKeyDeps;
this.rangeDeps = rangeDeps;
}
public boolean contains(TxnId txnId)
{
- return keyDeps.contains(txnId) || rangeDeps.contains(txnId);
+ Routable.Domain domain = txnId.domain();
+ if (domain.isRange())
+ return rangeDeps.contains(txnId);
+
+ if (CommandsForKey.managesExecution(txnId))
+ return keyDeps.contains(txnId);
+
+ return directKeyDeps.contains(txnId);
}
public boolean intersects(TxnId txnId, Ranges ranges)
{
- return keyDeps.intersects(txnId, ranges) || rangeDeps.intersects(txnId, ranges);
+ Routable.Domain domain = txnId.domain();
+ if (domain.isRange())
+ return rangeDeps.intersects(txnId, ranges);
+
+ if (CommandsForKey.managesExecution(txnId))
+ return keyDeps.intersects(txnId, ranges);
+
+ return directKeyDeps.intersects(txnId, ranges);
+ }
+
+ public SortedCursor<TxnId> txnIds(Key key)
+ {
+ SortedList<TxnId> keyDeps = this.keyDeps.txnIds(key);
+ SortedList<TxnId> rangeDeps = this.rangeDeps.computeTxnIds(key);
+ SortedList<TxnId> directKeyDeps = this.directKeyDeps.txnIds(key);
+ int count = Math.min(1, keyDeps.size()) + Math.min(1, directKeyDeps.size()) + Math.min(1, rangeDeps.size());
+ MergeFewDisjointSortedListsCursor<TxnId> cursor = new MergeFewDisjointSortedListsCursor<>(count);
+ if (keyDeps.size() > 0)
+ cursor.add(keyDeps);
+ if (rangeDeps.size() > 0)
+ cursor.add(rangeDeps);
+ if (directKeyDeps.size() > 0)
+ cursor.add(directKeyDeps);
+ cursor.init();
+ return cursor;
}
public Deps with(Deps that)
{
- return new Deps(this.keyDeps.with(that.keyDeps), this.rangeDeps.with(that.rangeDeps));
+ return new Deps(this.keyDeps.with(that.keyDeps), this.rangeDeps.with(that.rangeDeps), this.directKeyDeps.with(that.directKeyDeps));
}
public Deps without(Predicate<TxnId> remove)
{
- return new Deps(keyDeps.without(remove), rangeDeps.without(remove));
+ return new Deps(keyDeps.without(remove), rangeDeps.without(remove), directKeyDeps.without(remove));
}
public PartialDeps slice(Ranges covering)
@@ -131,117 +219,58 @@ public class Deps
public PartialDeps slice(Ranges covering, Ranges slice)
{
- return new PartialDeps(covering, keyDeps.slice(slice), rangeDeps.slice(slice));
+ return new PartialDeps(covering, keyDeps.slice(slice), rangeDeps.slice(slice), directKeyDeps.slice(slice));
}
public boolean isEmpty()
{
- return keyDeps.isEmpty() && rangeDeps.isEmpty();
+ return keyDeps.isEmpty() && rangeDeps.isEmpty() && directKeyDeps.isEmpty();
}
public int txnIdCount()
{
- return keyDeps.txnIdCount() + rangeDeps.txnIdCount();
+ return keyDeps.txnIdCount() + rangeDeps.txnIdCount() + directKeyDeps.txnIdCount();
}
- public int indexOf(TxnId txnId)
+ public TxnId txnId(int i)
{
- switch (txnId.domain())
{
- default: throw new AssertionError("Unknown domain: " + txnId.domain());
- case Key:
- return Arrays.binarySearch(keyDeps.txnIds, txnId);
- case Range:
- int i = Arrays.binarySearch(rangeDeps.txnIds, txnId);
- if (i < 0) i -= keyDeps.txnIdCount();
- else i += keyDeps.txnIdCount();
- return i;
+ int keyDepsLimit = keyDeps.txnIdCount();
+ if (i < keyDepsLimit)
+ return keyDeps.txnId(i);
+ i -= keyDepsLimit;
}
- }
- public TxnId txnId(int i)
- {
- return i < keyDeps.txnIdCount()
- ? keyDeps.txnId(i)
- : rangeDeps.txnId(i - keyDeps.txnIdCount());
- }
+ {
+ int directKeyDepsLimit = directKeyDeps.txnIdCount();
+ if (i < directKeyDepsLimit)
+ return directKeyDeps.txnId(i);
+ i -= directKeyDepsLimit;
+ }
- public TxnId minTxnId()
- {
- if (keyDeps.isEmpty() && rangeDeps.isEmpty())
- throw new IndexOutOfBoundsException();
- if (keyDeps.isEmpty()) return rangeDeps.txnId(0);
- if (rangeDeps.isEmpty()) return keyDeps.txnId(0);
- return TxnId.min(keyDeps.txnId(0), rangeDeps.txnId(0));
+ return rangeDeps.txnId(i);
}
public List<TxnId> txnIds()
{
- final int txnIdCount = txnIdCount();
- final int keyDepsCount = keyDeps.txnIdCount();
- return new AbstractList<TxnId>() {
+ return new AbstractList<>()
+ {
@Override
- public TxnId get(int index)
- {
- return index < keyDepsCount
- ? keyDeps.txnId(index)
- : rangeDeps.txnId(index - keyDepsCount);
- }
-
+ public TxnId get(int index) { return txnId(index); }
@Override
public int size()
{
- return txnIdCount;
+ return txnIdCount();
}
};
}
- public List<TxnId> computeTxnIds(Seekable keyOrRange)
- {
- List<TxnId> keyIds, rangeIds;
- switch (keyOrRange.domain())
- {
- default: throw new AssertionError();
- case Key:
- {
- Key key = keyOrRange.asKey();
- keyIds = keyDeps.txnIds(key);
- rangeIds = rangeDeps.computeTxnIds(key);
- break;
- }
- case Range:
- {
- Range range = keyOrRange.asRange();
- keyIds = keyDeps.txnIds(range);
- rangeIds = rangeDeps.computeTxnIds(range);
- }
- }
-
- if (rangeIds.isEmpty()) return keyIds;
- if (keyIds.isEmpty()) return rangeIds;
-
- List<TxnId> output = new ArrayList<>(keyIds.size() + rangeIds.size());
- int ki = 0, ri = 0;
- while (ki < keyIds.size() && ri < rangeIds.size())
- {
- int c = keyIds.get(ki).compareTo(rangeIds.get(ri));
- Invariants.checkState(c != 0);
- if (c < 0) output.add(keyIds.get(ki++));
- else output.add(rangeIds.get(ri++));
- }
- while (ki < keyIds.size())
- output.add(keyIds.get(ki++));
- while (ri < rangeIds.size())
- output.add(rangeIds.get(ri++));
- return output;
- }
-
public Participants<?> participants(TxnId txnId)
{
switch (txnId.domain())
{
default: throw new AssertionError();
- case Key: return keyDeps.participants(txnId);
+ case Key: return CommandsForKey.managesExecution(txnId) ? keyDeps.participants(txnId) : directKeyDeps.participants(txnId);
case Range: return rangeDeps.participants(txnId);
}
}
@@ -250,19 +279,21 @@ public class Deps
public void forEachUniqueTxnId(Ranges ranges, Consumer<TxnId> forEach)
{
keyDeps.forEachUniqueTxnId(ranges, forEach);
+ directKeyDeps.forEachUniqueTxnId(ranges, forEach);
rangeDeps.forEachUniqueTxnId(ranges, forEach);
}
public static <T> Deps merge(List<T> list, Function<T, Deps> getter)
{
return new Deps(KeyDeps.merge(list, getter, deps -> deps.keyDeps),
- RangeDeps.merge(list, getter, deps -> deps.rangeDeps));
+ RangeDeps.merge(list, getter, deps -> deps.rangeDeps),
+ KeyDeps.merge(list, getter, deps -> deps.directKeyDeps));
}
@Override
public String toString()
{
- return keyDeps.toString() + ", " + rangeDeps.toString();
+ return keyDeps + ", " + rangeDeps + ", " + directKeyDeps;
}
@Override
@@ -273,14 +304,15 @@ public class Deps
public boolean equals(Deps that)
{
- return that != null && this.keyDeps.equals(that.keyDeps) && this.rangeDeps.equals(that.rangeDeps);
+ return that != null && this.keyDeps.equals(that.keyDeps) && this.rangeDeps.equals(that.rangeDeps) && this.directKeyDeps.equals(that.directKeyDeps);
}
public @Nullable TxnId maxTxnId()
{
TxnId maxKeyDep = keyDeps.isEmpty() ? null : keyDeps.txnId(keyDeps.txnIdCount() - 1);
TxnId maxRangeDep = rangeDeps.isEmpty() ? null : rangeDeps.txnId(rangeDeps.txnIdCount() - 1);
- return TxnId.nonNullOrMax(maxKeyDep, maxRangeDep);
+ TxnId maxDirectKeyDep = directKeyDeps.isEmpty() ? null : directKeyDeps.txnId(directKeyDeps.txnIdCount() - 1);
+ return TxnId.nonNullOrMax(TxnId.nonNullOrMax(maxKeyDep, maxRangeDep), maxDirectKeyDep);
}
@Override
diff --git a/accord-core/src/main/java/accord/primitives/KeyDeps.java b/accord-core/src/main/java/accord/primitives/KeyDeps.java
index 180be4dc..eb9c5b22 100644
--- a/accord-core/src/main/java/accord/primitives/KeyDeps.java
+++ b/accord-core/src/main/java/accord/primitives/KeyDeps.java
@@ -515,6 +515,11 @@ public class KeyDeps implements Iterable<Map.Entry<Key, TxnId>>
return txnIds[i];
}
+ public int indexOf(TxnId txnId)
+ {
+ return Arrays.binarySearch(txnIds, txnId);
+ }
+
public SortedArrayList<TxnId> txnIds()
{
return new SortedArrayList<>(txnIds);
diff --git a/accord-core/src/main/java/accord/primitives/LatestDeps.java b/accord-core/src/main/java/accord/primitives/LatestDeps.java
index c2a03e3c..911da3fd 100644
--- a/accord-core/src/main/java/accord/primitives/LatestDeps.java
+++ b/accord-core/src/main/java/accord/primitives/LatestDeps.java
@@ -43,7 +43,7 @@ public class LatestDeps extends ReducingRangeMap<LatestDeps.LatestEntry>
public static class SerializerSupport
{
- public static <V> LatestDeps create(boolean inclusiveEnds, RoutingKey[] starts, LatestEntry[] values)
+ public static LatestDeps create(boolean inclusiveEnds, RoutingKey[] starts, LatestEntry[] values)
{
return new LatestDeps(inclusiveEnds, starts, values);
}
@@ -128,14 +128,16 @@ public class LatestDeps extends ReducingRangeMap<LatestDeps.LatestEntry>
if (deps == null) return null;
KeyDeps keyDeps = deps.keyDeps;
RangeDeps rangeDeps = deps.rangeDeps;
+ KeyDeps directKeyDeps = deps.directKeyDeps;
Keys keys = deps.keyDeps.keys;
boolean slice = keys.indexOf(start) != -1 || keys.indexOf(end) != -1 - keys.size();
+ if (!slice) slice = directKeyDeps.keys.indexOf(start) != -1 || directKeyDeps.keys.indexOf(end) != -1 - keys.size();
if (!slice) slice = rangeDeps.indexOfStart(start) != -1 || rangeDeps.indexOfStart(end) != -1 - rangeDeps.rangeCount();
if (!slice) return deps;
Ranges ranges = Ranges.of(start.rangeFactory().newRange(start, end));
- return new Deps(keyDeps.slice(ranges), rangeDeps.slice(ranges));
+ return new Deps(keyDeps.slice(ranges), rangeDeps.slice(ranges), directKeyDeps.slice(ranges));
}
public String toString()
@@ -186,7 +188,7 @@ public class LatestDeps extends ReducingRangeMap<LatestDeps.LatestEntry>
if (deps == null)
return null;
- return new Deps(deps.keyDeps.slice(ranges), deps.rangeDeps.slice(ranges));
+ return new Deps(deps.keyDeps.slice(ranges), deps.rangeDeps.slice(ranges), deps.directKeyDeps.slice(ranges));
}
static class Builder extends AbstractIntervalBuilder<RoutingKey, LatestEntry, LatestDeps>
@@ -309,8 +311,9 @@ public class LatestDeps extends ReducingRangeMap<LatestDeps.LatestEntry>
return Deps.NONE;
KeyDeps keyDeps = KeyDeps.merge(stream(Merge::forProposal, (d, r) -> d.keyDeps.slice(r)));
+ KeyDeps directKeyDeps = KeyDeps.merge(stream(Merge::forProposal, (d, r) -> d.directKeyDeps.slice(r)));
RangeDeps rangeDeps = RangeDeps.merge(stream(Merge::forProposal, (d, r) -> d.rangeDeps.slice(r)));
- return new Deps(keyDeps, rangeDeps);
+ return new Deps(keyDeps, rangeDeps, directKeyDeps);
}
MergedCommitResult mergeCommit(TxnId txnId, Timestamp executeAt)
@@ -321,8 +324,9 @@ public class LatestDeps extends ReducingRangeMap<LatestDeps.LatestEntry>
List<Range> sufficientFor = new ArrayList<>();
boolean useLocalDeps = txnId.equals(executeAt);
KeyDeps keyDeps = KeyDeps.merge(stream(forCommit(useLocalDeps, sufficientFor), (d, r) -> d.keyDeps.slice(r)));
+ KeyDeps directKeyDeps = KeyDeps.merge(stream(forCommit(useLocalDeps, sufficientFor), (d, r) -> d.directKeyDeps.slice(r)));
RangeDeps rangeDeps = RangeDeps.merge(stream(forCommit(useLocalDeps, sufficientFor), (d, r) -> d.rangeDeps.slice(r)));
- return new MergedCommitResult(new Deps(keyDeps, rangeDeps), Ranges.of(sufficientFor.toArray(new Range[0])));
+ return new MergedCommitResult(new Deps(keyDeps, rangeDeps, directKeyDeps), Ranges.of(sufficientFor.toArray(new Range[0])));
}
private <V> Stream<V> stream(TriFunction<Ranges, MergeEntry, BiFunction<Deps, Ranges, V>, Stream<V>> selector, BiFunction<Deps, Ranges, V> getter)
diff --git a/accord-core/src/main/java/accord/primitives/PartialDeps.java b/accord-core/src/main/java/accord/primitives/PartialDeps.java
index 9367328b..7b17b3c7 100644
--- a/accord-core/src/main/java/accord/primitives/PartialDeps.java
+++ b/accord-core/src/main/java/accord/primitives/PartialDeps.java
@@ -22,7 +22,7 @@ import accord.utils.Invariants;
public class PartialDeps extends Deps
{
- public static final PartialDeps NONE = new PartialDeps(Ranges.EMPTY, KeyDeps.NONE, RangeDeps.NONE);
+ public static final PartialDeps NONE = new PartialDeps(Ranges.EMPTY, KeyDeps.NONE, RangeDeps.NONE, KeyDeps.NONE);
public static Builder builder(Ranges covering)
{
@@ -39,7 +39,10 @@ public class PartialDeps extends Deps
@Override
public PartialDeps build()
{
- return new PartialDeps(covering, keyBuilder.build(), rangeBuilder == null ? RangeDeps.NONE : rangeBuilder.build());
+ return new PartialDeps(covering,
+ keyBuilder.build(),
+ rangeBuilder == null ? RangeDeps.NONE : rangeBuilder.build(),
+ directKeyBuilder == null ? KeyDeps.NONE : directKeyBuilder.build());
}
}
@@ -49,11 +52,12 @@ public class PartialDeps extends Deps
// where the store has some ranges that we participate in, and some we do not; we will not correctly construct covering in some cases
public final Ranges covering;
- public PartialDeps(Ranges covering, KeyDeps keyDeps, RangeDeps rangeDeps)
+ public PartialDeps(Ranges covering, KeyDeps keyDeps, RangeDeps rangeDeps, KeyDeps directKeyDeps)
{
- super(keyDeps, rangeDeps);
+ super(keyDeps, rangeDeps, directKeyDeps);
this.covering = covering;
Invariants.checkState(covering.containsAll(keyDeps.keys));
+ Invariants.checkState(covering.containsAll(directKeyDeps.keys));
Invariants.checkState(rangeDeps.isCoveredBy(covering));
}
@@ -62,14 +66,6 @@ public class PartialDeps extends Deps
return covering.containsAll(participants);
}
- public boolean isEqualOrFuller(PartialDeps that)
- {
- // TODO (required): this is likely not correct: could have all txnIds but be missing some txnId<->key relations
- return covering.containsAll(that.covering)
- && keyDeps.txnIds().containsAll(that.keyDeps.txnIds())
- && rangeDeps.txnIds().containsAll(that.rangeDeps.txnIds());
- }
-
public Deps with(Deps that)
{
if (that instanceof PartialDeps)
@@ -79,17 +75,18 @@ public class PartialDeps extends Deps
public PartialDeps with(PartialDeps that)
{
- Invariants.checkArgument((this.rangeDeps == null) == (that.rangeDeps == null));
return new PartialDeps(that.covering.with(this.covering),
this.keyDeps.with(that.keyDeps),
- this.rangeDeps == null ? null : this.rangeDeps.with(that.rangeDeps));
+ this.rangeDeps.with(that.rangeDeps),
+ this.directKeyDeps.with(that.directKeyDeps)
+ );
}
public Deps reconstitute(FullRoute<?> route)
{
if (!covers(route.participants()))
throw new IllegalArgumentException();
- return new Deps(keyDeps, rangeDeps);
+ return new Deps(keyDeps, rangeDeps, directKeyDeps);
}
// covering might cover a wider set of ranges, some of which may have no involved keys
@@ -98,10 +95,8 @@ public class PartialDeps extends Deps
if (!covers(covering))
throw new IllegalArgumentException();
- if (covers(covering))
- return this;
-
- return new PartialDeps(covering, keyDeps, rangeDeps);
+ if (covers(covering)) return this;
+ else throw Invariants.illegalArgument(this.covering + " does not cover " + covering);
}
@Override
diff --git a/accord-core/src/main/java/accord/primitives/PartialTxn.java b/accord-core/src/main/java/accord/primitives/PartialTxn.java
index 06d470e0..6bcb9ca8 100644
--- a/accord-core/src/main/java/accord/primitives/PartialTxn.java
+++ b/accord-core/src/main/java/accord/primitives/PartialTxn.java
@@ -18,8 +18,6 @@
package accord.primitives;
-import java.util.Objects;
-
import accord.api.Query;
import accord.api.Read;
import accord.api.Update;
@@ -59,8 +57,6 @@ public interface PartialTxn extends Txn
return covering().containsAll(participants);
}
- boolean isEqualOrFuller(PartialTxn txn);
-
static PartialTxn merge(@Nullable PartialTxn a, @Nullable PartialTxn b)
{
return a == null ? b : b == null ? a : a.with(b);
@@ -112,17 +108,6 @@ public interface PartialTxn extends Txn
return covering.containsAll(ranges);
}
- @Override
- public boolean isEqualOrFuller(PartialTxn txn)
- {
- return kind() == txn.kind()
- && covering().containsAll(txn.covering())
- && keys().containsAll(txn.keys())
- && read().isEqualOrFuller(txn.read())
- && Objects.equals(query(), txn.query())
- && ((update() == null && txn.update() == null) || (update() != null && txn.update() != null && update().isEqualOrFuller(txn.update())));
- }
-
@Override
public Txn reconstitute(FullRoute<?> route)
{
diff --git a/accord-core/src/main/java/accord/primitives/RangeDeps.java b/accord-core/src/main/java/accord/primitives/RangeDeps.java
index b609a8cc..4cc18d29 100644
--- a/accord-core/src/main/java/accord/primitives/RangeDeps.java
+++ b/accord-core/src/main/java/accord/primitives/RangeDeps.java
@@ -18,7 +18,6 @@
package accord.primitives;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Iterator;
@@ -44,6 +43,7 @@ import accord.utils.RelationMultiMap;
import accord.utils.SearchableRangeList;
import accord.utils.SortedArrays;
import accord.utils.SortedArrays.SortedArrayList;
+import accord.utils.SortedList;
import accord.utils.SymmetricComparator;
import accord.utils.TriFunction;
import net.nicoulaj.compilecommand.annotations.DontInline;
@@ -658,20 +658,38 @@ public class RangeDeps implements Iterable<Map.Entry<Range, TxnId>>
return txnIds(rangesToTxnIds, start, end);
}
- public List<TxnId> computeTxnIds(Key key)
+ static class ListBuilder
{
- List<TxnId> result = new ArrayList<>();
- forEachUniqueTxnId(key, result::add);
- result.sort(TxnId::compareTo);
- return result;
+ TxnId[] buffer = NO_TXNIDS;
+ int count;
+
+ void add(TxnId txnId)
+ {
+ if (count == buffer.length)
+ buffer = cachedTxnIds().resize(buffer, count, Math.max(8, count + (count >> 1)));
+ buffer[count++] = txnId;
+ }
+
+ SortedList<TxnId> build()
+ {
+ TxnId[] txnIds = cachedTxnIds().completeAndDiscard(buffer, count);
+ Arrays.sort(txnIds);
+ return new SortedArrayList<>(txnIds);
+ }
+ }
+
+ public SortedList<TxnId> computeTxnIds(Key key)
+ {
+ ListBuilder builder = new ListBuilder();
+ forEachUniqueTxnId(key, builder::add);
+ return builder.build();
}
public List<TxnId> computeTxnIds(Range key)
{
- List<TxnId> result = new ArrayList<>();
- forEachUniqueTxnId(key, result::add);
- result.sort(TxnId::compareTo);
- return result;
+ ListBuilder builder = new ListBuilder();
+ forEachUniqueTxnId(key, builder::add);
+ return builder.build();
}
private SortedRelationList<TxnId> txnIds(int[] ids, int start, int end)
diff --git a/accord-core/src/main/java/accord/primitives/SyncPoint.java b/accord-core/src/main/java/accord/primitives/SyncPoint.java
index 5be269f4..a9abc19f 100644
--- a/accord-core/src/main/java/accord/primitives/SyncPoint.java
+++ b/accord-core/src/main/java/accord/primitives/SyncPoint.java
@@ -28,6 +28,10 @@ import accord.utils.Invariants;
* expect the whole cluster to process these, and we do not want transaction processing to be held up,
* so while these are processed much like a transaction, they are invisible to real transactions which
* may proceed before this is witnessed by the node processing it.
+ *
+ * TODO (required): is this safe for barrier transactions? Since it is invisible to other transactions, it can be recovered
+ * to execute at its TxnId time, even if a later txn exists. We must either make it visible to other transactions
+ * for coordination (but not necessarily for execution), or else require that it has an Accept round.
*/
public class SyncPoint<S extends Seekables<?, ?>>
{
diff --git a/accord-core/src/main/java/accord/utils/ArrayBuffers.java b/accord-core/src/main/java/accord/utils/ArrayBuffers.java
index ba633ccc..2e66d2e5 100644
--- a/accord-core/src/main/java/accord/utils/ArrayBuffers.java
+++ b/accord-core/src/main/java/accord/utils/ArrayBuffers.java
@@ -21,6 +21,7 @@ package accord.utils;
import accord.api.Key;
import accord.api.RoutingKey;
import accord.primitives.Range;
+import accord.primitives.Timestamp;
import accord.primitives.TxnId;
import java.lang.reflect.Array;
@@ -57,6 +58,7 @@ public class ArrayBuffers
private static final ThreadLocal<ObjectBufferCache<RoutingKey>> ROUTINGKEYS = ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 9, RoutingKey[]::new));
private static final ThreadLocal<ObjectBufferCache<Range>> KEYRANGES = ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 7, Range[]::new));
private static final ThreadLocal<ObjectBufferCache<TxnId>> TXN_IDS = ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 12, TxnId[]::new));
+ private static final ThreadLocal<ObjectBufferCache<Timestamp>> TIMESTAMPS = ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 12, Timestamp[]::new));
public static IntBuffers cachedInts()
{
@@ -83,6 +85,11 @@ public class ArrayBuffers
return TXN_IDS.get();
}
+ public static ObjectBuffers<Timestamp> cachedTimestamps()
+ {
+ return TIMESTAMPS.get();
+ }
+
public static <T> ObjectBuffers<T> uncached(IntFunction<T[]> allocator) { return new UncachedObjectBuffers<>(allocator); }
public static IntBuffers uncachedInts() { return UncachedIntBuffers.INSTANCE; }
@@ -162,6 +169,14 @@ public class ArrayBuffers
*/
T[] get(int minSize);
+ /**
+ * Return an {@code T[]} of size at least {@code size}, where size is the exact
+ * number of items that will be populated by the caller. In most cases this
+ * can allocate an output array, but in some use cases it may still return a larger buffer.
+ * No need to invoke complete on the return value.
+ */
+ T[] getAndCompleteExact(int size);
+
/**
* Return an {@code T[]} of size at least {@code minSize}, possibly from a pool,
* and copy the contents of {@code copyAndDiscard} into it.
@@ -235,7 +250,7 @@ public class ArrayBuffers
*
* Depending on implementation, this is either saved from the last such invocation, or else simply returns the size of the buffer parameter.
*/
- int lengthOfLast(T[] buffer);
+ int sizeOfLast(T[] buffer);
}
private static final class UncachedIntBuffers implements IntBuffers
@@ -288,6 +303,12 @@ public class ArrayBuffers
return allocator.apply(minSize);
}
+ @Override
+ public T[] getAndCompleteExact(int size)
+ {
+ return allocator.apply(size);
+ }
+
@Override
public T[] complete(T[] buffer, int usedSize)
{
@@ -304,7 +325,7 @@ public class ArrayBuffers
}
@Override
- public int lengthOfLast(T[] buffer)
+ public int sizeOfLast(T[] buffer)
{
return buffer.length;
}
@@ -457,7 +478,7 @@ public class ArrayBuffers
}
@Override
- public int lengthOfLast(T[] buffer)
+ public int sizeOfLast(T[] buffer)
{
return buffer.length;
}
@@ -479,6 +500,12 @@ public class ArrayBuffers
{
return getInternal(minSize);
}
+
+ @Override
+ public T[] getAndCompleteExact(int size)
+ {
+ return allocator.apply(size);
+ }
}
/**
@@ -508,6 +535,13 @@ public class ArrayBuffers
return objs.get(minSize);
}
+ @Override
+ public T[] getAndCompleteExact(int size)
+ {
+ length = size;
+ return get(size);
+ }
+
@Override
public T[] complete(T[] buffer, int usedSize)
{
@@ -568,7 +602,7 @@ public class ArrayBuffers
}
@Override
- public int lengthOfLast(T[] buffer)
+ public int sizeOfLast(T[] buffer)
{
if (length == -1)
throw illegalState("Attempted to get last length but no call to complete called");
@@ -576,6 +610,105 @@ public class ArrayBuffers
}
}
+ /**
+ * Returns the buffer to the caller, saving the length if necessary
+ */
+ public static class RecursiveObjectBuffers<T> implements ObjectBuffers<T>
+ {
+ final ObjectBuffers<T> wrapped;
+ T[] cur, alt;
+ int lastSize, maxBufferSize;
+
+ public RecursiveObjectBuffers(ObjectBuffers<T> wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public T[] get(int minSize)
+ {
+ if (alt != null)
+ {
+ if (alt.length >= minSize)
+ return alt;
+
+ wrapped.forceDiscard(alt, Math.min(maxBufferSize, alt.length));
+ }
+ return alt = wrapped.get(minSize);
+ }
+
+ @Override
+ public T[] getAndCompleteExact(int size)
+ {
+ updateSize(size);
+ T[] buf = get(size);
+ this.alt = cur;
+ this.cur = buf;
+ return buf;
+ }
+
+ @Override
+ public T[] complete(T[] buffer, int usedSize)
+ {
+ updateSize(usedSize);
+ Invariants.checkArgument(buffer == alt);
+ alt = cur;
+ cur = buffer;
+ return buffer;
+ }
+
+ @Override
+ public T[] completeAndDiscard(T[] buffer, int usedSize)
+ {
+ return complete(buffer, usedSize);
+ }
+
+ @Override
+ public T[] completeWithExisting(T[] buffer, int usedSize)
+ {
+ updateSize(usedSize);
+ return buffer;
+ }
+
+ private void updateSize(int size)
+ {
+ lastSize = size;
+ maxBufferSize = Math.max(size, maxBufferSize);
+ }
+
+ @Override
+ public boolean discard(T[] buffer, int usedSize)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean forceDiscard(T[] buffer, int usedSize)
+ {
+ return false;
+ }
+
+ @Override
+ public int sizeOfLast(T[] buffer)
+ {
+ return lastSize;
+ }
+
+ public void discardBuffers()
+ {
+ if (cur != null)
+ {
+ wrapped.forceDiscard(cur, Math.min(maxBufferSize, cur.length));
+ cur = null;
+ }
+ if (alt != null)
+ {
+ wrapped.forceDiscard(alt, Math.min(maxBufferSize, alt.length));
+ alt = null;
+ }
+ }
+ }
+
/**
* Returns the buffer to the caller, saving the length if necessary
*/
diff --git a/accord-core/src/main/java/accord/utils/Invariants.java b/accord-core/src/main/java/accord/utils/Invariants.java
index ad9d8b35..2f951d21 100644
--- a/accord-core/src/main/java/accord/utils/Invariants.java
+++ b/accord-core/src/main/java/accord/utils/Invariants.java
@@ -28,14 +28,32 @@ import static java.lang.String.format;
public class Invariants
{
- // TODO (now): configure by system parameter and turn off by default
- private static final boolean PARANOID = true;
- private static final boolean DEBUG = true;
+ public enum Paranoia
+ {
+ NONE, CONSTANT, LINEAR, SUPERLINEAR, QUADRATIC
+ }
+
+ public enum ParanoiaCostFactor
+ {
+ LOW, HIGH
+ }
+
+ private static final int PARANOIA_COMPUTE = Paranoia.valueOf(System.getProperty("accord.paranoia.cpu", "NONE").toUpperCase()).ordinal();
+ private static final int PARANOIA_MEMORY = Paranoia.valueOf(System.getProperty("accord.paranoia.memory", "NONE").toUpperCase()).ordinal();
+ private static final int PARANOIA_FACTOR = ParanoiaCostFactor.valueOf(System.getProperty("accord.paranoia.costfactor", "LOW").toUpperCase()).ordinal();
+ private static boolean IS_PARANOID = PARANOIA_COMPUTE > 0 || PARANOIA_MEMORY > 0;
+ private static final boolean DEBUG = System.getProperty("accord.debug", "false").equals("true");
public static boolean isParanoid()
{
- return PARANOID;
+ return IS_PARANOID;
}
+
+ public static boolean testParanoia(Paranoia compute, Paranoia memory, ParanoiaCostFactor factor)
+ {
+ return PARANOIA_COMPUTE >= compute.ordinal() && PARANOIA_MEMORY >= memory.ordinal() && PARANOIA_FACTOR >= factor.ordinal();
+ }
+
public static boolean debug()
{
return DEBUG;
@@ -92,7 +110,7 @@ public class Invariants
public static void paranoid(boolean condition)
{
- if (PARANOID && !condition)
+ if (isParanoid() && !condition)
illegalState();
}
@@ -324,6 +342,14 @@ public class Invariants
return param;
}
+ @Inline
+ public static int checkNonNegative(int index)
+ {
+ if (index < 0)
+ throw illegalState("Index %d expected to be non-negative", index);
+ return index;
+ }
+
public static <O> O cast(Object o, Class<O> klass)
{
try
@@ -336,6 +362,18 @@ public class Invariants
}
}
+ public static void checkIndex(int index, int length)
+ {
+ if (!(index >= 0 && index < length))
+ {
+ if (index < 0)
+ throw new IndexOutOfBoundsException("Index " + index + " must not be negative");
+ if (length < 0)
+ throw new IndexOutOfBoundsException("Length " + length + " must not be negative");
+ throw new IndexOutOfBoundsException(String.format("%d must be less than %d", index, length));
+ }
+ }
+
public static void checkIndexInBounds(int realLength, int offset, int length)
{
if (realLength == 0 || length == 0)
@@ -348,4 +386,5 @@ public class Invariants
if (endOffset > realLength)
throw new IndexOutOfBoundsException(String.format("Offset %d, length = %d; real length was %d", offset, length, realLength));
}
+
}
diff --git a/accord-core/src/main/java/accord/utils/MergeFewDisjointSortedListsCursor.java b/accord-core/src/main/java/accord/utils/MergeFewDisjointSortedListsCursor.java
new file mode 100644
index 00000000..91b5a77e
--- /dev/null
+++ b/accord-core/src/main/java/accord/utils/MergeFewDisjointSortedListsCursor.java
@@ -0,0 +1,156 @@
+/*
+ * 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 accord.utils;
+
+import java.util.Arrays;
+import javax.annotation.Nonnull;
+
+public class MergeFewDisjointSortedListsCursor<T extends Comparable<? super T>> implements SortedCursor<T>
+{
+ // Holds and is comparable by the head item of an iterator it owns
+ protected static final class Candidate<T extends Comparable<? super T>> implements Comparable<Candidate<T>>
+ {
+ private final SortedList<? extends T> list;
+ private int itemIdx;
+ private T item;
+
+ public Candidate(@Nonnull SortedList<? extends T> list)
+ {
+ Invariants.checkState(!list.isEmpty());
+ this.list = list;
+ this.item = list.get(0);
+ }
+
+ /** @return this if our iterator had an item, and it is now available, otherwise null */
+ private Candidate<T> advance()
+ {
+ if (++itemIdx >= list.size())
+ return null;
+
+ item = list.get(itemIdx);
+ return this;
+ }
+
+ private boolean find(Comparable<? super T> find)
+ {
+ boolean found;
+ int i = list.findNext(itemIdx, find);
+ found = i >= 0;
+ if (i < 0) i = -1 - i;
+ if ((itemIdx = i) < list.size())
+ item = list.get(itemIdx = i);
+ else
+ item = null;
+ return found;
+ }
+
+ public int compareTo(Candidate<T> that)
+ {
+ return this.item.compareTo(that.item);
+ }
+ }
+
+ protected final Candidate<T>[] heap;
+
+ /** Number of non-exhausted iterators. */
+ int size;
+
+ public MergeFewDisjointSortedListsCursor(int capacity)
+ {
+ this.heap = (Candidate<T>[]) new Candidate[capacity];
+ }
+
+ public void add(SortedList<? extends T> list)
+ {
+ Candidate<T> candidate = new Candidate<>(list);
+ heap[size++] = candidate;
+ }
+
+ public void init()
+ {
+ Arrays.sort(heap, 0, size);
+ }
+
+ @Override
+ public boolean hasCur()
+ {
+ return size > 0;
+ }
+
+ @Override
+ public T cur()
+ {
+ return heap[0].item;
+ }
+
+ public void advance()
+ {
+ Candidate<T> sink = heap[0].advance();
+ if (sink == null)
+ {
+ heap[0] = null;
+ if (--size > 0)
+ System.arraycopy(heap, 1, heap, 0, size);
+ }
+ else
+ {
+ int i = 1;
+ for (; i < size && sink.compareTo(heap[i]) > 0; ++i)
+ heap[i - 1] = heap[i];
+ heap[i - 1] = sink;
+ }
+ }
+
+ @Override
+ public boolean find(Comparable<? super T> find)
+ {
+ int i = 0;
+ int removedCount = 0;
+ boolean found = false;
+ while (i < size)
+ {
+ int c = find.compareTo(heap[i].item);
+ if (c <= 0)
+ {
+ found |= c == 0;
+ if (removedCount > 0)
+ System.arraycopy(heap, removedCount, heap, 0, size - removedCount);
+ break;
+ }
+
+ Candidate<T> candidate = heap[i];
+ found |= candidate.find(find);
+ if (candidate.item == null)
+ {
+ Invariants.checkState(candidate.itemIdx == candidate.list.size());
+ ++removedCount;
+ }
+ else if (removedCount > 0)
+ {
+ heap[i - removedCount] = candidate;
+ }
+ i++;
+ }
+
+ while (removedCount-- > 0)
+ heap[--size] = null;
+
+ Arrays.sort(heap, 0, size);
+ return found;
+ }
+}
\ No newline at end of file
diff --git a/accord-core/src/main/java/accord/utils/RelationMultiMap.java b/accord-core/src/main/java/accord/utils/RelationMultiMap.java
index 7eb8e824..590bd9fe 100644
--- a/accord-core/src/main/java/accord/utils/RelationMultiMap.java
+++ b/accord-core/src/main/java/accord/utils/RelationMultiMap.java
@@ -573,9 +573,9 @@ public class RelationMultiMap
try
{
outKeys = SortedArrays.linearUnion(leftKeys, leftKeysLength, rightKeys, rightKeysLength, keyComparator, keyBuffers);
- outKeysLength = keyBuffers.lengthOfLast(outKeys);
+ outKeysLength = keyBuffers.sizeOfLast(outKeys);
outValues = SortedArrays.linearUnion(leftValues, leftValuesLength, rightValues, rightValuesLength, valueComparator, valueBuffers);
- outValuesLength = valueBuffers.lengthOfLast(outValues);
+ outValuesLength = valueBuffers.sizeOfLast(outValues);
remapLeft = remapToSuperset(leftValues, leftValuesLength, outValues, outValuesLength, valueComparator, intBuffers);
remapRight = remapToSuperset(rightValues, rightValuesLength, outValues, outValuesLength, valueComparator, intBuffers);
diff --git a/accord-core/src/main/java/accord/utils/SortedArrays.java b/accord-core/src/main/java/accord/utils/SortedArrays.java
index 30c179cb..8da30df1 100644
--- a/accord-core/src/main/java/accord/utils/SortedArrays.java
+++ b/accord-core/src/main/java/accord/utils/SortedArrays.java
@@ -20,10 +20,10 @@ package accord.utils;
import java.util.AbstractList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Comparator;
import java.util.function.IntBinaryOperator;
import java.util.function.IntFunction;
-import java.util.stream.StreamSupport;
import accord.utils.ArrayBuffers.ObjectBuffers;
import accord.utils.ArrayBuffers.IntBufferAllocator;
@@ -48,6 +48,7 @@ public class SortedArrays
final T[] array;
public SortedArrayList(T[] array)
{
+ // implicitly checks entries are non-null
this.array = checkArgument(array, SortedArrays::isSortedUnique);
}
@@ -79,13 +80,40 @@ public class SortedArrays
{
return test.array.length == SortedArrays.foldlIntersection(Comparable::compareTo, array, 0, array.length, test.array, 0, test.array.length, (t, p, v, li, ri) -> v + 1, 0, 0, test.array.length);
}
+
+ public static class Builder<T extends Comparable<? super T>>
+ {
+ final T[] array;
+ int count;
+
+ public Builder(T[] array)
+ {
+ this.array = array;
+ }
+
+ public void add(T value)
+ {
+ array[count++] = value;
+ }
+
+ public SortedArrayList<T> build()
+ {
+ Invariants.checkState(count == array.length);
+ return new SortedArrayList<>(array);
+ }
+ }
}
public static class ExtendedSortedArrayList<T extends Comparable<? super T>> extends SortedArrayList<T>
{
- public static <T extends Comparable<? super T>> ExtendedSortedArrayList<T> sortedCopyOf(Iterable<T> iterator, IntFunction<T[]> allocator)
+ public static <T extends Comparable<? super T>> ExtendedSortedArrayList<T> sortedCopyOf(Collection<T> copy, IntFunction<T[]> allocator)
{
- return new ExtendedSortedArrayList<>(checkArgument(StreamSupport.stream(iterator.spliterator(), false).sorted().toArray(allocator), SortedArrays::isSortedUnique), allocator);
+ T[] array = allocator.apply(copy.size());
+ array = copy.toArray(array);
+ Arrays.sort(array);
+ // implicitly checks entries are non-null
+ checkArgument(array, SortedArrays::isSortedUnique);
+ return new ExtendedSortedArrayList<>(array, allocator);
}
final IntFunction<T[]> allocator;
@@ -188,8 +216,10 @@ public class SortedArrays
{
if (leftStart == 0)
return buffers.completeWithExisting(left, leftEnd);
- result = buffers.get(leftEnd - leftStart);
- System.arraycopy(left, leftStart, result, 0, leftEnd - leftStart);
+
+ int size = leftEnd - leftStart;
+ result = buffers.getAndCompleteExact(size);
+ System.arraycopy(left, leftStart, result, 0, size);
return result;
}
// no elements matched or only a subset matched
@@ -227,10 +257,13 @@ public class SortedArrays
{
if (rightStart == 0)
return buffers.completeWithExisting(right, rightEnd);
- result = buffers.get(rightEnd - rightStart);
- System.arraycopy(right, rightStart, result, 0, rightEnd - rightStart);
+
+ int size = rightEnd - rightStart;
+ result = buffers.getAndCompleteExact(size);
+ System.arraycopy(right, rightStart, result, 0, size);
return result;
}
+
// no elements matched or only a subset matched
result = buffers.get((rightEnd - rightStart) + (leftEnd - leftIdx));
resultSize = rightIdx - rightStart;
@@ -272,11 +305,12 @@ public class SortedArrays
while (rightIdx < rightEnd)
result[resultSize++] = right[rightIdx++];
- return buffers.complete(result, resultSize);
+ return buffers.completeAndDiscard(result, resultSize);
}
- finally
+ catch (Throwable t)
{
buffers.discard(result, resultSize);
+ throw t;
}
}
@@ -1291,6 +1325,37 @@ public class SortedArrays
return initialValue;
}
+ @Inline
+ public static <T extends Comparable<? super T>> boolean isSubset(T[] test, T[] superset)
+ {
+ return isSubset(Comparable::compareTo, test, 0, test.length, superset, 0, superset.length);
+ }
+
+ @Inline
+ public static <T extends Comparable<? super T>> boolean isSubset(T[] test, int testFrom, int testTo, T[] superset, int supersetFrom, int supersetTo)
+ {
+ return isSubset(Comparable::compareTo, test, testFrom, testTo, superset, supersetFrom, supersetTo);
+ }
+
+ @Inline
+ public static <T> boolean isSubset(AsymmetricComparator<? super T, ? super T> comparator, T[] test, int testFrom, int testTo, T[] superset, int supersetFrom, int supersetTo)
+ {
+ while (true)
+ {
+ long abi = findNextIntersection(test, testFrom, testTo, superset, supersetFrom, supersetTo, comparator);
+ if (abi < 0)
+ return true;
+
+ int nextai = (int)(abi);
+ if (testFrom != nextai)
+ return false;
+
+ supersetFrom = (int)(abi >>> 32);
+ ++testFrom;
+ ++supersetFrom;
+ }
+ }
+
public static <T extends Comparable<T>> void assertSorted(T[] array)
{
if (!isSorted(array))
diff --git a/accord-core/src/main/java/accord/api/Read.java b/accord-core/src/main/java/accord/utils/SortedCursor.java
similarity index 55%
copy from accord-core/src/main/java/accord/api/Read.java
copy to accord-core/src/main/java/accord/utils/SortedCursor.java
index 3b77ed3c..4dcbaabc 100644
--- a/accord-core/src/main/java/accord/api/Read.java
+++ b/accord-core/src/main/java/accord/utils/SortedCursor.java
@@ -15,24 +15,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package accord.utils;
-package accord.api;
-
-import accord.local.SafeCommandStore;
-import accord.primitives.Ranges;
-import accord.primitives.Seekable;
-import accord.primitives.Seekables;
-import accord.primitives.Timestamp;
-import accord.utils.async.AsyncChain;
-
-/**
- * A read to be performed on potentially multiple shards, the inputs of which may be fed to a {@link Query}
- */
-public interface Read
+public interface SortedCursor<T extends Comparable<? super T>>
{
- Seekables<?, ?> keys();
- AsyncChain<Data> read(Seekable key, SafeCommandStore commandStore, Timestamp executeAt, DataStore store);
- Read slice(Ranges ranges);
- Read merge(Read other);
- default boolean isEqualOrFuller(Read other) { return true; }
+ // move to the first item >= find, returning true if an exact match was found
+ boolean find(Comparable<? super T> find);
+ void advance();
+ boolean hasCur();
+ T cur();
}
diff --git a/accord-core/src/main/java/accord/utils/btree/BTree.java b/accord-core/src/main/java/accord/utils/btree/BTree.java
index 8bb5d529..ccb4688e 100644
--- a/accord-core/src/main/java/accord/utils/btree/BTree.java
+++ b/accord-core/src/main/java/accord/utils/btree/BTree.java
@@ -501,6 +501,23 @@ public class BTree
}
}
+ public static <Compare, Existing extends Compare, Insert extends Compare> Object[] updateExisting(Object[] toUpdate,
+ Insert insert,
+ int index,
+ UpdateFunction<Insert, Existing> updateF)
+ {
+ Existing prev = (Existing) toUpdate[index];
+ Object next = updateF.merge(prev, insert);
+ if (next == prev)
+ return toUpdate;
+
+ Object[] merged = new Object[toUpdate.length];
+ System.arraycopy(toUpdate, 0, merged, 0, index);
+ merged[index] = next;
+ System.arraycopy(toUpdate, index + 1, merged, index + 1, toUpdate.length - (index + 1));
+ return merged;
+ }
+
public static void reverseInSitu(Object[] tree)
{
reverseInSitu(tree, height(tree), true);
diff --git a/accord-core/src/test/java/accord/impl/IntKey.java b/accord-core/src/test/java/accord/impl/IntKey.java
index ae575505..cf4cef61 100644
--- a/accord-core/src/test/java/accord/impl/IntKey.java
+++ b/accord-core/src/test/java/accord/impl/IntKey.java
@@ -115,6 +115,12 @@ public class IntKey implements RoutableKey
{
super(key);
}
+
+ @Override
+ public accord.primitives.Range asRange()
+ {
+ return new Range(new Routing(key - 1), new Routing(key));
+ }
}
public static class Routing extends IntKey implements accord.api.RoutingKey
diff --git a/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java b/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java
index 7e75acc1..621ba41d 100644
--- a/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java
+++ b/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java
@@ -310,6 +310,10 @@ public class PrefixedIntHashKey implements RoutableKey
int rightInt = that instanceof Sentinel ? (((Sentinel) that).isMin ? -1 : 1) : 0;
rc = Integer.compare(leftInt, rightInt);
}
+ else if (this instanceof Key && that instanceof Key)
+ {
+ rc = Integer.compare(this.key, other.key);
+ }
}
}
return rc;
diff --git a/accord-core/src/test/java/accord/impl/TestAgent.java b/accord-core/src/test/java/accord/impl/TestAgent.java
index dfe5abd7..87f1503a 100644
--- a/accord-core/src/test/java/accord/impl/TestAgent.java
+++ b/accord-core/src/test/java/accord/impl/TestAgent.java
@@ -120,6 +120,18 @@ public class TestAgent implements Agent
return MICROSECONDS.convert(10, SECONDS);
}
+ @Override
+ public long cfkHlcPruneDelta()
+ {
+ return 1000;
+ }
+
+ @Override
+ public int cfkPruneInterval()
+ {
+ return 1;
+ }
+
@Override
public Txn emptyTxn(Txn.Kind kind, Seekables<?, ?> keysOrRanges)
{
diff --git a/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java b/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java
index 4e96d890..319330df 100644
--- a/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java
+++ b/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java
@@ -64,6 +64,9 @@ import accord.utils.ReflectionUtils.Difference;
import accord.utils.async.AsyncChain;
import accord.utils.async.AsyncChains;
+import static accord.utils.Invariants.Paranoia.LINEAR;
+import static accord.utils.Invariants.ParanoiaCostFactor.HIGH;
+
public class DelayedCommandStores extends InMemoryCommandStores.SingleThread
{
//TODO (correctness): remove once we have a Journal integration that does not change the values of a command...
@@ -163,6 +166,9 @@ public class DelayedCommandStores extends InMemoryCommandStores.SingleThread
@Override
public void validateRead(Command current)
{
+ if (!Invariants.testParanoia(LINEAR, LINEAR, HIGH))
+ return;
+
// "loading" the command doesn't make sense as we don't "store" the command...
if (current.txnId().kind() == Txn.Kind.EphemeralRead)
return;
@@ -210,7 +216,7 @@ public class DelayedCommandStores extends InMemoryCommandStores.SingleThread
public <T> AsyncChain<T> submit(Callable<T> fn)
{
Task<T> task = new DelayedTask<>(fn);
- if (Invariants.isParanoid())
+ if (Invariants.testParanoia(LINEAR, LINEAR, HIGH))
{
return AsyncChains.detectLeak(agent::onUncaughtException, () -> {
boolean wasEmpty = pending.isEmpty();
@@ -278,6 +284,7 @@ public class DelayedCommandStores extends InMemoryCommandStores.SingleThread
@Override
public void postExecute()
{
+ super.postExecute();
commands.entrySet().forEach(e -> {
InMemorySafeCommand safe = e.getValue();
if (!safe.isModified()) return;
diff --git a/accord-core/src/test/java/accord/impl/basic/Journal.java b/accord-core/src/test/java/accord/impl/basic/Journal.java
index 9258c0e0..7c8c8373 100644
--- a/accord-core/src/test/java/accord/impl/basic/Journal.java
+++ b/accord-core/src/test/java/accord/impl/basic/Journal.java
@@ -64,6 +64,7 @@ public class Journal implements LocalRequest.Handler, Runnable
private final MessageListener messageListener;
private final Map<Key, List<Diff>> diffs = new HashMap<>();
private Node node;
+ boolean isScheduled;
public Journal(MessageListener messageListener)
{
@@ -73,7 +74,13 @@ public class Journal implements LocalRequest.Handler, Runnable
public void start(Node node)
{
this.node = node;
- node.scheduler().recurring(this, 1, TimeUnit.MILLISECONDS);
+ }
+
+ private void ensureScheduled()
+ {
+ if (isScheduled) return;
+ node.scheduler().once(this, 1, TimeUnit.MILLISECONDS);
+ isScheduled = true;
}
public void shutdown()
@@ -84,6 +91,7 @@ public class Journal implements LocalRequest.Handler, Runnable
@Override
public <R> void handle(LocalRequest<R> message, BiConsumer<? super R, Throwable> callback, Node node)
{
+ ensureScheduled();
messageListener.onMessage(NodeSink.Action.DELIVER, node.id(), node.id(), -1, message);
if (message.type().hasSideEffects())
{
@@ -96,6 +104,7 @@ public class Journal implements LocalRequest.Handler, Runnable
public void handle(Request request, Node.Id from, ReplyContext replyContext)
{
+ ensureScheduled();
if (request.type() != null && request.type().hasSideEffects())
{
// enqueue
@@ -108,6 +117,7 @@ public class Journal implements LocalRequest.Handler, Runnable
@Override
public void run()
{
+ isScheduled = false;
if (this.node == null)
return;
try
diff --git a/accord-core/src/test/java/accord/impl/list/ListAgent.java b/accord-core/src/test/java/accord/impl/list/ListAgent.java
index 343be285..6d6cee5a 100644
--- a/accord-core/src/test/java/accord/impl/list/ListAgent.java
+++ b/accord-core/src/test/java/accord/impl/list/ListAgent.java
@@ -102,6 +102,18 @@ public class ListAgent implements Agent
return timeout;
}
+ @Override
+ public long cfkHlcPruneDelta()
+ {
+ return 100;
+ }
+
+ @Override
+ public int cfkPruneInterval()
+ {
+ return 1;
+ }
+
@Override
public Txn emptyTxn(Txn.Kind kind, Seekables<?, ?> keysOrRanges)
{
diff --git a/accord-core/src/test/java/accord/local/WaitingOnTest.java b/accord-core/src/test/java/accord/local/WaitingOnTest.java
new file mode 100644
index 00000000..68a522e8
--- /dev/null
+++ b/accord-core/src/test/java/accord/local/WaitingOnTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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 accord.local;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import accord.local.Command.WaitingOn;
+import accord.primitives.Deps;
+import accord.primitives.Keys;
+import accord.primitives.TxnId;
+import accord.utils.AccordGens;
+import accord.utils.Gen;
+import accord.utils.Gens;
+import org.assertj.core.api.Assertions;
+
+import static accord.utils.Property.qt;
+
+class WaitingOnTest
+{
+ private static final Gen<Gen<Boolean>> BOOLEAN_DISTRO = Gens.bools().mixedDistribution();
+ private static final Gen<Deps> DEPS_GEN = AccordGens.depsFromKey(AccordGens.intKeys(), AccordGens.ranges(AccordGens.intRoutingKey()));
+ private static final Gen<Gen<WaitingOn>> WAITING_ON_DISTRO = rs -> AccordGens.waitingOn(DEPS_GEN, BOOLEAN_DISTRO.next(rs),
+ BOOLEAN_DISTRO.next(rs),
+ BOOLEAN_DISTRO.next(rs),
+ BOOLEAN_DISTRO.next(rs));
+
+ @Test
+ void property()
+ {
+ qt().forAll(Gens.flatten(WAITING_ON_DISTRO)).check(WaitingOnTest::validateWaitingOn);
+ }
+
+ private static void validateWaitingOn(WaitingOn waitingOn)
+ {
+ validateIndexOf(waitingOn);
+ validateDirectKeyTxnReachable(waitingOn);
+ validateWaitingOnKey(waitingOn);
+ validateNextWaitingOn(waitingOn);
+ }
+
+ private static void validateIndexOf(WaitingOn waitingOn)
+ {
+ // property: indexOf(txnId(i)) == i
+ WaitingOn.Update update = new WaitingOn.Update(waitingOn);
+ for (int i = 0, size = waitingOn.txnIdCount(); i < size; i++)
+ {
+ TxnId expected = waitingOn.txnId(i);
+ Assertions.assertThat(waitingOn.indexOf(expected))
+ .describedAs("Txn %s indexOf is not what is expected", expected)
+ .isEqualTo(i);
+
+ // what about update?
+ Assertions.assertThat(update.txnId(i)).isEqualTo(expected);
+ Assertions.assertThat(update.indexOf(expected))
+ .describedAs("Txn %s Update.indexOf is not what is expected", expected)
+ .isEqualTo(i);
+ }
+ }
+
+ private static void validateDirectKeyTxnReachable(WaitingOn waitingOn)
+ {
+ // property: direct key txn are reachable
+ for (TxnId expected : waitingOn.directKeyDeps.txnIds())
+ {
+ Assertions.assertThat(waitingOn.indexOf(expected))
+ .describedAs("Txn %s came from directKeyDeps but indexOf does not match", expected)
+ .isEqualTo(waitingOn.directRangeDeps.txnIdCount() + waitingOn.directKeyDeps.indexOf(expected));
+ }
+ }
+
+ private static void validateWaitingOnKey(WaitingOn waitingOn)
+ {
+ // property: isWaitingOnKey(key_index) == is_set(key_offset + key_index)
+ Keys keys = waitingOn.keys;
+ int offset = waitingOn.directRangeDeps.txnIdCount() + waitingOn.directKeyDeps.txnIdCount();
+ boolean hasKeyWaiting = false;
+ for (int i = 0; i < keys.size(); i++)
+ {
+ boolean expected = waitingOn.waitingOn.get(offset + i);
+ Assertions.assertThat(waitingOn.isWaitingOnKey(i))
+ .isEqualTo(expected);
+ hasKeyWaiting |= expected;
+ }
+ Assertions.assertThat(waitingOn.isWaitingOnKey()).isEqualTo(hasKeyWaiting);
+ }
+
+ private static void validateNextWaitingOn(WaitingOn waitingOn)
+ {
+ TxnId nextWaitingOn = waitingOn.nextWaitingOn();
+ List<TxnId> waitingOnTxns = waitingOnTxns(waitingOn);
+ TxnId maxTxn = waitingOnTxns.stream().max(Comparator.naturalOrder()).orElse(null);
+ TxnId minTxn = waitingOnTxns.stream().min(Comparator.naturalOrder()).orElse(null);
+ Assertions.assertThat(nextWaitingOn)
+ .describedAs("nextWaitingOn did not match expected")
+ .isEqualTo(maxTxn);
+
+ WaitingOn.Update update = new WaitingOn.Update(waitingOn);
+ Assertions.assertThat(update.minWaitingOnTxnId())
+ .describedAs("Update.minWaitingOnTxnId did not match expected")
+ .isEqualTo(minTxn);
+ Assertions.assertThat(update.minWaitingOnTxnIdx())
+ .describedAs("Update.minWaitingOnTxnIdx did not match expected")
+ .isEqualTo(minTxn == null ? -1 : waitingOn.indexOf(minTxn));
+ }
+
+ private static List<TxnId> waitingOnTxns(WaitingOn waitingOn)
+ {
+ List<TxnId> matches = new ArrayList<>();
+ for (int i = 0, size = waitingOn.txnIdCount(); i < size; i++)
+ {
+ if (waitingOn.waitingOn.get(i))
+ matches.add(waitingOn.txnId(i));
+ }
+ return matches;
+ }
+}
\ No newline at end of file
diff --git a/accord-core/src/test/java/accord/local/cfk/CommandsForKeyTest.java b/accord-core/src/test/java/accord/local/cfk/CommandsForKeyTest.java
new file mode 100644
index 00000000..c893702f
--- /dev/null
+++ b/accord-core/src/test/java/accord/local/cfk/CommandsForKeyTest.java
@@ -0,0 +1,1093 @@
+/*
+ * 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 accord.local.cfk;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.junit.jupiter.api.Test;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.Agent;
+import accord.api.Data;
+import accord.api.DataStore;
+import accord.api.Key;
+import accord.api.ProgressLog;
+import accord.api.Query;
+import accord.api.Read;
+import accord.api.Result;
+import accord.api.RoutingKey;
+import accord.api.Update;
+import accord.impl.IntKey;
+import accord.local.Command;
+import accord.local.Command.AbstractCommand;
+import accord.local.CommandStore;
+import accord.local.CommandStores;
+import accord.local.CommonAttributes;
+import accord.local.Listeners;
+import accord.local.Node;
+import accord.local.NodeTimeService;
+import accord.local.PreLoadContext;
+import accord.local.SafeCommand;
+import accord.local.SafeCommandStore;
+import accord.local.SaveStatus;
+import accord.local.Status;
+import accord.local.cfk.CommandsForKey.TxnInfo;
+import accord.primitives.Ballot;
+import accord.primitives.Deps;
+import accord.primitives.EpochSupplier;
+import accord.primitives.FullRoute;
+import accord.primitives.Keys;
+import accord.primitives.Participants;
+import accord.primitives.Range;
+import accord.primitives.Ranges;
+import accord.primitives.Routable.Domain;
+import accord.primitives.Route;
+import accord.primitives.Seekable;
+import accord.primitives.Seekables;
+import accord.primitives.Timestamp;
+import accord.primitives.Txn;
+import accord.primitives.TxnId;
+import accord.primitives.Unseekables;
+import accord.primitives.Writes;
+import accord.utils.DefaultRandom;
+import accord.utils.Invariants;
+import accord.utils.RandomSource;
+import accord.utils.async.AsyncChain;
+import accord.utils.async.AsyncResults;
+
+import static accord.local.Command.NotDefined.notDefined;
+import static accord.local.Status.Durability.NotDurable;
+
+// TODO (expected): test setting redundant before
+// TODO (expected): test ballot updates
+// TODO (expected): test transition to Erased
+// TODO (expected): ensure execution is not too early
+// TODO (expected): validate mapReduce
+// TODO (expected): insert linearizability violations and detect them
+public class CommandsForKeyTest
+{
+ private static final Logger logger = LoggerFactory.getLogger(CommandsForKeyTest.class);
+ private static final Key KEY = IntKey.key(1);
+ private static final Range RANGE = IntKey.range(0, 2);
+ private static final Keys KEYS = IntKey.keys(1);
+ private static final Ranges RANGES = Ranges.of(IntKey.range(0, 2));
+ private static final Txn KEY_TXN = new Txn.InMemory(KEYS, new TestRead(KEYS), new TestQuery());
+ private static final Txn RANGE_TXN = new Txn.InMemory(RANGES, new TestRead(RANGES), new TestQuery());
+ private static final FullRoute KEY_ROUTE = KEYS.toRoute(IntKey.routing(1));
+ private static final FullRoute RANGE_ROUTE = RANGES.toRoute(IntKey.routing(1));
+
+ static class CommandUpdate
+ {
+ final Command prev, next;
+
+ CommandUpdate(Command prev, Command next)
+ {
+ this.prev = prev;
+ this.next = next;
+ }
+ }
+
+ // TODO (expected): randomise ballots
+ static class Canon implements NotifySink
+ {
+ private static final TxnId MIN = new TxnId(1, 1, Txn.Kind.Read, Domain.Key, new Node.Id(1));
+
+ // TODO (expected): randomise ratios
+ static final Txn.Kind[] KINDS = new Txn.Kind[] { Txn.Kind.Read, Txn.Kind.Write, Txn.Kind.EphemeralRead, Txn.Kind.SyncPoint, Txn.Kind.ExclusiveSyncPoint };
+ final RandomSource rnd;
+ final Node.Id[] nodeIds;
+ final Domain[] domains;
+ final TreeSet<TxnId> unwitnessed = new TreeSet<>();
+ final TreeSet<TxnId> undecided = new TreeSet<>();
+ final TreeSet<TxnId> candidates = new TreeSet<>();
+ final TreeSet<TxnId> unfinished = new TreeSet<>();
+ final TreeMap<Timestamp, Command> byId = new TreeMap<>();
+ final TreeMap<Timestamp, Command> committedByExecuteAt = new TreeMap<>();
+ final Set<Timestamp> executeAts = new HashSet<>();
+ boolean closing;
+ int undecidedCount;
+
+ static final EnumMap<SaveStatus, SaveStatus[]> TRANSITIONS = new EnumMap<>(SaveStatus.class);
+
+ void set(Command prev, Command next)
+ {
+ byId.put(next.txnId(), next);
+ if (next.hasBeen(Status.Committed))
+ {
+ undecided.remove(next.txnId());
+ committedByExecuteAt.put(next.executeAt(), next);
+ if (next.hasBeen(Status.Stable))
+ {
+ if (prev.saveStatus() != next.saveStatus())
+ candidates.remove(next.txnId());
+
+ if (next.hasBeen(Status.Applied))
+ {
+ unfinished.remove(next.txnId());
+ if (!CommandsForKey.managesExecution(next.txnId()))
+ removeWaitingOn(next.txnId(), Timestamp.MAX);
+ }
+ else
+ {
+ Command.Committed committed = next.asCommitted();
+ if (!committed.isWaitingOnDependency() && (!prev.hasBeen(Status.Stable) || prev.asCommitted().isWaitingOnDependency()))
+ readyToExecute(committed);
+ }
+ }
+ if (next.hasBeen(Status.Committed) && !prev.hasBeen(Status.Committed) && !next.hasBeen(Status.Invalidated))
+ {
+ if (!next.executeAt().equals(next.txnId()) && !CommandsForKey.manages(next.txnId()))
+ removeWaitingOn(next.txnId(), next.executeAt());
+ }
+
+ }
+ }
+
+ private void readyToExecute(Command.Committed committed)
+ {
+ for (Command pred : committedByExecuteAt.headMap(committed.executeAt(), false).values())
+ Invariants.checkState(pred.hasBeen(Status.Applied) || !committed.txnId().kind().witnesses(pred.txnId()));
+ candidates.add(committed.txnId());
+ }
+
+ private void removeWaitingOn(TxnId waitingId, Timestamp until)
+ {
+ for (Command command : new ArrayList<>(committedByExecuteAt.subMap(waitingId, false, until, false).values()))
+ {
+ if (!command.hasBeen(Status.Stable))
+ continue;
+
+ Command.Committed committed = command.asCommitted();
+ Command.WaitingOn waitingOn = committed.waitingOn;
+ if (waitingOn.isWaitingOn(waitingId))
+ {
+ Command.WaitingOn.Update update = new Command.WaitingOn.Update(waitingOn);
+ update.removeWaitingOn(waitingId);
+ set(committed, Command.Committed.committed(committed, committed, update.build()));
+ }
+ }
+ }
+
+ @Override
+ public void notWaiting(SafeCommandStore safeStore, TxnId txnId, Key key)
+ {
+ Command.Committed prev = byId.get(txnId).asCommitted();
+ Command.WaitingOn.Update waitingOn = new Command.WaitingOn.Update(prev.waitingOn);
+ if (!waitingOn.removeWaitingOn(key))
+ return;
+
+ if (!prev.txnId().kind().awaitsOnlyDeps())
+ {
+ for (Command command : committedByExecuteAt.headMap(prev.executeAt(), false).values())
+ Invariants.checkState(command.txnId().domain() == Domain.Range || !prev.txnId().kind().witnesses(command.txnId()) || command.saveStatus().compareTo(SaveStatus.Applied) >= 0);
+ }
+
+ if (prev.txnId().domain() == Domain.Key)
+ {
+ for (Command command : committedByExecuteAt.tailMap(prev.executeAt(), false).values())
+ Invariants.checkState(command.txnId().kind().awaitsOnlyDeps() || !command.txnId().kind().witnesses(prev.txnId()) || command.saveStatus().compareTo(SaveStatus.Stable) < 0 || command.asCommitted().waitingOn.isWaitingOnKey(0));
+ }
+
+ Command.Committed next = Command.Committed.committed(prev, prev, waitingOn.build());
+ set(prev, next);
+ }
+
+ @Override
+ public void notWaiting(SafeCommandStore safeStore, SafeCommand safeCommand, Key key)
+ {
+ notWaiting(safeStore, safeCommand.txnId(), key);
+ }
+
+ @Override
+ public void waitingOnCommit(SafeCommandStore safeStore, TxnInfo uncommitted, Key key)
+ {
+ }
+
+ static
+ {
+ TRANSITIONS.put(SaveStatus.NotDefined, new SaveStatus[] { SaveStatus.PreAccepted, SaveStatus.AcceptedInvalidate, SaveStatus.AcceptedInvalidateWithDefinition, SaveStatus.Accepted, SaveStatus.AcceptedWithDefinition, SaveStatus.Committed, SaveStatus.Stable, SaveStatus.Invalidated });
+ TRANSITIONS.put(SaveStatus.PreAccepted, new SaveStatus[] { SaveStatus.AcceptedInvalidateWithDefinition, SaveStatus.AcceptedWithDefinition, SaveStatus.Committed, SaveStatus.Stable, SaveStatus.Invalidated });
+ // permit updated ballot and moving to other statuses
+ TRANSITIONS.put(SaveStatus.AcceptedInvalidate, new SaveStatus[] { SaveStatus.Invalidated });
+ TRANSITIONS.put(SaveStatus.AcceptedInvalidateWithDefinition, new SaveStatus[] { SaveStatus.Invalidated });
+ TRANSITIONS.put(SaveStatus.Accepted, new SaveStatus[] { SaveStatus.Committed, SaveStatus.Stable, SaveStatus.Invalidated });
+ TRANSITIONS.put(SaveStatus.AcceptedWithDefinition, new SaveStatus[] { SaveStatus.Committed, SaveStatus.Stable, SaveStatus.Invalidated });
+ TRANSITIONS.put(SaveStatus.Committed, new SaveStatus[] { SaveStatus.Stable });
+ TRANSITIONS.put(SaveStatus.Stable, new SaveStatus[] { SaveStatus.Applied });
+ }
+
+ Canon(RandomSource rnd)
+ {
+ this.rnd = rnd;
+ this.nodeIds = new Node.Id[10];
+ for (int i = 0 ; i < nodeIds.length ; ++i)
+ nodeIds[i] = new Node.Id(i + 1);
+ this.domains = Domain.values();
+ }
+
+ Canon(RandomSource rnd, Node.Id[] nodeIds)
+ {
+ this.rnd = rnd;
+ this.nodeIds = nodeIds;
+ this.domains = Domain.values();
+ }
+
+ boolean isDone()
+ {
+ return closing && unfinished.size() == 0;
+ }
+
+ void close()
+ {
+ closing = true;
+ }
+
+ CommandUpdate update(boolean hasWaitingTasks)
+ {
+ boolean generate = !closing && rnd.decide(1 / (1f + undecidedCount));
+ if (!generate && candidates.isEmpty() && hasWaitingTasks)
+ return null;
+ Invariants.checkArgument (!candidates.isEmpty() || (unfinished.size() == unwitnessed.size()));
+ Command prev = generate || candidates.isEmpty() ? unwitnessed(generateId()) : selectOne(candidates);
+ boolean invalidate = false;
+ if (prev.txnId().kind() == Txn.Kind.ExclusiveSyncPoint && !prev.hasBeen(Status.Committed))
+ {
+ // may need to invalidate
+ if (byId.tailMap(prev.txnId(), false).values().stream().anyMatch(c -> c.hasBeen(Status.Committed) && c.txnId().kind().witnesses(prev.txnId())))
+ invalidate = true;
+ }
+ Command next = invalidate ? AbstractCommand.validate((AbstractCommand)update(prev, SaveStatus.Invalidated))
+ : AbstractCommand.validate((AbstractCommand)update(prev));
+ set(prev, next);
+ unwitnessed.remove(next.txnId());
+ return new CommandUpdate(prev, next);
+ }
+
+ private Command update(Command prev)
+ {
+ SaveStatus[] candidates = TRANSITIONS.get(prev.saveStatus());
+ return update(prev, candidates[rnd.nextInt(candidates.length)]);
+ }
+
+ private Command update(Command prev, SaveStatus newStatus)
+ {
+ switch (newStatus)
+ {
+ default:
+ case NotDefined:
+ case TruncatedApply:
+ case TruncatedApplyWithDeps:
+ case TruncatedApplyWithOutcome:
+ case PreApplied:
+ case PreCommitted:
+ throw new AssertionError();
+
+ case PreAccepted:
+ return preaccepted(prev.txnId());
+
+ case Accepted:
+ case AcceptedWithDefinition:
+ return accepted(prev.txnId(), generateExecuteAt(prev.txnId()), newStatus);
+
+ case AcceptedInvalidate:
+ case AcceptedInvalidateWithDefinition:
+ return acceptedInvalidated(prev.txnId(), newStatus);
+
+ case Committed:
+ return committed(prev.txnId(), prev.executeAtIfKnown(generateExecuteAt(prev.txnId())));
+
+ case Stable:
+ return stable(prev.txnId(), prev.executeAtIfKnown(generateExecuteAt(prev.txnId())), prev.hasBeen(Status.Committed) ? prev.asCommitted() : null);
+
+ case Applied:
+ return applied(prev.txnId(), prev.executeAtIfKnown(generateExecuteAt(prev.txnId())), prev.hasBeen(Status.Committed) ? prev.asCommitted() : null);
+
+ case Invalidated:
+ return invalidated(prev.txnId());
+ }
+ }
+
+ private Deps generateDeps(TxnId txnId, Timestamp executeAt, Status forStatus)
+ {
+ maybeGenerateUnwitnessed();
+ try (Deps.Builder builder = new Deps.Builder())
+ {
+ for (Command command : byId.headMap(executeAt, false).values())
+ {
+ if (txnId.equals(command.txnId())) continue;
+ if (!txnId.kind().witnesses(command.txnId())) continue;
+
+ if (command.hasBeen(forStatus.compareTo(Status.Accepted) <= 0 ? Status.Committed : Status.Accepted) || rnd.nextBoolean())
+ {
+ unwitnessed.remove(command.txnId());
+ builder.add(command.txnId().domain() == Domain.Key ? KEY : RANGE, command.txnId());
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ private Timestamp generateExecuteAt(TxnId txnId)
+ {
+ if (txnId.kind().awaitsOnlyDeps())
+ return txnId;
+
+ Timestamp min = Timestamp.NONE;
+ if (!committedByExecuteAt.isEmpty())
+ {
+ min = committedByExecuteAt.lastEntry().getValue().executeAt();
+ min = Timestamp.fromValues(min.epoch(), min.hlc() + 1, min.node);
+ }
+
+ if (min.compareTo(txnId) <= 0 && rnd.nextBoolean())
+ return txnId;
+
+ min = Timestamp.max(min, Timestamp.fromValues(txnId.epoch(), txnId.hlc() + 1, txnId.node));
+ Timestamp max = Timestamp.fromValues(min.epoch(), min.hlc() + 100, min.node);
+
+ Timestamp executeAt = generateTimestamp(min, max, true);
+ Invariants.checkState(executeAt.compareTo(txnId) >= 0);
+ executeAts.add(executeAt);
+ return executeAt;
+ }
+
+ private void maybeGenerateUnwitnessed()
+ {
+ int transitiveCount = rnd.decide(1 / (1f + unwitnessed.size())) ? rnd.nextInt(0, 3) : 0;
+ while (transitiveCount-- > 0)
+ {
+ // generate new unwitnessed transactions
+ TxnId next = generateId();
+ byId.put(next, unwitnessed(next));
+ }
+ }
+
+ TxnId generateId()
+ {
+ TxnId min = MIN;
+ TxnId max;
+ if (byId.isEmpty()) max = new TxnId(1, 100, Txn.Kind.Read, Domain.Key, nodeIds[0]);
+ else
+ {
+ max = byId.lastEntry().getValue().txnId();
+ switch (rnd.nextInt(3))
+ {
+ default: throw new AssertionError();
+ case 2:
+ min = max;
+ case 1:
+ max = new TxnId(max.epoch(), max.hlc() + 100, max.kind(), max.domain(), max.node);
+ case 0:
+
+ }
+ }
+ return generateId(min, max, true);
+ }
+
+ TxnId generateId(TxnId min, TxnId max, boolean unique)
+ {
+ TxnId result = generateId(min, max);
+ while (unique && (byId.containsKey(result) || executeAts.contains(result)))
+ result = generateId(min, max);
+ return result;
+ }
+
+ TxnId generateId(TxnId min, TxnId max)
+ {
+ long epoch = min.epoch() == max.epoch() ? min.epoch() : rnd.nextLong(min.epoch(), max.epoch());
+ long hlc = min.hlc() == max.hlc() ? min.hlc() : rnd.nextLong(min.hlc(), max.hlc());
+
+ Node.Id node;
+ if (hlc == min.hlc()) node = min.node.id == nodeIds.length + 1 ? min.node : nodeIds[rnd.nextInt(min.node.id - 1, nodeIds.length)];
+ else if (hlc == max.hlc()) node = max.node.id == 1 ? max.node : nodeIds[rnd.nextInt(0, max.node.id - 1)];
+ else node = rnd.pick(nodeIds);
+
+ Txn.Kind kind;
+ if (hlc == min.hlc()) kind = min.kind();
+ else if (hlc == max.hlc()) kind = max.kind();
+ else kind = rnd.pick(KINDS);
+
+ Domain domain;
+ if (hlc == min.hlc() && min.domain() == Domain.Range) domain = Domain.Range;
+ else if (hlc == max.hlc() && max.domain() == Domain.Key) domain = Domain.Key;
+ else domain = rnd.nextBoolean() ? Domain.Key : Domain.Range;
+
+ return new TxnId(epoch, hlc, kind, domain, node);
+ }
+
+ Timestamp generateTimestamp(Timestamp min, Timestamp max, boolean unique)
+ {
+ Timestamp result = generateTimestamp(min, max);
+ while (unique && (byId.containsKey(result) || executeAts.contains(result)))
+ result = generateTimestamp(min, max);
+ return result;
+ }
+
+ Timestamp generateTimestamp(Timestamp min, Timestamp max)
+ {
+ Invariants.checkArgument(min.flags() == 0);
+ Invariants.checkArgument(max.flags() == 0);
+ long epoch = min.epoch() == max.epoch() ? min.epoch() : rnd.nextLong(min.epoch(), max.epoch());
+ long hlc;
+ Node.Id node;
+ if (min.hlc() == max.hlc()) hlc = min.hlc();
+ else hlc = rnd.nextLong(min.hlc(), max.hlc());
+
+ if (hlc == min.hlc()) node = min.node.id == nodeIds.length + 1 ? min.node : nodeIds[rnd.nextInt(min.node.id - 1, nodeIds.length)];
+ else if (hlc == max.hlc()) node = max.node.id == 1 ? max.node : nodeIds[rnd.nextInt(0, max.node.id - 1)];
+ else node = rnd.pick(nodeIds);
+
+ Timestamp result = Timestamp.fromValues(epoch, hlc, node);
+ Invariants.checkState(result.compareTo(min) >= 0);
+ return result;
+ }
+
+ Command unwitnessed(TxnId txnId)
+ {
+ Command command = notDefined(common(txnId), Ballot.ZERO);
+ unwitnessed.add(txnId);
+ undecided.add(txnId);
+ candidates.add(txnId);
+ unfinished.add(txnId);
+ return command;
+ }
+
+ Command preaccepted(TxnId txnId)
+ {
+ return Command.PreAccepted.preAccepted(common(txnId), txnId, Ballot.ZERO);
+ }
+
+ Command acceptedInvalidated(TxnId txnId, SaveStatus saveStatus)
+ {
+ return Command.Accepted.accepted(common(txnId, saveStatus.known.definition.isKnown()),
+ saveStatus, txnId, Ballot.ZERO, Ballot.ZERO);
+ }
+
+ Command accepted(TxnId txnId, Timestamp executeAt, SaveStatus saveStatus)
+ {
+ Deps deps = generateDeps(txnId, txnId, Status.Accepted);
+ return Command.Accepted.accepted(common(txnId, saveStatus.known.definition.isKnown()).partialDeps(deps.slice(RANGES)),
+ saveStatus, executeAt, Ballot.ZERO, Ballot.ZERO);
+ }
+
+ Command committed(TxnId txnId, Timestamp executeAt)
+ {
+ Deps deps = generateDeps(txnId, executeAt, Status.Committed);
+ return Command.Committed.committed(common(txnId).partialDeps(deps.slice(RANGES)), SaveStatus.Committed, executeAt, Ballot.ZERO, Ballot.ZERO, null);
+ }
+
+ Command stable(TxnId txnId, Timestamp executeAt, @Nullable Command.Committed committed)
+ {
+ Deps deps = committed == null ? generateDeps(txnId, executeAt, Status.Stable) : committed.partialDeps();
+ CommonAttributes common = common(txnId).partialDeps(deps.slice(RANGES));
+ Command.WaitingOn waitingOn = initialiseWaitingOn(txnId, executeAt, common.route(), deps);
+ return Command.Committed.committed(common, SaveStatus.Stable, executeAt, Ballot.ZERO, Ballot.ZERO, waitingOn);
+ }
+
+ Command applied(TxnId txnId, Timestamp executeAt, @Nullable Command.Committed committed)
+ {
+ Deps deps = committed == null ? generateDeps(txnId, executeAt, Status.Applied) : committed.partialDeps();
+ CommonAttributes common = common(txnId).partialDeps(deps.slice(RANGES));
+ Command.WaitingOn waitingOn = committed == null || committed.waitingOn == null ? initialiseWaitingOn(txnId, executeAt, common.route(), deps) : committed.waitingOn;
+ return new Command.Executed(common, SaveStatus.Applied, executeAt, Ballot.ZERO, Ballot.ZERO, waitingOn,
+ new Writes(txnId, executeAt, KEYS, null), new Result(){});
+ }
+
+ Command invalidated(TxnId txnId)
+ {
+ return new Command.Truncated(common(txnId), SaveStatus.Invalidated, Timestamp.NONE, null, null);
+ }
+
+ CommonAttributes.Mutable common(TxnId txnId)
+ {
+ return common(txnId, true);
+ }
+
+ CommonAttributes.Mutable common(TxnId txnId, boolean withDefinition)
+ {
+ CommonAttributes.Mutable result = new CommonAttributes.Mutable(txnId)
+ .durability(NotDurable)
+ .route(txnId.domain() == Domain.Key ? KEY_ROUTE : RANGE_ROUTE);
+
+ if (withDefinition)
+ result.partialTxn((txnId.domain() == Domain.Key ? KEY_TXN : RANGE_TXN).slice(RANGES, true));
+
+ return result;
+ }
+
+ private Command.WaitingOn initialiseWaitingOn(TxnId txnId, Timestamp executeAt, Route<?> route, Deps deps)
+ {
+ Command.WaitingOn.Update waitingOn = Command.WaitingOn.Update.initialise(txnId, route, RANGES, deps);
+ for (int i = 0 ; i < waitingOn.txnIdCount() ; ++i)
+ {
+ TxnId dep = waitingOn.txnId(i);
+ Command command = byId.get(dep);
+ if (command.hasBeen(Status.Applied) || (command.hasBeen(Status.Committed) && command.executeAt().compareTo(executeAt) > 0))
+ waitingOn.removeWaitingOn(dep);
+ }
+ return waitingOn.build();
+ }
+
+ private Command selectOne(TreeSet<TxnId> from)
+ {
+ TxnId bound = generateId(from.first(), from.last());
+ TxnId txnId = from.floor(bound);
+ return byId.get(txnId);
+ }
+ }
+
+ @Test
+ public void testOne()
+ {
+// test(449555883255211L, 1000);
+ test(System.nanoTime(), 1000);
+ }
+
+ @Test
+ public void testMany()
+ {
+ long seed = System.nanoTime();
+ for (int i = 0 ; i < 1000 ; ++i)
+ {
+ test(seed++, 1000);
+ }
+ }
+
+ private static void test(long seed, int minCount)
+ {
+ logger.info("Seed {}", seed);
+ try
+ {
+ final RandomSource rnd = new DefaultRandom(seed);
+
+ final float runTaskChance = Math.max(0.01f, rnd.nextFloat());
+ final float pruneChance = rnd.nextFloat() * (rnd.nextBoolean() ? 0.1f : 0.01f);
+ final int pruneHlcDelta = 1 << rnd.nextInt(10);
+ final int pruneInterval = 1 << rnd.nextInt(5);
+ final Canon canon = new Canon(rnd);
+ TestCommandStore commandStore = new TestCommandStore(pruneInterval, pruneHlcDelta);
+ TestSafeCommandsForKey safeCfk = new TestSafeCommandsForKey(new CommandsForKey(KEY));
+ TestSafeStore safeStore = new TestSafeStore(canon, commandStore, safeCfk);
+ int c = 0;
+ while (!canon.isDone())
+ {
+ if (++c >= minCount)
+ canon.close();
+
+ if (rnd.decide(runTaskChance - (runTaskChance/(1 + commandStore.queue.size()))))
+ commandStore.runOneTask(safeStore);
+
+ CommandUpdate update = canon.update(!commandStore.queue.isEmpty());
+ if (update == null)
+ {
+ commandStore.runOneTask(safeStore);
+ continue;
+ }
+ TestSafeCommand safeCommand = new TestSafeCommand(update.next.txnId(), canon, update.next);
+
+ CommandsForKeyUpdate result;
+ if (CommandsForKey.manages(update.next.txnId()))
+ {
+ CommandsForKey prev = safeCfk.current();
+ result = prev.update(update.next);
+ safeCfk.set(result.cfk());
+ if (rnd.decide(pruneChance))
+ safeCfk.set(safeCfk.current.maybePrune(pruneInterval, pruneHlcDelta));
+ result.postProcess(safeStore, prev, update.next, canon);
+ }
+
+ if (!CommandsForKey.managesExecution(update.next.txnId()) && update.next.hasBeen(Status.Stable) && !update.next.hasBeen(Status.Truncated))
+ {
+ CommandsForKey prev = safeCfk.current();
+ result = prev.registerUnmanaged(safeStore, safeCommand, canon);
+ safeCfk.set(result.cfk());
+ result.postProcess(safeStore, prev, null, canon);
+ }
+ }
+ }
+ catch (Throwable t)
+ {
+ throw new AssertionError("Seed " + seed + " failed", t);
+ }
+ }
+
+
+ static class TestSafeCommand extends SafeCommand
+ {
+ final Canon canon;
+ Command current;
+ public TestSafeCommand(TxnId txnId, Canon canon, Command command)
+ {
+ super(txnId);
+ this.canon = canon;
+ current = command;
+ }
+
+ @Override
+ public Command current() { return current; }
+
+ @Override
+ public void invalidate() {}
+
+ @Override
+ public boolean invalidated() { return false; }
+
+ @Override
+ public void addListener(Command.TransientListener listener) {}
+
+ @Override
+ public boolean removeListener(Command.TransientListener listener) { return false; }
+
+ @Override
+ public Listeners<Command.TransientListener> transientListeners() { return null; }
+
+ @Override
+ protected void set(Command command)
+ {
+ canon.set(current, command);
+ current = command;
+ }
+ }
+
+ static class TestSafeCommandsForKey extends SafeCommandsForKey
+ {
+ CommandsForKey current;
+ public TestSafeCommandsForKey(CommandsForKey cfk)
+ {
+ super(cfk.key());
+ current = cfk;
+ }
+
+ @Override
+ public CommandsForKey current() { return current; }
+
+ @Override
+ protected void set(CommandsForKey command)
+ {
+ current = command;
+ }
+ }
+
+ static class TestSafeStore extends SafeCommandStore
+ {
+ final Canon canon;
+ final TestCommandStore commandStore;
+ final TestSafeCommandsForKey cfk;
+
+ TestSafeStore(Canon canon, TestCommandStore commandStore, TestSafeCommandsForKey cfk)
+ {
+ this.canon = canon;
+ this.commandStore = commandStore;
+ this.cfk = cfk;
+ }
+
+ @Override
+ protected SafeCommand getInternal(TxnId txnId)
+ {
+ return new TestSafeCommand(txnId, canon, canon.byId.get(txnId));
+ }
+
+ @Override
+ protected SafeCommand getInternalIfLoadedAndInitialised(TxnId txnId)
+ {
+ return getInternal(txnId);
+ }
+
+ @Override
+ protected SafeCommandsForKey getInternal(Key key)
+ {
+ Invariants.checkArgument(key.equals(cfk.key()));
+ return cfk;
+ }
+
+ @Override
+ public SafeCommand get(TxnId txnId, RoutingKey unseekable)
+ {
+ return getInternal(txnId);
+ }
+
+ @Override
+ public SafeCommand get(TxnId txnId)
+ {
+ return getInternal(txnId);
+ }
+
+ @Override
+ public SafeCommandsForKey get(Key key)
+ {
+ return getInternal(key);
+ }
+
+ @Override
+ public SafeCommand get(TxnId txnId, EpochSupplier toEpoch, Unseekables<?> unseekables)
+ {
+ return getInternal(txnId);
+ }
+
+ @Nullable
+ @Override
+ public SafeCommand ifInitialised(TxnId txnId)
+ {
+ return getInternal(txnId);
+ }
+
+ @Override
+ public SafeCommand ifLoadedAndInitialised(TxnId txnId)
+ {
+ if (txnId.compareTo(cfk.current.prunedBefore()) < 0)
+ return null;
+
+ return getInternal(txnId);
+ }
+
+ @Override
+ protected void update(Command prev, Command updated)
+ {
+ }
+
+ @Override
+ protected SafeCommandsForKey getInternalIfLoadedAndInitialised(Key key)
+ {
+ if (key.equals(cfk.key()))
+ return cfk;
+ return null;
+ }
+
+ @Override
+ public boolean canExecuteWith(PreLoadContext context)
+ {
+ return true;
+ }
+
+ @Override
+ public <P1, T> T mapReduceActive(Seekables<?, ?> keys, Ranges slice, @Nullable Timestamp withLowerTxnId, Txn.Kind.Kinds kinds, CommandFunction<P1, T, T> map, P1 p1, T initialValue)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <P1, T> T mapReduceFull(Seekables<?, ?> keys, Ranges slice, TxnId testTxnId, Txn.Kind.Kinds testKind, TestStartedAt testStartedAt, TestDep testDep, TestStatus testStatus, CommandFunction<P1, T, T> map, P1 p1, T initialValue)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CommandStore commandStore()
+ {
+ return commandStore;
+ }
+
+ @Override
+ public DataStore dataStore()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Agent agent()
+ {
+ return commandStore;
+ }
+
+ @Override
+ public ProgressLog progressLog()
+ {
+ return new TestProgressLog();
+ }
+
+ @Override
+ public NodeTimeService time()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CommandStores.RangesForEpoch ranges()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void registerHistoricalTransactions(Deps deps)
+ {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static final class TestRead implements Read
+ {
+ final Seekables<?, ?> keys;
+ private TestRead(Seekables<?, ?> keys)
+ {
+ this.keys = keys;
+ }
+
+ @Override
+ public Seekables<?, ?> keys()
+ {
+ return keys;
+ }
+
+ @Override
+ public AsyncChain<Data> read(Seekable key, SafeCommandStore commandStore, Timestamp executeAt, DataStore store)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Read slice(Ranges ranges)
+ {
+ return this;
+ }
+
+ @Override
+ public Read merge(Read other)
+ {
+ return this;
+ }
+ }
+
+ private static class TestQuery implements Query
+ {
+ @Override
+ public Result compute(@Nonnull TxnId txnId, @Nonnull Timestamp executeAt, @Nonnull Seekables<?, ?> keys, @Nullable Data data, @Nullable Read read, @Nullable Update update)
+ {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static class TestCommandStore extends CommandStore implements Agent
+ {
+ class Task extends AsyncResults.AbstractResult<Void>
+ {
+ final Consumer<? super SafeCommandStore> consumer;
+ Task(Consumer<? super SafeCommandStore> consumer)
+ {
+ this.consumer = consumer;
+ }
+
+ public void run(SafeCommandStore safeStore)
+ {
+ consumer.accept(safeStore);
+ trySetResult(null, null);
+ }
+ }
+
+ final int pruneInterval, pruneHlcDelta;
... 794 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org