in accord-core/src/main/java/accord/coordinate/Invalidate.java [145:261]
private void invalidate()
{
Invariants.checkState(!isPrepareDone);
isPrepareDone = true;
// first look to see if it has already been
{
FullRoute<?> route = InvalidateReply.findRoute(replies);
RoutingKey homeKey = route != null ? route.homeKey() : InvalidateReply.findHomeKey(replies);
InvalidateReply maxReply = InvalidateReply.max(replies);
switch (maxReply.status)
{
default: throw new IllegalStateException();
case AcceptedInvalidate:
// latest accept also invalidating, so we're on the same page and should finish our invalidation
case NotWitnessed:
break;
case PreAccepted:
if (tracker.isSafeToInvalidate() || transitivelyInvokedByPriorInvalidation)
break;
case Accepted:
case PreCommitted:
case Committed:
case ReadyToExecute:
case PreApplied:
case Applied:
// TODO (desired, efficiency): if we see Committed or above, go straight to Execute if we have assembled enough information
if (route != null)
{
// The data we see might have made it only to a minority in the event of PreAccept ONLY.
// We want to protect against infinite loops, so we inform the recovery of the state we have
// witnessed during our initial invalidation.
// However, if the state is not guaranteed to be recoverable (i.e. PreAccept/NotWitnessed),
// we do not relay this information unless we can guarantee that any shard recovery may contact
// has been prevented from reaching a _later_ fast-path decision by our promises.
// Which means checking we contacted every shard, since we only reach this point if we have promises
// from every shard we contacted.
// Note that there's lots of scope for variations in behaviour here, but lots of care is needed.
Status witnessedByInvalidation = maxReply.status;
if (!witnessedByInvalidation.hasBeen(Accepted))
{
Invariants.checkState(tracker.all(InvalidationShardTracker::isPromised));
if (!invalidateWith.containsAll(route))
witnessedByInvalidation = null;
}
RecoverWithRoute.recover(node, ballot, txnId, route, witnessedByInvalidation, callback);
}
else if (homeKey != null)
{
Invariants.checkState(maxReply.status.hasBeen(Accepted) || tracker.all(InvalidationShardTracker::isPromised));
// if we included the home shard, and we have either a recoverable status OR have not rejected the fast path,
// we must have at least one response that should contain the Route
if (invalidateWith.contains(homeKey) && tracker.isPromisedForKey(homeKey, txnId.epoch()))
throw new IllegalStateException("Received replies from a node that must have known the route, but that did not include it");
// if < Accepted, we should have short-circuited to invalidation above. This guarantees no Invaldate/Recover loop, as any later status will forbid invoking Invalidate
Invariants.checkState(!(transitivelyInvokedByPriorInvalidation && !maxReply.status.hasBeen(Accepted)));
Status witnessedByInvalidation = maxReply.status;
if (!witnessedByInvalidation.hasBeen(Accepted))
{
Invariants.checkState(tracker.all(InvalidationShardTracker::isPromised));
if (!invalidateWith.contains(homeKey))
witnessedByInvalidation = null;
}
RecoverWithHomeKey.recover(node, txnId, homeKey, witnessedByInvalidation, callback);
}
else
{
throw new IllegalStateException("Received a reply from a node that must have known the homeKey, but that did not include it");
}
return;
case Invalidated:
// TODO (desired, API consistency): standardise semantics of whether local application of state prior is async or sync to callback
isDone = true;
commitInvalidate();
return;
}
}
// if we have witnessed the transaction, but are able to invalidate, do we want to proceed?
// Probably simplest to do so, but perhaps better for user if we don't.
Ranges ranges = Ranges.of(tracker.promisedShard().range);
// we look up by TxnId at the target node, so it's fine to pick a RoutingKey even if it's a range transaction
RoutingKey invalidateWithKey = invalidateWith.slice(ranges).get(0).someIntersectingRoutingKey(ranges);
proposeInvalidate(node, ballot, txnId, invalidateWithKey, (success, fail) -> {
/*
We're now inside our *exactly once* callback we registered with proposeInvalidate, and we need to
make sure we honour our own exactly once semantics with {@code callback}.
So we are responsible for all exception handling.
*/
isDone = true;
if (fail != null)
{
callback.accept(null, fail);
}
else
{
try
{
commitInvalidate();
}
catch (Throwable t)
{
callback.accept(null, t);
}
}
});
}