All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.atomix.copycat.client.session.ClientSequencer Maven / Gradle / Ivy

There is a newer version: 1.2.8
Show newest version
/*
 * 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.debug("{} - 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.debug("{} - 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.debug("{} - 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.debug("{} - 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(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy