/*
* 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 org.junit.Test;
import org.mockito.InOrder;
import io.aeron.logbuffer.FrameDescriptor;
import io.aeron.logbuffer.TermRebuilder;
import io.aeron.protocol.DataHeaderFlyweight;
import io.aeron.protocol.HeaderFlyweight;
import org.agrona.concurrent.UnsafeBuffer;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import static org.mockito.Mockito.*;
import static io.aeron.logbuffer.LogBufferDescriptor.TERM_MIN_LENGTH;
import static io.aeron.logbuffer.LogBufferDescriptor.computePosition;
import static org.agrona.BitUtil.align;
public class LossDetectorTest
{
private static final int TERM_BUFFER_LENGTH = TERM_MIN_LENGTH;
private static final int POSITION_BITS_TO_SHIFT = Integer.numberOfTrailingZeros(TERM_BUFFER_LENGTH);
private static final int MASK = TERM_BUFFER_LENGTH - 1;
private static final byte[] DATA = new byte[36];
static
{
for (int i = 0; i < DATA.length; i++)
{
DATA[i] = (byte)i;
}
}
private static final int MESSAGE_LENGTH = DataHeaderFlyweight.HEADER_LENGTH + DATA.length;
private static final int ALIGNED_FRAME_LENGTH = align(MESSAGE_LENGTH, FrameDescriptor.FRAME_ALIGNMENT);
private static final int SESSION_ID = 0x5E55101D;
private static final int STREAM_ID = 0xC400E;
private static final int TERM_ID = 0xEE81D;
private static final long ACTIVE_TERM_POSITION = computePosition(TERM_ID, 0, POSITION_BITS_TO_SHIFT, TERM_ID);
private static final StaticDelayGenerator DELAY_GENERATOR = new StaticDelayGenerator(
TimeUnit.MILLISECONDS.toNanos(20), false);
private static final StaticDelayGenerator DELAY_GENERATOR_WITH_IMMEDIATE = new StaticDelayGenerator(
TimeUnit.MILLISECONDS.toNanos(20), true);
private final UnsafeBuffer termBuffer = new UnsafeBuffer(ByteBuffer.allocateDirect(TERM_BUFFER_LENGTH));
private final UnsafeBuffer rcvBuffer = new UnsafeBuffer(ByteBuffer.allocateDirect(MESSAGE_LENGTH));
private final DataHeaderFlyweight dataHeader = new DataHeaderFlyweight();
private LossDetector lossDetector;
private LossHandler lossHandler;
private long currentTime = 0;
public LossDetectorTest()
{
lossHandler = mock(LossHandler.class);
lossDetector = new LossDetector(DELAY_GENERATOR, lossHandler);
dataHeader.wrap(rcvBuffer);
}
@Test
public void shouldNotSendNakWhenBufferIsEmpty()
{
final long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION;
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(100);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verifyZeroInteractions(lossHandler);
}
@Test
public void shouldNotNakIfNoMissingData()
{
final long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(1));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(40);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verifyZeroInteractions(lossHandler);
}
@Test
public void shouldNakMissingData()
{
final long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(40);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verify(lossHandler).onGapDetected(TERM_ID, offsetOfMessage(1), gapLength());
}
@Test
public void shouldRetransmitNakForMissingData()
{
final long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(30);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(60);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verify(lossHandler, atLeast(2)).onGapDetected(TERM_ID, offsetOfMessage(1), gapLength());
}
@Test
public void shouldStopNakOnReceivingData()
{
long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(20);
insertDataFrame(offsetOfMessage(1));
rebuildPosition += (ALIGNED_FRAME_LENGTH * 3);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(100);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verifyZeroInteractions(lossHandler);
}
@Test
public void shouldHandleMoreThan2Gaps()
{
long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 7);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
insertDataFrame(offsetOfMessage(4));
insertDataFrame(offsetOfMessage(6));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(40);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
insertDataFrame(offsetOfMessage(1));
rebuildPosition += (3 * ALIGNED_FRAME_LENGTH);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(80);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
final InOrder inOrder = inOrder(lossHandler);
inOrder.verify(lossHandler, atLeast(1)).onGapDetected(TERM_ID, offsetOfMessage(1), gapLength());
inOrder.verify(lossHandler, atLeast(1)).onGapDetected(TERM_ID, offsetOfMessage(3), gapLength());
inOrder.verify(lossHandler, never()).onGapDetected(TERM_ID, offsetOfMessage(5), gapLength());
}
@Test
public void shouldReplaceOldNakWithNewNak()
{
long rebuildPosition = ACTIVE_TERM_POSITION;
long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(20);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
insertDataFrame(offsetOfMessage(4));
insertDataFrame(offsetOfMessage(1));
rebuildPosition += (ALIGNED_FRAME_LENGTH * 3);
hwmPosition = (ALIGNED_FRAME_LENGTH * 5);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
currentTime = TimeUnit.MILLISECONDS.toNanos(100);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verify(lossHandler, atLeast(1)).onGapDetected(TERM_ID, offsetOfMessage(3), gapLength());
}
@Test
public void shouldHandleImmediateNak()
{
lossDetector = getLossHandlerWithImmediate();
final long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verify(lossHandler).onGapDetected(TERM_ID, offsetOfMessage(1), gapLength());
}
@Test
public void shouldNotNakImmediatelyByDefault()
{
final long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verifyZeroInteractions(lossHandler);
}
@Test
public void shouldOnlySendNaksOnceOnMultipleScans()
{
lossDetector = getLossHandlerWithImmediate();
final long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
insertDataFrame(offsetOfMessage(0));
insertDataFrame(offsetOfMessage(2));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verify(lossHandler).onGapDetected(TERM_ID, offsetOfMessage(1), gapLength());
}
@Test
public void shouldHandleHwmGreaterThanCompletedBuffer()
{
lossDetector = getLossHandlerWithImmediate();
long rebuildPosition = ACTIVE_TERM_POSITION;
final long hwmPosition = ACTIVE_TERM_POSITION + TERM_BUFFER_LENGTH + ALIGNED_FRAME_LENGTH;
insertDataFrame(offsetOfMessage(0));
rebuildPosition += ALIGNED_FRAME_LENGTH;
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verify(lossHandler).onGapDetected(TERM_ID, offsetOfMessage(1), TERM_BUFFER_LENGTH - (int)rebuildPosition);
}
@Test
public void shouldHandleNonZeroInitialTermOffset()
{
lossDetector = getLossHandlerWithImmediate();
final long rebuildPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 3);
final long hwmPosition = ACTIVE_TERM_POSITION + (ALIGNED_FRAME_LENGTH * 5);
insertDataFrame(offsetOfMessage(2));
insertDataFrame(offsetOfMessage(4));
lossDetector.scan(termBuffer, rebuildPosition, hwmPosition, currentTime, MASK, POSITION_BITS_TO_SHIFT, TERM_ID);
verify(lossHandler).onGapDetected(TERM_ID, offsetOfMessage(3), gapLength());
verifyNoMoreInteractions(lossHandler);
}
private LossDetector getLossHandlerWithImmediate()
{
return new LossDetector(DELAY_GENERATOR_WITH_IMMEDIATE, lossHandler);
}
private void insertDataFrame(final int offset)
{
insertDataFrame(offset, DATA);
}
private void insertDataFrame(final int offset, final byte[] payload)
{
dataHeader
.termId(TERM_ID)
.streamId(STREAM_ID)
.sessionId(SESSION_ID)
.termOffset(offset)
.frameLength(payload.length + DataHeaderFlyweight.HEADER_LENGTH)
.headerType(HeaderFlyweight.HDR_TYPE_DATA)
.flags(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)
.version(HeaderFlyweight.CURRENT_VERSION);
rcvBuffer.putBytes(dataHeader.dataOffset(), payload);
TermRebuilder.insert(termBuffer, offset, rcvBuffer, payload.length + DataHeaderFlyweight.HEADER_LENGTH);
}
private int offsetOfMessage(final int index)
{
return index * ALIGNED_FRAME_LENGTH;
}
private int gapLength()
{
return ALIGNED_FRAME_LENGTH;
}
}