
io.atomix.copycat.client.session.ClientSequencer Maven / Gradle / Ivy
/*
* Copyright 2016 the original author or authors.
*
* Licensed 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 io.atomix.copycat.client.session;
import io.atomix.copycat.protocol.OperationResponse;
import io.atomix.copycat.protocol.PublishRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
/**
* Client response sequencer.
*
* The way operations are applied to replicated state machines, allows responses to be handled in a consistent
* manner. Command responses will always have an {@code eventIndex} less than the response {@code index}. This is
* because commands always occur before the events they trigger, and because events are always associated
* with a command index and never a query index, the previous {@code eventIndex} for a command response will always be less
* than the response {@code index}.
*
* Alternatively, the previous {@code eventIndex} for a query response may be less than or equal to the response
* {@code index}. However, in contrast to commands, queries always occur after prior events. This means
* for a given index, the precedence is command -> event -> query.
*
* Since operations for an index will always occur in a consistent order, sequencing operations is a trivial task.
* When a response is received, once the response is placed in sequential order, pending events up to the response's
* {@code eventIndex} may be completed. Because command responses will never have an {@code eventIndex} equal to their
* own response {@code index}, events will always stop prior to the command. But query responses may have an
* {@code eventIndex} equal to their own response {@code index}, and in that case the event will be completed prior
* to the completion of the query response.
*
* Events can also be received later than sequenced operations. When an event is received, it's first placed in
* sequential order as is the case with operation responses. Once placed in sequential order, if no requests are
* outstanding, the event is immediately completed. This ensures that events that are published during a period
* of inactivity in the session can still be completed upon reception since the event is guaranteed not to have
* occurred concurrently with any other operation. If requests for the session are outstanding, the event is placed
* in a queue and the algorithm for checking sequenced responses is run again.
*
* @author
* When an operation is sequenced, it's first sequenced in the order in which it was submitted to the cluster.
* Once placed in sequential request order, if a response's {@code eventIndex} is greater than the last completed
* {@code eventIndex}, we attempt to sequence pending events. If after sequencing pending events the response's
* {@code eventIndex} is equal to the last completed {@code eventIndex} then the response can be immediately
* completed. If not enough events are pending to meet the sequence requirement, the sequencing of responses is
* stopped until events are received.
*
* @param sequence The request sequence number.
* @param response The response to sequence.
* @param callback The callback to sequence.
*/
public void sequenceResponse(long sequence, OperationResponse response, Runnable callback) {
// If the request sequence number is equal to the next response sequence number, attempt to complete the response.
if (sequence == responseSequence + 1) {
if (completeResponse(response, callback)) {
++responseSequence;
completeResponses();
} else {
responseCallbacks.put(sequence, new ResponseCallback(response, callback));
}
} else {
responseCallbacks.put(sequence, new ResponseCallback(response, callback));
}
}
/**
* Completes all sequenced responses.
*/
private void completeResponses() {
// Iterate through queued responses and complete as many as possible.
ResponseCallback response = responseCallbacks.get(responseSequence + 1);
while (response != null) {
// If the response was completed, remove the response callback from the response queue,
// increment the response sequence number, and check the next response.
if (completeResponse(response.response, response.callback)) {
responseCallbacks.remove(++responseSequence);
response = responseCallbacks.get(responseSequence + 1);
} else {
break;
}
}
// Once we've completed as many responses as possible, if no more operations are outstanding
// and events remain in the event queue, complete the events.
if (requestSequence == responseSequence) {
EventCallback eventCallback = eventCallbacks.poll();
while (eventCallback != null) {
LOGGER.trace("{} - Completing {}", state.getSessionId(), eventCallback.request);
eventCallback.run();
eventIndex = eventCallback.request.eventIndex();
eventCallback = eventCallbacks.poll();
}
}
}
/**
* Completes a sequenced response if possible.
*/
private boolean completeResponse(OperationResponse response, Runnable callback) {
// If the response is null, that indicates an exception occurred. The best we can do is complete
// the response in sequential order.
if (response == null) {
LOGGER.trace("{} - Completing failed request", state.getSessionId());
callback.run();
return true;
}
// If the response's event index is greater than the current event index, that indicates that events that were
// published prior to the response have not yet been completed. Attempt to complete pending events.
long responseEventIndex = response.eventIndex();
if (responseEventIndex > eventIndex) {
// For each pending event with an eventIndex less than or equal to the response eventIndex, complete the event.
// This is safe since we know that sequenced responses should see sequential order of events.
EventCallback eventCallback = eventCallbacks.peek();
while (eventCallback != null && eventCallback.request.eventIndex() <= responseEventIndex) {
eventCallbacks.remove();
LOGGER.trace("{} - Completing {}", state.getSessionId(), eventCallback.request);
eventCallback.run();
eventIndex = eventCallback.request.eventIndex();
eventCallback = eventCallbacks.peek();
}
// If the response event index is still greater than the last sequenced event index, check
// enqueued events to determine whether any events can be skipped. This is necessary to
// ensure that a response with a missing event can still trigger prior events.
if (responseEventIndex > eventIndex) {
for (EventCallback event : eventCallbacks) {
// If the event's previous index is consistent with the current event index and the event
// index is greater than the response event index, set the response event index to the
// event's previous index.
if (event.request.previousIndex() <= eventIndex && event.request.eventIndex() >= response.eventIndex()) {
responseEventIndex = event.request.previousIndex();
break;
}
}
}
}
// If after completing pending events the eventIndex is greater than or equal to the response's eventIndex, complete the response.
// Note that the event protocol initializes the eventIndex to the session ID.
if (responseEventIndex <= eventIndex || (eventIndex == 0 && responseEventIndex == state.getSessionId())) {
LOGGER.trace("{} - Completing {}", state.getSessionId(), response);
callback.run();
return true;
} else {
return false;
}
}
/**
* Response callback holder.
*/
private static final class ResponseCallback implements Runnable {
private final OperationResponse response;
private final Runnable callback;
private ResponseCallback(OperationResponse response, Runnable callback) {
this.response = response;
this.callback = callback;
}
@Override
public void run() {
callback.run();
}
}
/**
* Event callback holder.
*/
private static final class EventCallback implements Runnable {
private final PublishRequest request;
private final Runnable callback;
private EventCallback(PublishRequest request, Runnable callback) {
this.request = request;
this.callback = callback;
}
@Override
public void run() {
callback.run();
}
}
}