/*
* Copyright 2014 WANdisco
*
* WANdisco 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 c5db.replication;
import c5db.ReplicatorConstants;
import c5db.interfaces.replication.GeneralizedReplicator;
import c5db.interfaces.replication.IndexCommitNotice;
import c5db.interfaces.replication.ReplicateSubmissionInfo;
import c5db.interfaces.replication.Replicator;
import c5db.interfaces.replication.ReplicatorInstanceEvent;
import c5db.interfaces.replication.ReplicatorReceipt;
import c5db.util.C5Futures;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.jetbrains.annotations.Nullable;
import org.jetlang.channels.ChannelSubscription;
import org.jetlang.fibers.Fiber;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutionException;
/**
* A GeneralizedReplicator that makes use of a {@link c5db.interfaces.replication.Replicator},
* processing its ReplicatorReceipts and IndexCommitNotices to provide a more general-purpose
* interface.
*/
public class C5GeneralizedReplicator implements GeneralizedReplicator {
private final long nodeId;
private final Replicator replicator;
private final Fiber fiber;
private SettableFuture<Void> availableFuture;
/**
* Queue of receipts for pending log requests and their futures; access this queue only
* from the fiber. See {@link c5db.replication.C5GeneralizedReplicator.ReceiptWithCompletionFuture}
*/
private final Queue<ReceiptWithCompletionFuture> receiptQueue =
new ArrayDeque<>(ReplicatorConstants.REPLICATOR_MAXIMUM_SIMULTANEOUS_LOG_REQUESTS);
/**
* Both the fiber and replicator must be started by the user of this class, and the
* user takes responsibility for their disposal.
*/
public C5GeneralizedReplicator(Replicator replicator, Fiber fiber) {
this.nodeId = replicator.getId();
this.replicator = replicator;
this.fiber = fiber;
setupCommitNoticeSubscription();
setupEventNoticeSubscription();
}
@Override
public ListenableFuture<ReplicateSubmissionInfo> replicate(List<ByteBuffer> data) throws InterruptedException,
InvalidReplicatorStateException {
final ReceiptWithCompletionFuture receiptWithCompletionFuture =
new ReceiptWithCompletionFuture(replicator.logData(data));
if (receiptWithCompletionFuture.receiptFuture == null) {
throw new InvalidReplicatorStateException("Replicator is not in the leader state");
}
// Add to the queue on the fiber for concurrency safety
fiber.execute(
() -> receiptQueue.add(receiptWithCompletionFuture));
return Futures.transform(receiptWithCompletionFuture.receiptFuture,
(ReplicatorReceipt receipt) ->
new ReplicateSubmissionInfo(receipt.seqNum, receiptWithCompletionFuture.completionFuture));
}
@Override
public ListenableFuture<Void> isAvailableFuture() {
SettableFuture<Void> returnedFuture = SettableFuture.create();
fiber.execute(() -> {
if (this.availableFuture == null) {
// By placing this future here, the handleEventNotice method will know someone is waiting
// for notification of availability; that method will handle setting this.
this.availableFuture = returnedFuture;
} else {
// Some other invocation of this method is already waiting for notification of availability,
// so setup that existing availableFuture to "forward" its result to the newly created one.
C5Futures.addCallback(this.availableFuture, returnedFuture::set, returnedFuture::setException, fiber);
}
});
return returnedFuture;
}
private void setupCommitNoticeSubscription() {
final String quorumId = replicator.getQuorumId();
final long serverNodeId = replicator.getId();
replicator.getCommitNoticeChannel().subscribe(
new ChannelSubscription<>(this.fiber, this::handleCommitNotice,
(notice) ->
notice.nodeId == serverNodeId
&& notice.quorumId.equals(quorumId)));
}
private void setupEventNoticeSubscription() {
replicator.getEventChannel().subscribe(fiber, this::handleEventNotice);
}
/**
* The core of the logic is in this method. When we receive an IndexCommitNotice, we need
* to find out which, if any, of the pending replicate requests are affected by it. We do
* that by examining their receipts, if the receipts are available.
*
* @param notice An IndexCommitNotice received from the internal Replicator.
*/
@FiberOnly
private void handleCommitNotice(IndexCommitNotice notice) {
while (!receiptQueue.isEmpty() && receiptQueue.peek().receiptFuture.isDone()) {
ReceiptWithCompletionFuture receiptWithCompletionFuture = receiptQueue.peek();
SettableFuture<Void> completionFuture = receiptWithCompletionFuture.completionFuture;
ReplicatorReceipt receipt = getReceiptOrSetException(receiptWithCompletionFuture);
if (receipt != null) {
if (notice.lastIndex < receipt.seqNum) {
// old commit notice
return;
} else if (receipt.seqNum < notice.firstIndex) {
completionFuture.setException(new IOException("commit notice skipped over the receipt's seqNum"));
} else if (notice.term != receipt.term) {
completionFuture.setException(new IOException("commit notice's term differs from that of receipt"));
} else {
// receipt.seqNum is within the range of the commit notice, and the terms match: replication is complete
completionFuture.set(null);
}
}
receiptQueue.poll();
}
}
/**
* This method runs whenever our wrapped Replicator emits an event notice. It checks to
* see if the replicator is announcing that it has been elected leader of its quorum. If so,
* then this GeneralizedReplicator is now available for submission of replication requests.
* Therefore, check if anyone has been waiting to be notified of our availability, by having
* left an unset availableFuture. If so, notify them by setting that pending future..
*/
@FiberOnly
private void handleEventNotice(ReplicatorInstanceEvent eventNotice) {
if (availableFuture != null
&& eventNotice.instance == replicator
&& eventNotice.eventType == ReplicatorInstanceEvent.EventType.LEADER_ELECTED
&& eventNotice.newLeader == nodeId) {
// Notify past callers of isAvailableFuture
availableFuture.set(null);
// Remove the future so that a subsequent invocation of this method will not try
// to set it again.
availableFuture = null;
}
}
/**
* This method assumes that the receiptFuture is "done". If the future is set with a value,
* return its value, the ReplicatorReceipt. On the other hand, if it is an exception, set
* the completionFuture with that exception and return null. A null return value guarantees
* the completionFuture has been set with an exception.
*/
@Nullable
private ReplicatorReceipt getReceiptOrSetException(ReceiptWithCompletionFuture receiptWithCompletionFuture) {
ListenableFuture<ReplicatorReceipt> receiptFuture = receiptWithCompletionFuture.receiptFuture;
SettableFuture<?> completionFuture = receiptWithCompletionFuture.completionFuture;
assert receiptFuture.isDone();
try {
ReplicatorReceipt receipt = C5Futures.getUninterruptibly(receiptFuture);
if (receipt == null) {
completionFuture.setException(new IOException("replicator returned a null receipt"));
}
return receipt;
} catch (ExecutionException e) {
completionFuture.setException(e);
return null;
}
}
/**
* C5GeneralizedReplicator keeps track of all the times it's requested to replicate
* data. Each of these replication requests, to its internal replicator, yields a
* receipt. Later on, the internal replicator will issue a commit notice, indicating
* that one or more earlier requests have been completed. The receipts are bundled
* together with SettableFutures so that the user of this object can be notified
* when that happens. ReceiptWithCompletionFuture is the class used to bundle the receipt and
* the completion future together.
*/
private static class ReceiptWithCompletionFuture {
public final ListenableFuture<ReplicatorReceipt> receiptFuture;
public final SettableFuture<Void> completionFuture = SettableFuture.create();
private ReceiptWithCompletionFuture(ListenableFuture<ReplicatorReceipt> receiptFuture) {
this.receiptFuture = receiptFuture;
}
}
}