/* * 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.buffer.RawLog; import io.aeron.driver.cmd.NewPublicationCmd; import io.aeron.driver.media.UdpChannel; import io.aeron.driver.status.SystemCounters; import io.aeron.protocol.StatusMessageFlyweight; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.stubbing.Answer; import io.aeron.driver.cmd.SenderCmd; import io.aeron.driver.media.ControlTransportPoller; import io.aeron.driver.media.SendChannelEndpoint; import io.aeron.logbuffer.HeaderWriter; import io.aeron.logbuffer.LogBufferDescriptor; import io.aeron.logbuffer.TermAppender; import io.aeron.protocol.DataHeaderFlyweight; import io.aeron.protocol.HeaderFlyweight; import io.aeron.protocol.SetupFlyweight; import org.agrona.concurrent.OneToOneConcurrentArrayQueue; import org.agrona.concurrent.UnsafeBuffer; import org.agrona.concurrent.status.AtomicCounter; import org.agrona.concurrent.status.AtomicLongPosition; import org.agrona.concurrent.status.Position; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.Queue; import static io.aeron.logbuffer.LogBufferDescriptor.PARTITION_COUNT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.*; import static io.aeron.logbuffer.FrameDescriptor.FRAME_ALIGNMENT; import static org.agrona.BitUtil.align; public class SenderTest { private static final int TERM_BUFFER_LENGTH = LogBufferDescriptor.TERM_MIN_LENGTH; private static final int MAX_FRAME_LENGTH = 1024; private static final int SESSION_ID = 1; private static final int STREAM_ID = 2; private static final int INITIAL_TERM_ID = 3; private static final byte[] PAYLOAD = "Payload is here!".getBytes(); private static final UnsafeBuffer HEADER = DataHeaderFlyweight.createDefaultHeader( SESSION_ID, STREAM_ID, INITIAL_TERM_ID); private static final int FRAME_LENGTH = HEADER.capacity() + PAYLOAD.length; private static final int ALIGNED_FRAME_LENGTH = align(FRAME_LENGTH, FRAME_ALIGNMENT); private final ControlTransportPoller mockTransportPoller = mock(ControlTransportPoller.class); private final RawLog rawLog = LogBufferHelper.newTestLogBuffers(TERM_BUFFER_LENGTH); private TermAppender[] termAppenders; private NetworkPublication publication; private Sender sender; private final FlowControl flowControl = spy(new UnicastFlowControl()); private final RetransmitHandler mockRetransmitHandler = mock(RetransmitHandler.class); private long currentTimestamp = 0; private final Queue<ByteBuffer> receivedFrames = new ArrayDeque<>(); private final UdpChannel udpChannel = UdpChannel.parse("aeron:udp?endpoint=localhost:40123"); private final InetSocketAddress rcvAddress = udpChannel.remoteData(); private final DataHeaderFlyweight dataHeader = new DataHeaderFlyweight(); private final SetupFlyweight setupHeader = new SetupFlyweight(); private final SystemCounters mockSystemCounters = mock(SystemCounters.class); private final OneToOneConcurrentArrayQueue<SenderCmd> senderCommandQueue = new OneToOneConcurrentArrayQueue<>(Configuration.CMD_QUEUE_CAPACITY); private final HeaderWriter headerWriter = new HeaderWriter(HEADER); private Answer<Integer> saveByteBufferAnswer = (invocation) -> { final Object args[] = invocation.getArguments(); final ByteBuffer buffer = (ByteBuffer)args[0]; final int length = buffer.limit() - buffer.position(); receivedFrames.add(ByteBuffer.allocateDirect(length).put(buffer)); // we don't pass on the args, so don't reset buffer.position() back return length; }; @Before public void setUp() throws Exception { final SendChannelEndpoint mockSendChannelEndpoint = mock(SendChannelEndpoint.class); when(mockSendChannelEndpoint.udpChannel()).thenReturn(udpChannel); when(mockSendChannelEndpoint.send(any())).thenAnswer(saveByteBufferAnswer); when(mockSystemCounters.get(any())).thenReturn(mock(AtomicCounter.class)); sender = new Sender( new MediaDriver.Context() .controlTransportPoller(mockTransportPoller) .systemCounters(mockSystemCounters) .senderCommandQueue(senderCommandQueue) .nanoClock(() -> currentTimestamp)); LogBufferDescriptor.initialiseTailWithTermId(rawLog.metaData(), 0, INITIAL_TERM_ID); termAppenders = new TermAppender[PARTITION_COUNT]; for (int i = 0; i < PARTITION_COUNT; i++) { termAppenders[i] = new TermAppender(rawLog.termBuffers()[i], rawLog.metaData(), i); } publication = new NetworkPublication( 1, mockSendChannelEndpoint, () -> currentTimestamp, () -> currentTimestamp / (1_000_000L), rawLog, mock(Position.class), new AtomicLongPosition(), new AtomicLongPosition(), SESSION_ID, STREAM_ID, INITIAL_TERM_ID, MAX_FRAME_LENGTH, mockSystemCounters, flowControl, mockRetransmitHandler, new NetworkPublicationThreadLocals(), Configuration.PUBLICATION_UNBLOCK_TIMEOUT_NS, false); senderCommandQueue.offer(new NewPublicationCmd(publication)); } @After public void tearDown() throws Exception { sender.onClose(); } @Test public void shouldSendSetupFrameOnChannelWhenTimeoutWithoutStatusMessage() throws Exception { sender.doWork(); assertThat(receivedFrames.size(), is(1)); currentTimestamp += Configuration.PUBLICATION_SETUP_TIMEOUT_NS - 1; sender.doWork(); assertThat(receivedFrames.size(), is(1)); currentTimestamp += 10; sender.doWork(); assertThat(receivedFrames.size(), is(2)); setupHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(setupHeader.frameLength(), is(SetupFlyweight.HEADER_LENGTH)); assertThat(setupHeader.initialTermId(), is(INITIAL_TERM_ID)); assertThat(setupHeader.activeTermId(), is(INITIAL_TERM_ID)); assertThat(setupHeader.streamId(), is(STREAM_ID)); assertThat(setupHeader.sessionId(), is(SESSION_ID)); assertThat(setupHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_SETUP)); assertThat(setupHeader.flags(), is((short)0)); assertThat(setupHeader.version(), is((short)HeaderFlyweight.CURRENT_VERSION)); } @Test public void shouldSendMultipleSetupFramesOnChannelWhenTimeoutWithoutStatusMessage() throws Exception { sender.doWork(); assertThat(receivedFrames.size(), is(1)); currentTimestamp += Configuration.PUBLICATION_SETUP_TIMEOUT_NS - 1; sender.doWork(); currentTimestamp += 10; sender.doWork(); assertThat(receivedFrames.size(), is(2)); } @Test public void shouldNotSendSetupFrameAfterReceivingStatusMessage() throws Exception { final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(0); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit(flowControl.onStatusMessage(msg, rcvAddress, )); sender.doWork(); assertThat(receivedFrames.size(), is(1)); receivedFrames.remove(); currentTimestamp += Configuration.PUBLICATION_SETUP_TIMEOUT_NS + 10; sender.doWork(); assertThat(receivedFrames.size(), is(1)); dataHeader.wrap(receivedFrames.remove()); assertThat(dataHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_DATA)); // heartbeat assertThat(dataHeader.frameLength(), is(0)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(1))); } @Test public void shouldSendSetupFrameAfterReceivingStatusMessageWithSetupBit() throws Exception { final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(ALIGNED_FRAME_LENGTH); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit( // flowControl.onStatusMessage(INITIAL_TERM_ID, 0, ALIGNED_FRAME_LENGTH, rcvAddress)); final UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(PAYLOAD.length)); buffer.putBytes(0, PAYLOAD); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); assertThat(receivedFrames.size(), is(2)); // setup then data receivedFrames.remove(); receivedFrames.remove(); publication.triggerSendSetupFrame(); sender.doWork(); assertThat(receivedFrames.size(), is(0)); // setup has been sent already, have to wait currentTimestamp += Configuration.PUBLICATION_SETUP_TIMEOUT_NS + 10; sender.doWork(); assertThat(receivedFrames.size(), is(1)); setupHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(setupHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_SETUP)); } @Test public void shouldBeAbleToSendOnChannel() throws Exception { final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(ALIGNED_FRAME_LENGTH); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit( // flowControl.onStatusMessage(INITIAL_TERM_ID, 0, ALIGNED_FRAME_LENGTH, rcvAddress)); final UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(PAYLOAD.length)); buffer.putBytes(0, PAYLOAD); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); assertThat(receivedFrames.size(), is(2)); setupHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(setupHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_SETUP)); dataHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(dataHeader.frameLength(), is(FRAME_LENGTH)); assertThat(dataHeader.termId(), is(INITIAL_TERM_ID)); assertThat(dataHeader.streamId(), is(STREAM_ID)); assertThat(dataHeader.sessionId(), is(SESSION_ID)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(1))); assertThat(dataHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_DATA)); assertThat(dataHeader.flags(), is(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)); assertThat(dataHeader.version(), is((short)HeaderFlyweight.CURRENT_VERSION)); } @Test public void shouldBeAbleToSendOnChannelTwice() throws Exception { final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(2 * ALIGNED_FRAME_LENGTH); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit( // flowControl.onStatusMessage(INITIAL_TERM_ID, 0, (2 * ALIGNED_FRAME_LENGTH), rcvAddress)); final UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(PAYLOAD.length)); buffer.putBytes(0, PAYLOAD); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); assertThat(receivedFrames.size(), is(3)); setupHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(setupHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_SETUP)); dataHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(dataHeader.frameLength(), is(FRAME_LENGTH)); assertThat(dataHeader.termId(), is(INITIAL_TERM_ID)); assertThat(dataHeader.streamId(), is(STREAM_ID)); assertThat(dataHeader.sessionId(), is(SESSION_ID)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(1))); assertThat(dataHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_DATA)); assertThat(dataHeader.flags(), is(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)); assertThat(dataHeader.version(), is((short)HeaderFlyweight.CURRENT_VERSION)); dataHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(dataHeader.frameLength(), is(FRAME_LENGTH)); assertThat(dataHeader.termId(), is(INITIAL_TERM_ID)); assertThat(dataHeader.streamId(), is(STREAM_ID)); assertThat(dataHeader.sessionId(), is(SESSION_ID)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(2))); assertThat(dataHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_DATA)); assertThat(dataHeader.flags(), is(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)); assertThat(dataHeader.version(), is((short)HeaderFlyweight.CURRENT_VERSION)); } @Test public void shouldNotSendUntilStatusMessageReceived() throws Exception { final UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(PAYLOAD.length)); buffer.putBytes(0, PAYLOAD); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); assertThat(receivedFrames.size(), is(1)); setupHeader.wrap(receivedFrames.remove()); assertThat(setupHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_SETUP)); final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(ALIGNED_FRAME_LENGTH); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit( // flowControl.onStatusMessage(INITIAL_TERM_ID, 0, ALIGNED_FRAME_LENGTH, rcvAddress)); sender.doWork(); assertThat(receivedFrames.size(), is(1)); dataHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(dataHeader.frameLength(), is(FRAME_LENGTH)); assertThat(dataHeader.termId(), is(INITIAL_TERM_ID)); assertThat(dataHeader.streamId(), is(STREAM_ID)); assertThat(dataHeader.sessionId(), is(SESSION_ID)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(1))); assertThat(dataHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_DATA)); assertThat(dataHeader.flags(), is(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)); assertThat(dataHeader.version(), is((short)HeaderFlyweight.CURRENT_VERSION)); } @Test public void shouldNotBeAbleToSendAfterUsingUpYourWindow() throws Exception { final UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(PAYLOAD.length)); buffer.putBytes(0, PAYLOAD); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(ALIGNED_FRAME_LENGTH); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit( // flowControl.onStatusMessage(INITIAL_TERM_ID, 0, ALIGNED_FRAME_LENGTH, rcvAddress)); sender.doWork(); assertThat(receivedFrames.size(), is(2)); receivedFrames.remove(); // skip setup dataHeader.wrap(new UnsafeBuffer(receivedFrames.remove())); assertThat(dataHeader.frameLength(), is(FRAME_LENGTH)); assertThat(dataHeader.termId(), is(INITIAL_TERM_ID)); assertThat(dataHeader.streamId(), is(STREAM_ID)); assertThat(dataHeader.sessionId(), is(SESSION_ID)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(1))); assertThat(dataHeader.headerType(), is(HeaderFlyweight.HDR_TYPE_DATA)); assertThat(dataHeader.flags(), is(DataHeaderFlyweight.BEGIN_AND_END_FLAGS)); assertThat(dataHeader.version(), is((short)HeaderFlyweight.CURRENT_VERSION)); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); assertThat(receivedFrames.size(), is(0)); } @Test public void shouldSendLastDataFrameAsHeartbeatWhenIdle() throws Exception { final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(ALIGNED_FRAME_LENGTH); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit( // flowControl.onStatusMessage(INITIAL_TERM_ID, 0, ALIGNED_FRAME_LENGTH, rcvAddress)); final UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(PAYLOAD.length)); buffer.putBytes(0, PAYLOAD); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); assertThat(receivedFrames.size(), is(2)); // should send ticks receivedFrames.remove(); // skip setup & data frame receivedFrames.remove(); currentTimestamp += Configuration.PUBLICATION_HEARTBEAT_TIMEOUT_NS - 1; sender.doWork(); assertThat(receivedFrames.size(), is(0)); // should not send yet currentTimestamp += 10; sender.doWork(); assertThat(receivedFrames.size(), greaterThanOrEqualTo(1)); // should send ticks dataHeader.wrap(receivedFrames.remove()); assertThat(dataHeader.frameLength(), is(0)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(2))); } @Test public void shouldSendMultipleDataFramesAsHeartbeatsWhenIdle() { final StatusMessageFlyweight msg = mock(StatusMessageFlyweight.class); when(msg.consumptionTermId()).thenReturn(INITIAL_TERM_ID); when(msg.consumptionTermOffset()).thenReturn(0); when(msg.receiverWindowLength()).thenReturn(ALIGNED_FRAME_LENGTH); publication.onStatusMessage(msg, rcvAddress); // publication.senderPositionLimit( // flowControl.onStatusMessage(INITIAL_TERM_ID, 0, ALIGNED_FRAME_LENGTH, rcvAddress)); final UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(PAYLOAD.length)); buffer.putBytes(0, PAYLOAD); termAppenders[0].appendUnfragmentedMessage(headerWriter, buffer, 0, PAYLOAD.length, null); sender.doWork(); assertThat(receivedFrames.size(), is(2)); // should send ticks receivedFrames.remove(); receivedFrames.remove(); // skip setup & data frame currentTimestamp += Configuration.PUBLICATION_HEARTBEAT_TIMEOUT_NS - 1; sender.doWork(); assertThat(receivedFrames.size(), is(0)); // should not send yet currentTimestamp += 10; sender.doWork(); assertThat(receivedFrames.size(), greaterThanOrEqualTo(1)); // should send ticks dataHeader.wrap(receivedFrames.remove()); assertThat(dataHeader.frameLength(), is(0)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(2))); currentTimestamp += Configuration.PUBLICATION_HEARTBEAT_TIMEOUT_NS - 1; sender.doWork(); assertThat(receivedFrames.size(), is(0)); // should not send yet currentTimestamp += 10; sender.doWork(); assertThat(receivedFrames.size(), greaterThanOrEqualTo(1)); // should send ticks dataHeader.wrap(receivedFrames.remove()); assertThat(dataHeader.frameLength(), is(0)); assertThat(dataHeader.termOffset(), is(offsetOfMessage(2))); } private int offsetOfMessage(final int offset) { return (offset - 1) * align(HEADER.capacity() + PAYLOAD.length, FRAME_ALIGNMENT); } }