/* * 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.archiver; import io.aeron.ExclusivePublication; import io.aeron.logbuffer.*; import io.aeron.protocol.DataHeaderFlyweight; import org.agrona.*; import org.agrona.concurrent.*; import org.junit.*; import org.mockito.Mockito; import java.io.File; import static io.aeron.archiver.TestUtil.makeTempDir; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; public class ReplaySessionTest { private static final String REPLAY_CHANNEL = "aeron:ipc"; private static final int REPLAY_STREAM_ID = 101; private static final int RECORDING_ID = 0; private static final int TERM_BUFFER_LENGTH = 4096 * 4; private static final int INITIAL_TERM_ID = 8231773; private static final int INITIAL_TERM_OFFSET = 1024; private static final long JOINING_POSITION = INITIAL_TERM_OFFSET; private static final long RECORDING_POSITION = INITIAL_TERM_OFFSET; private static final int MTU_LENGTH = 4096; private static final long TIME = 0; private File archiveDir; private int messageIndex = 0; private ControlSessionProxy proxy; private EpochClock epochClock; @Before public void setup() throws Exception { archiveDir = makeTempDir(); proxy = Mockito.mock(ControlSessionProxy.class); epochClock = mock(EpochClock.class); try (Recorder recorder = new Recorder.Builder() .archiveDir(archiveDir) .epochClock(epochClock) .recordingId(RECORDING_ID) .termBufferLength(TERM_BUFFER_LENGTH) .initialTermId(INITIAL_TERM_ID) .joiningPosition(JOINING_POSITION) .mtuLength(MTU_LENGTH) .source("source") .sessionId(1) .channel("channel") .streamId(1) .forceWrites(true) .forceMetadataUpdates(true) .build()) { when(epochClock.time()).thenReturn(TIME); final UnsafeBuffer buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(TERM_BUFFER_LENGTH, 64)); buffer.setMemory(0, TERM_BUFFER_LENGTH, (byte) 0); final DataHeaderFlyweight headerFlyweight = new DataHeaderFlyweight(); headerFlyweight.wrap(buffer, INITIAL_TERM_OFFSET, DataHeaderFlyweight.HEADER_LENGTH); headerFlyweight .termOffset(INITIAL_TERM_OFFSET) .termId(INITIAL_TERM_ID) .headerType(DataHeaderFlyweight.HDR_TYPE_DATA) .frameLength(1024); buffer.setMemory( INITIAL_TERM_OFFSET + DataHeaderFlyweight.HEADER_LENGTH, 1024 - DataHeaderFlyweight.HEADER_LENGTH, (byte) 1); final Header header = new Header(INITIAL_TERM_ID, Integer.numberOfLeadingZeros(TERM_BUFFER_LENGTH)); header.buffer(buffer); header.offset(INITIAL_TERM_OFFSET); assertEquals(1024, header.frameLength()); assertEquals(INITIAL_TERM_ID, header.termId()); assertEquals(INITIAL_TERM_OFFSET, header.offset()); recorder.onFragment( buffer, header.offset() + DataHeaderFlyweight.HEADER_LENGTH, header.frameLength() - DataHeaderFlyweight.HEADER_LENGTH, header); } try (RecordingFragmentReader reader = new RecordingFragmentReader(RECORDING_ID, archiveDir)) { final int polled = reader.controlledPoll( (buffer, offset, length, header) -> { assertEquals(offset, INITIAL_TERM_OFFSET + DataHeaderFlyweight.HEADER_LENGTH); assertEquals(length, 1024 - DataHeaderFlyweight.HEADER_LENGTH); return true; }, 1); assertEquals(1, polled); } } @After public void teardown() { IoUtil.delete(archiveDir, false); } @Test public void shouldReplayDataFromFile() { final long length = 1024L; final long correlationId = 1L; final ExclusivePublication replay = Mockito.mock(ExclusivePublication.class); final ExclusivePublication control = Mockito.mock(ExclusivePublication.class); final ArchiveConductor conductor = Mockito.mock(ArchiveConductor.class); final ReplaySession replaySession = replaySession( RECORDING_ID, length, correlationId, replay, control, conductor); when(control.isClosed()).thenReturn(false); when(control.isConnected()).thenReturn(true); when(replay.isClosed()).thenReturn(false); when(replay.isConnected()).thenReturn(false); replaySession.doWork(); when(replay.isConnected()).thenReturn(true); when(control.isConnected()).thenReturn(true); replaySession.doWork(); // notifies that initiated verify(proxy, times(1)).sendResponse(control, null, correlationId); final UnsafeBuffer mockTermBuffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(4096, 64)); when(replay.tryClaim(anyInt(), any(ExclusiveBufferClaim.class))).then( (invocation) -> { final int claimedSize = invocation.getArgument(0); final ExclusiveBufferClaim buffer = invocation.getArgument(1); buffer.wrap(mockTermBuffer, 0, claimedSize + DataHeaderFlyweight.HEADER_LENGTH); messageIndex++; return (long) claimedSize; }); assertNotEquals(0, replaySession.doWork()); assertTrue(messageIndex > 0); final int expectedFrameLength = 1024; assertEquals(expectedFrameLength, mockTermBuffer.getInt(0)); // TODO: add validation for reserved value and flags assertFalse(replaySession.isDone()); // move clock to finish lingering when(epochClock.time()).thenReturn(ReplaySession.LINGER_LENGTH_MS + TIME + 1L); replaySession.doWork(); assertTrue(replaySession.isDone()); } @Test public void shouldFailToReplayDataForNonExistentStream() { final long length = 1024L; final long correlationId = 1L; final ExclusivePublication replay = Mockito.mock(ExclusivePublication.class); final ExclusivePublication control = Mockito.mock(ExclusivePublication.class); final ArchiveConductor conductor = Mockito.mock(ArchiveConductor.class); final ReplaySession replaySession = replaySession( RECORDING_ID + 1, length, correlationId, replay, control, conductor); // this is a given since they are closed by the session only when(replay.isClosed()).thenReturn(false); when(control.isClosed()).thenReturn(false); when(replay.isConnected()).thenReturn(true); when(control.isConnected()).thenReturn(true); assertEquals(1, replaySession.doWork()); // failure notification verify(proxy, times(1)).sendResponse(eq(control), notNull(), eq(correlationId)); assertTrue(replaySession.isDone()); } @Test public void shouldGiveUpIfPublishersAreNotConnectedAfterOneSecond() { final long length = 1024L; final long correlationId = 1L; final ExclusivePublication replay = Mockito.mock(ExclusivePublication.class); final ExclusivePublication control = Mockito.mock(ExclusivePublication.class); final ArchiveConductor conductor = Mockito.mock(ArchiveConductor.class); final ReplaySession replaySession = replaySession( RECORDING_ID, length, correlationId, replay, control, conductor); when(replay.isClosed()).thenReturn(false); when(control.isClosed()).thenReturn(false); when(replay.isConnected()).thenReturn(false); // does not switch to replay mode until BOTH publications are established replaySession.doWork(); when(epochClock.time()).thenReturn(ReplaySession.LINGER_LENGTH_MS + TIME + 1L); replaySession.doWork(); assertTrue(replaySession.isDone()); } private ReplaySession replaySession( final long recordingId, final long length, final long correlationId, final ExclusivePublication replay, final ExclusivePublication control, final ArchiveConductor conductor) { when(conductor.newReplayPublication( eq(REPLAY_CHANNEL), eq(REPLAY_STREAM_ID), eq(RECORDING_POSITION), eq(MTU_LENGTH), eq(INITIAL_TERM_ID), eq(TERM_BUFFER_LENGTH))) .thenReturn(replay); return new ReplaySession( recordingId, RECORDING_POSITION, length, conductor, control, archiveDir, proxy, 0, correlationId, epochClock, REPLAY_CHANNEL, REPLAY_STREAM_ID); } }