/* * Copyright 2014-2017 Real Logic Ltd. * * 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.aeron.driver; import io.aeron.driver.status.SystemCounters; import io.aeron.protocol.DataHeaderFlyweight; import org.agrona.collections.BiInt2ObjectMap; import org.agrona.concurrent.status.AtomicCounter; import org.agrona.concurrent.NanoClock; import static io.aeron.driver.Configuration.MAX_RETRANSMITS_DEFAULT; import static io.aeron.driver.status.SystemCounterDescriptor.INVALID_PACKETS; /** * Tracking and handling of retransmit request, NAKs, for senders, and receivers. * * A max number of retransmits is permitted by {@link Configuration#MAX_RETRANSMITS_DEFAULT}. Additional received NAKs * will be ignored if this maximum is reached. */ public class RetransmitHandler { private final BiInt2ObjectMap<RetransmitAction> activeRetransmitsMap = new BiInt2ObjectMap<>(); private final RetransmitAction[] retransmitActionPool = new RetransmitAction[MAX_RETRANSMITS_DEFAULT]; private final NanoClock nanoClock; private final FeedbackDelayGenerator delayGenerator; private final FeedbackDelayGenerator lingerTimeoutGenerator; private final AtomicCounter invalidPackets; /** * Create a retransmit handler. * * @param nanoClock used to determine time * @param systemCounters for recording significant events. * @param delayGenerator to use for delay determination * @param lingerTimeoutGenerator to use for linger timeout */ public RetransmitHandler( final NanoClock nanoClock, final SystemCounters systemCounters, final FeedbackDelayGenerator delayGenerator, final FeedbackDelayGenerator lingerTimeoutGenerator) { this.nanoClock = nanoClock; this.invalidPackets = systemCounters.get(INVALID_PACKETS); this.delayGenerator = delayGenerator; this.lingerTimeoutGenerator = lingerTimeoutGenerator; for (int i = 0; i < MAX_RETRANSMITS_DEFAULT; i++) { retransmitActionPool[i] = new RetransmitAction(); } } /** * Called on reception of a NAK to start retransmits handling. * * @param termId from the NAK and the term id of the buffer to retransmit from * @param termOffset from the NAK and the offset of the data to retransmit * @param length of the missing data * @param termLength of the term buffer. * @param retransmitSender to call if an immediate retransmit is required */ public void onNak( final int termId, final int termOffset, final int length, final int termLength, final RetransmitSender retransmitSender) { if (!isInvalid(termOffset, termLength)) { if (null == activeRetransmitsMap.get(termId, termOffset) && activeRetransmitsMap.size() < MAX_RETRANSMITS_DEFAULT) { final RetransmitAction action = assignRetransmitAction(); action.termId = termId; action.termOffset = termOffset; action.length = Math.min(length, termLength - termOffset); final long delay = determineRetransmitDelay(); if (0 == delay) { retransmitSender.resend(termId, termOffset, action.length); action.linger(determineLingerTimeout(), nanoClock.nanoTime()); } else { action.delay(delay, nanoClock.nanoTime()); } activeRetransmitsMap.put(termId, termOffset, action); } } } /** * Called to indicate a retransmission is received that may obviate the need to send one ourselves. * * NOTE: Currently only called from unit tests. Would be used for retransmitting from receivers for NAK suppression * * @param termId of the data * @param termOffset of the data */ public void onRetransmitReceived(final int termId, final int termOffset) { final RetransmitAction action = activeRetransmitsMap.get(termId, termOffset); if (null != action && State.DELAYED == action.state) { activeRetransmitsMap.remove(termId, termOffset); action.cancel(); // do not go into linger } } /** * Called to process any outstanding timeouts. * * @param nowNs time in nanoseconds * @param retransmitSender to call on retransmissions * @return count of expired actions performed */ public int processTimeouts(final long nowNs, final RetransmitSender retransmitSender) { int result = 0; if (activeRetransmitsMap.size() > 0) { for (final RetransmitAction action : retransmitActionPool) { switch (action.state) { case DELAYED: if (nowNs > action.expireNs) { retransmitSender.resend(action.termId, action.termOffset, action.length); action.linger(determineLingerTimeout(), nanoClock.nanoTime()); result++; } break; case LINGERING: if (nowNs > action.expireNs) { action.cancel(); activeRetransmitsMap.remove(action.termId, action.termOffset); result++; } break; } } } return result; } private boolean isInvalid(final int termOffset, final int termLength) { final boolean isInvalid = (termOffset > (termLength - DataHeaderFlyweight.HEADER_LENGTH)) || (termOffset < 0); if (isInvalid) { invalidPackets.increment(); } return isInvalid; } private long determineRetransmitDelay() { return delayGenerator.generateDelay(); } private long determineLingerTimeout() { return lingerTimeoutGenerator.generateDelay(); } private RetransmitAction assignRetransmitAction() { for (final RetransmitAction action : retransmitActionPool) { if (State.INACTIVE == action.state) { return action; } } throw new IllegalStateException("Maximum number of active RetransmitActions reached"); } private enum State { DELAYED, LINGERING, INACTIVE } static final class RetransmitAction { long expireNs; int termId; int termOffset; int length; State state = State.INACTIVE; public void delay(final long delayNs, final long nowNs) { state = State.DELAYED; expireNs = nowNs + delayNs; } public void linger(final long timeoutNs, final long nowNs) { state = State.LINGERING; expireNs = nowNs + timeoutNs; } public void cancel() { state = State.INACTIVE; } } }