/*
* 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 org.agrona.concurrent.ringbuffer;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.agrona.concurrent.MessageHandler;
import org.agrona.concurrent.UnsafeBuffer;
import static java.lang.Boolean.TRUE;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.agrona.BitUtil.align;
import static org.agrona.concurrent.ringbuffer.ManyToOneRingBuffer.PADDING_MSG_TYPE_ID;
import static org.agrona.concurrent.ringbuffer.RecordDescriptor.*;
public class ManyToOneRingBufferTest
{
private static final int MSG_TYPE_ID = 7;
private static final int CAPACITY = 4096;
private static final int TOTAL_BUFFER_LENGTH = CAPACITY + RingBufferDescriptor.TRAILER_LENGTH;
private static final int TAIL_COUNTER_INDEX = CAPACITY + RingBufferDescriptor.TAIL_POSITION_OFFSET;
private static final int HEAD_COUNTER_INDEX = CAPACITY + RingBufferDescriptor.HEAD_POSITION_OFFSET;
private static final int HEAD_COUNTER_CACHE_INDEX = CAPACITY + RingBufferDescriptor.HEAD_CACHE_POSITION_OFFSET;
private final UnsafeBuffer buffer = mock(UnsafeBuffer.class);
private ManyToOneRingBuffer ringBuffer;
@Before
public void setUp()
{
when(buffer.capacity()).thenReturn(TOTAL_BUFFER_LENGTH);
ringBuffer = new ManyToOneRingBuffer(buffer);
}
@Test
public void shouldWriteToEmptyBuffer()
{
final int length = 8;
final int recordLength = length + HEADER_LENGTH;
final int alignedRecordLength = align(recordLength, ALIGNMENT);
final long tail = 0L;
final long head = 0L;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn(tail);
when(buffer.compareAndSetLong(TAIL_COUNTER_INDEX, tail, tail + alignedRecordLength))
.thenReturn(TRUE);
final UnsafeBuffer srcBuffer = new UnsafeBuffer(new byte[1024]);
final int srcIndex = 0;
assertTrue(ringBuffer.write(MSG_TYPE_ID, srcBuffer, srcIndex, length));
final InOrder inOrder = inOrder(buffer);
inOrder.verify(buffer).putLongOrdered((int)tail, makeHeader(-recordLength, MSG_TYPE_ID));
inOrder.verify(buffer).putBytes(encodedMsgOffset((int)tail), srcBuffer, srcIndex, length);
inOrder.verify(buffer).putIntOrdered(lengthOffset((int)tail), recordLength);
}
@Test
public void shouldRejectWriteWhenInsufficientSpace()
{
final int length = 200;
final long head = 0L;
final long tail = head + (CAPACITY - align(length - ALIGNMENT, ALIGNMENT));
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn(tail);
final UnsafeBuffer srcBuffer = new UnsafeBuffer(new byte[1024]);
final int srcIndex = 0;
assertFalse(ringBuffer.write(MSG_TYPE_ID, srcBuffer, srcIndex, length));
verify(buffer, never()).putInt(anyInt(), anyInt());
verify(buffer, never()).compareAndSetLong(anyInt(), anyLong(), anyLong());
verify(buffer, never()).putBytes(anyInt(), eq(srcBuffer), anyInt(), anyInt());
verify(buffer, never()).putIntOrdered(anyInt(), anyInt());
}
@Test
public void shouldRejectWriteWhenBufferFull()
{
final int length = 8;
final long head = 0L;
final long tail = head + CAPACITY;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn(tail);
final UnsafeBuffer srcBuffer = new UnsafeBuffer(new byte[1024]);
final int srcIndex = 0;
assertFalse(ringBuffer.write(MSG_TYPE_ID, srcBuffer, srcIndex, length));
verify(buffer, never()).putInt(anyInt(), anyInt());
verify(buffer, never()).compareAndSetLong(anyInt(), anyLong(), anyLong());
verify(buffer, never()).putIntOrdered(anyInt(), anyInt());
}
@Test
public void shouldInsertPaddingRecordPlusMessageOnBufferWrap()
{
final int length = 200;
final int recordLength = length + HEADER_LENGTH;
final int alignedRecordLength = align(recordLength, ALIGNMENT);
final long tail = CAPACITY - HEADER_LENGTH;
final long head = tail - (ALIGNMENT * 4);
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn(tail);
when(buffer.compareAndSetLong(TAIL_COUNTER_INDEX, tail, tail + alignedRecordLength + ALIGNMENT))
.thenReturn(TRUE);
final UnsafeBuffer srcBuffer = new UnsafeBuffer(new byte[1024]);
final int srcIndex = 0;
assertTrue(ringBuffer.write(MSG_TYPE_ID, srcBuffer, srcIndex, length));
final InOrder inOrder = inOrder(buffer);
inOrder.verify(buffer).putLongOrdered((int)tail, makeHeader(HEADER_LENGTH, PADDING_MSG_TYPE_ID));
inOrder.verify(buffer).putLongOrdered(0, makeHeader(-recordLength, MSG_TYPE_ID));
inOrder.verify(buffer).putBytes(encodedMsgOffset(0), srcBuffer, srcIndex, length);
inOrder.verify(buffer).putIntOrdered(lengthOffset(0), recordLength);
}
@Test
public void shouldInsertPaddingRecordPlusMessageOnBufferWrapWithHeadEqualToTail()
{
final int length = 200;
final int recordLength = length + HEADER_LENGTH;
final int alignedRecordLength = align(recordLength, ALIGNMENT);
final long tail = CAPACITY - HEADER_LENGTH;
final long head = tail;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn(tail);
when(buffer.compareAndSetLong(TAIL_COUNTER_INDEX, tail, tail + alignedRecordLength + ALIGNMENT))
.thenReturn(TRUE);
final UnsafeBuffer srcBuffer = new UnsafeBuffer(new byte[1024]);
final int srcIndex = 0;
assertTrue(ringBuffer.write(MSG_TYPE_ID, srcBuffer, srcIndex, length));
final InOrder inOrder = inOrder(buffer);
inOrder.verify(buffer).putLongOrdered((int)tail, makeHeader(HEADER_LENGTH, PADDING_MSG_TYPE_ID));
inOrder.verify(buffer).putLongOrdered(0, makeHeader(-recordLength, MSG_TYPE_ID));
inOrder.verify(buffer).putBytes(encodedMsgOffset(0), srcBuffer, srcIndex, length);
inOrder.verify(buffer).putIntOrdered(lengthOffset(0), recordLength);
}
@Test
public void shouldReadNothingFromEmptyBuffer()
{
final long head = 0L;
when(buffer.getLong(HEAD_COUNTER_INDEX)).thenReturn(head);
final MessageHandler handler = (msgTypeId, buffer, index, length) -> fail("should not be called");
final int messagesRead = ringBuffer.read(handler);
assertThat(messagesRead, is(0));
}
@Test
public void shouldNotReadSingleMessagePartWayThroughWriting()
{
final long head = 0L;
final int headIndex = (int)head;
when(buffer.getLong(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getIntVolatile(lengthOffset(headIndex))).thenReturn(0);
final int[] times = new int[1];
final MessageHandler handler = (msgTypeId, buffer, index, length) -> times[0]++;
final int messagesRead = ringBuffer.read(handler);
assertThat(messagesRead, is(0));
assertThat(times[0], is(0));
final InOrder inOrder = inOrder(buffer);
inOrder.verify(buffer, times(1)).getLongVolatile(headIndex);
inOrder.verify(buffer, times(0)).setMemory(headIndex, 0, (byte)0);
inOrder.verify(buffer, times(0)).putLongOrdered(HEAD_COUNTER_INDEX, headIndex);
}
@Test
public void shouldReadTwoMessages()
{
final int msgLength = 16;
final int recordLength = HEADER_LENGTH + msgLength;
final int alignedRecordLength = align(recordLength, ALIGNMENT);
final long tail = alignedRecordLength * 2;
final long head = 0L;
final int headIndex = (int)head;
when(buffer.getLong(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(headIndex)).thenReturn(makeHeader(recordLength, MSG_TYPE_ID));
when(buffer.getLongVolatile(headIndex + alignedRecordLength))
.thenReturn(makeHeader(recordLength, MSG_TYPE_ID));
final int[] times = new int[1];
final MessageHandler handler = (msgTypeId, buffer, index, length) -> times[0]++;
final int messagesRead = ringBuffer.read(handler);
assertThat(messagesRead, is(2));
assertThat(times[0], is(2));
final InOrder inOrder = inOrder(buffer);
inOrder.verify(buffer, times(1)).setMemory(headIndex, alignedRecordLength * 2, (byte)0);
inOrder.verify(buffer, times(1)).putLongOrdered(HEAD_COUNTER_INDEX, tail);
}
@Test
public void shouldLimitReadOfMessages()
{
final int msgLength = 16;
final int recordLength = HEADER_LENGTH + msgLength;
final int alignedRecordLength = align(recordLength, ALIGNMENT);
final long head = 0L;
final int headIndex = (int)head;
when(buffer.getLong(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(headIndex)).thenReturn(makeHeader(recordLength, MSG_TYPE_ID));
final int[] times = new int[1];
final MessageHandler handler = (msgTypeId, buffer, index, length) -> times[0]++;
final int limit = 1;
final int messagesRead = ringBuffer.read(handler, limit);
assertThat(messagesRead, is(1));
assertThat(times[0], is(1));
final InOrder inOrder = inOrder(buffer);
inOrder.verify(buffer, times(1)).setMemory(headIndex, alignedRecordLength, (byte)0);
inOrder.verify(buffer, times(1)).putLongOrdered(HEAD_COUNTER_INDEX, head + alignedRecordLength);
}
@Test
public void shouldCopeWithExceptionFromHandler()
{
final int msgLength = 16;
final int recordLength = HEADER_LENGTH + msgLength;
final int alignedRecordLength = align(recordLength, ALIGNMENT);
final long tail = alignedRecordLength * 2;
final long head = 0L;
final int headIndex = (int)head;
when(buffer.getLong(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(headIndex)).thenReturn(makeHeader(recordLength, MSG_TYPE_ID));
when(buffer.getLongVolatile(headIndex + alignedRecordLength)).thenReturn(makeHeader(recordLength, MSG_TYPE_ID));
final int[] times = new int[1];
final MessageHandler handler =
(msgTypeId, buffer, index, length) ->
{
times[0]++;
if (times[0] == 2)
{
throw new RuntimeException();
}
};
try
{
ringBuffer.read(handler);
}
catch (final RuntimeException ignore)
{
assertThat(times[0], is(2));
final InOrder inOrder = inOrder(buffer);
inOrder.verify(buffer, times(1)).setMemory(headIndex, alignedRecordLength * 2, (byte)0);
inOrder.verify(buffer, times(1)).putLongOrdered(HEAD_COUNTER_INDEX, tail);
return;
}
fail("Should have thrown exception");
}
@Test
public void shouldNotUnblockWhenEmpty()
{
final long position = ALIGNMENT * 4;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn(position);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn(position);
assertFalse(ringBuffer.unblock());
}
@Test
public void shouldUnblockMessageWithHeader()
{
final int messageLength = ALIGNMENT * 4;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn((long)messageLength);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn((long)messageLength * 2);
when(buffer.getIntVolatile(messageLength)).thenReturn(-messageLength);
assertTrue(ringBuffer.unblock());
verify(buffer).putLongOrdered(messageLength, makeHeader(messageLength, PADDING_MSG_TYPE_ID));
}
@Test
public void shouldUnblockGapWithZeros()
{
final int messageLength = ALIGNMENT * 4;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn((long)messageLength);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn((long)messageLength * 3);
when(buffer.getIntVolatile(messageLength * 2)).thenReturn(messageLength);
assertTrue(ringBuffer.unblock());
verify(buffer).putLongOrdered(messageLength, makeHeader(messageLength, PADDING_MSG_TYPE_ID));
}
@Test
public void shouldNotUnblockGapWithMessageRaceOnSecondMessageIncreasingTailThenInterrupting()
{
final int messageLength = ALIGNMENT * 4;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn((long)messageLength);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn((long)messageLength * 3);
when(buffer.getIntVolatile(messageLength * 2)).thenReturn(0).thenReturn(messageLength);
assertFalse(ringBuffer.unblock());
verify(buffer, never()).putLongOrdered(messageLength, makeHeader(messageLength, PADDING_MSG_TYPE_ID));
}
@Test
public void shouldNotUnblockGapWithMessageRaceWhenScanForwardTakesAnInterrupt()
{
final int messageLength = ALIGNMENT * 4;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn((long)messageLength);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn((long)messageLength * 3);
when(buffer.getIntVolatile(messageLength * 2)).thenReturn(0).thenReturn(messageLength);
when(buffer.getIntVolatile(messageLength * 2 + ALIGNMENT)).thenReturn(7);
assertFalse(ringBuffer.unblock());
verify(buffer, never()).putLongOrdered(messageLength, makeHeader(messageLength, PADDING_MSG_TYPE_ID));
}
@Test
public void shouldCalculateCapacityForBuffer()
{
assertThat(ringBuffer.capacity(), is(CAPACITY));
}
@Test(expected = IllegalStateException.class)
public void shouldThrowExceptionForCapacityThatIsNotPowerOfTwo()
{
final int capacity = 777;
final int totalBufferLength = capacity + RingBufferDescriptor.TRAILER_LENGTH;
new ManyToOneRingBuffer(new UnsafeBuffer(new byte[totalBufferLength]));
}
@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionWhenMaxMessageSizeExceeded()
{
final UnsafeBuffer srcBuffer = new UnsafeBuffer(new byte[1024]);
ringBuffer.write(MSG_TYPE_ID, srcBuffer, 0, ringBuffer.maxMsgLength() + 1);
}
@Test
public void shouldInsertPaddingAndWriteToBuffer()
{
final int padding = 200;
final int messageLength = 400;
final int recordLength = messageLength + HEADER_LENGTH;
final int alignedRecordLength = align(recordLength, ALIGNMENT);
final long tail = 2 * CAPACITY - padding;
final long head = tail;
// free space is (200 + 300) more than message length (400)
// but contiguous space (300) is less than message length (400)
final long headCache = CAPACITY + 300;
when(buffer.getLongVolatile(HEAD_COUNTER_INDEX)).thenReturn(head);
when(buffer.getLongVolatile(TAIL_COUNTER_INDEX)).thenReturn(tail);
when(buffer.getLongVolatile(HEAD_COUNTER_CACHE_INDEX)).thenReturn(headCache);
when(buffer.compareAndSetLong(TAIL_COUNTER_INDEX, tail, tail + alignedRecordLength + padding))
.thenReturn(true);
final UnsafeBuffer srcBuffer = new UnsafeBuffer(new byte[messageLength]);
assertTrue(ringBuffer.write(MSG_TYPE_ID, srcBuffer, 0, messageLength));
}
}