import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
}
if (!current.cohort.getIdentifier().equals(txId)) {
- LOG.warn("{}: Head of queue is {}, ignoring consensus on transaction {}", logContext,
+ LOG.debug("{}: Head of pendingFinishCommits queue is {}, ignoring consensus on transaction {}", logContext,
current.cohort.getIdentifier(), txId);
return;
}
@SuppressWarnings("checkstyle:IllegalCatch")
private void processNextPendingTransaction() {
- while (!pendingTransactions.isEmpty()) {
- final CommitEntry entry = pendingTransactions.peek();
+ processNextPending(pendingTransactions, State.CAN_COMMIT_PENDING, entry -> {
final SimpleShardDataTreeCohort cohort = entry.cohort;
final DataTreeModification modification = cohort.getDataTreeModification();
- if (cohort.isFailed()) {
- LOG.debug("{}: Removing failed transaction {}", logContext, cohort.getIdentifier());
- pendingTransactions.remove();
- continue;
- }
-
- if (cohort.getState() != State.CAN_COMMIT_PENDING) {
- break;
- }
-
LOG.debug("{}: Validating transaction {}", logContext, cohort.getIdentifier());
Exception cause;
try {
// Failure path: propagate the failure, remove the transaction from the queue and loop to the next one
pendingTransactions.poll().cohort.failedCanCommit(cause);
- }
+ });
+ }
- maybeRunOperationOnPendingTransactionsComplete();
+ private void processNextPending() {
+ processNextPendingFinishCommit();
+ processNextPendingCommit();
+ processNextPendingTransaction();
}
- private void processNextPendingCommit() {
- while (!pendingCommits.isEmpty()) {
- final CommitEntry entry = pendingCommits.peek();
+ private void processNextPending(Queue<CommitEntry> queue, State allowedState, Consumer<CommitEntry> processor) {
+ while (!queue.isEmpty()) {
+ final CommitEntry entry = queue.peek();
final SimpleShardDataTreeCohort cohort = entry.cohort;
if (cohort.isFailed()) {
LOG.debug("{}: Removing failed transaction {}", logContext, cohort.getIdentifier());
- pendingCommits.remove();
+ queue.remove();
continue;
}
- if (cohort.getState() == State.COMMIT_PENDING) {
- startCommit(cohort, cohort.getCandidate());
+ if (cohort.getState() == allowedState) {
+ processor.accept(entry);
}
break;
maybeRunOperationOnPendingTransactionsComplete();
}
+ private void processNextPendingCommit() {
+ processNextPending(pendingCommits, State.COMMIT_PENDING,
+ entry -> startCommit(entry.cohort, entry.cohort.getCandidate()));
+ }
+
+ private void processNextPendingFinishCommit() {
+ processNextPending(pendingFinishCommits, State.FINISH_COMMIT_PENDING,
+ entry -> payloadReplicationComplete(entry.cohort.getIdentifier()));
+ }
+
private boolean peekNextPendingCommit() {
final CommitEntry first = pendingCommits.peek();
return first != null && first.cohort.getState() == State.COMMIT_PENDING;
}
- private void processNextPending() {
- processNextPendingCommit();
- processNextPendingTransaction();
- }
-
void startCanCommit(final SimpleShardDataTreeCohort cohort) {
final SimpleShardDataTreeCohort current = pendingTransactions.peek().cohort;
if (!cohort.equals(current)) {
LOG.debug("{}: Starting commit for transaction {}", logContext, current.getIdentifier());
+ final TransactionIdentifier txId = cohort.getIdentifier();
if (shard.canSkipPayload() || candidate.getRootNode().getModificationType() == ModificationType.UNMODIFIED) {
LOG.debug("{}: No replication required, proceeding to finish commit", logContext);
pendingCommits.remove();
pendingFinishCommits.add(entry);
- finishCommit(cohort);
+ cohort.finishCommitPending();
+ payloadReplicationComplete(txId);
return;
}
- final TransactionIdentifier txId = cohort.getIdentifier();
final Payload payload;
try {
payload = CommitTransactionPayload.create(txId, candidate);
assertEquals("Car node", carNode, optional.get());
}
+ @Test
+ public void testPipelinedTransactionsWithUnmodifiedCandidate() throws Exception {
+ doReturn(false).when(mockShard).canSkipPayload();
+
+ final ShardDataTreeCohort cohort1 = newShardDataTreeCohort(snapshot ->
+ snapshot.write(CarsModel.BASE_PATH, CarsModel.emptyContainer()));
+
+ final ShardDataTreeCohort cohort2 = newShardDataTreeCohort(snapshot ->
+ snapshot.merge(CarsModel.BASE_PATH, CarsModel.emptyContainer()));
+
+ final FutureCallback<UnsignedLong> commitCallback1 = immediate3PhaseCommit(cohort1);
+
+ verify(mockShard).persistPayload(eq(cohort1.getIdentifier()), any(CommitTransactionPayload.class), eq(false));
+
+ final FutureCallback<UnsignedLong> commitCallback2 = immediate3PhaseCommit(cohort2);
+
+ verify(mockShard, never()).persistPayload(eq(cohort2.getIdentifier()), any(CommitTransactionPayload.class),
+ anyBoolean());
+
+ // The payload instance doesn't matter - it just needs to be of type CommitTransactionPayload.
+ shardDataTree.applyReplicatedPayload(cohort1.getIdentifier(),
+ CommitTransactionPayload.create(nextTransactionId(), cohort1.getCandidate()));
+
+ InOrder inOrder = inOrder(commitCallback1, commitCallback2);
+ inOrder.verify(commitCallback1).onSuccess(any(UnsignedLong.class));
+ inOrder.verify(commitCallback2).onSuccess(any(UnsignedLong.class));
+
+ final DataTreeSnapshot snapshot =
+ shardDataTree.newReadOnlyTransaction(nextTransactionId()).getSnapshot();
+ Optional<NormalizedNode<?, ?>> optional = snapshot.readNode(CarsModel.BASE_PATH);
+ assertEquals("Car node present", true, optional.isPresent());
+ }
+
@SuppressWarnings("unchecked")
@Test
public void testAbortWithPendingCommits() throws Exception {