/* * 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.*; import io.aeron.archiver.codecs.RecordingDescriptorDecoder; import io.aeron.logbuffer.RawBlockHandler; import io.aeron.protocol.DataHeaderFlyweight; import org.agrona.*; import org.agrona.concurrent.*; import org.junit.*; import org.mockito.Mockito; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import static io.aeron.archiver.ArchiveUtil.recordingMetaFileName; import static java.nio.file.StandardOpenOption.*; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class RecordingSessionTest { private static final int SEGMENT_FILE_SIZE = 128 * 1024 * 1024; private final long recordingId = 12345; private final String channel = "channel"; private final String source = "sourceIdentity"; private final int streamId = 54321; private final int sessionId = 12345; private final int initialTermId = 0; private final int termBufferLength = 4096; private final int termOffset = 1024; private final int mtuLength = 1024; private final long joiningPosition = termOffset; private final File tempDirForTest = TestUtil.makeTempDir(); private final NotificationsProxy proxy; private final Image image; private final Catalog index; private FileChannel mockLogBufferChannel; private UnsafeBuffer mockLogBufferMapped; private File termFile; public RecordingSessionTest() throws IOException { proxy = mock(NotificationsProxy.class); index = mock(Catalog.class); when( index.addNewRecording( eq(source), eq(sessionId), eq(channel), eq(streamId), eq(termBufferLength), eq(mtuLength), eq(initialTermId), eq(joiningPosition), any(RecordingSession.class), eq(SEGMENT_FILE_SIZE))) .thenReturn(recordingId); final Subscription subscription = mockSubscription(channel, streamId); image = mockImage(source, sessionId, initialTermId, termBufferLength, subscription); } @Before public void setupMockTermBuff() throws IOException { termFile = File.createTempFile("test.rec", "source"); // size this file as a mock term buffer mockLogBufferChannel = FileChannel.open(termFile.toPath(), CREATE, READ, WRITE); mockLogBufferChannel.position(termBufferLength - 1); mockLogBufferChannel.write(ByteBuffer.wrap(new byte[1])); // write some data at term offset final ByteBuffer bb = ByteBuffer.allocate(100); mockLogBufferChannel.position(termOffset); mockLogBufferChannel.write(bb); mockLogBufferMapped = new UnsafeBuffer( mockLogBufferChannel.map(FileChannel.MapMode.READ_WRITE, 0, termBufferLength)); // prep a single message in the log buffer final DataHeaderFlyweight headerFlyweight = new DataHeaderFlyweight(); headerFlyweight.wrap(mockLogBufferMapped); headerFlyweight.headerType(DataHeaderFlyweight.HDR_TYPE_DATA).frameLength(100); } @After public void teardownMockTermBuff() { IoUtil.unmap(mockLogBufferMapped.byteBuffer()); CloseHelper.close(mockLogBufferChannel); IoUtil.delete(tempDirForTest, false); IoUtil.delete(termFile, false); } @Test public void shouldRecordFragmentsFromImage() throws Exception { final EpochClock epochClock = Mockito.mock(EpochClock.class); when(epochClock.time()).thenReturn(42L); final Recorder.Builder builder = new Recorder.Builder() .recordingFileLength(SEGMENT_FILE_SIZE) .archiveDir(tempDirForTest) .epochClock(epochClock); final RecordingSession session = new RecordingSession(proxy, index, image, builder); // pre-init assertEquals(Catalog.NULL_INDEX, session.recordingId()); session.doWork(); assertEquals(recordingId, session.recordingId()); // setup the mock image to pass on the mock log buffer when(image.rawPoll(any(), anyInt())).thenAnswer( (invocation) -> { final RawBlockHandler handle = invocation.getArgument(0); if (handle == null) { return 0; } handle.onBlock( mockLogBufferChannel, 0, mockLogBufferMapped, termOffset, 100, sessionId, 0); return 100; }); // expecting session to proxy the available data from the image assertNotEquals("Expect some work", 0, session.doWork()); // We now evaluate the output of the archiver... // meta data exists and is as expected final File recordingMetaFile = new File(tempDirForTest, recordingMetaFileName(session.recordingId())); assertTrue(recordingMetaFile.exists()); RecordingDescriptorDecoder metaData = ArchiveUtil.recordingMetaFileFormatDecoder(recordingMetaFile); assertEquals(recordingId, metaData.recordingId()); assertEquals(termBufferLength, metaData.termBufferLength()); assertEquals(streamId, metaData.streamId()); assertEquals(42L, metaData.startTime()); assertEquals(-1L, metaData.endTime()); assertEquals(source, metaData.source()); assertEquals(channel, metaData.channel()); // data exists and is as expected final File segmentFile = new File(tempDirForTest, ArchiveUtil.recordingDataFileName(recordingId, 0)); assertTrue(segmentFile.exists()); try (RecordingFragmentReader reader = new RecordingFragmentReader(session.recordingId(), tempDirForTest)) { final int polled = reader.controlledPoll( (buffer, offset, length, header) -> { assertEquals(100, header.frameLength()); assertEquals(termOffset + DataHeaderFlyweight.HEADER_LENGTH, offset); assertEquals(100 - DataHeaderFlyweight.HEADER_LENGTH, length); return true; }, 1); assertEquals(1, polled); } // next poll has no data when(image.rawPoll(any(), anyInt())).thenReturn(0); assertEquals("Expect no work", 0, session.doWork()); // image is closed when(image.isClosed()).thenReturn(true); when(epochClock.time()).thenReturn(128L); assertNotEquals("Expect some work", 0, session.doWork()); assertTrue(session.isDone()); metaData = ArchiveUtil.recordingMetaFileFormatDecoder(recordingMetaFile); assertEquals(128L, metaData.endTime()); IoUtil.unmap(metaData.buffer().byteBuffer()); } private Subscription mockSubscription(final String channel, final int streamId) { final Subscription subscription = mock(Subscription.class); when(subscription.channel()).thenReturn(channel); when(subscription.streamId()).thenReturn(streamId); return subscription; } private Image mockImage( final String sourceIdentity, final int sessionId, final int initialTermId, final int termBufferLength, final Subscription subscription) { final Image image = mock(Image.class); when(image.sessionId()).thenReturn(sessionId); when(image.sourceIdentity()).thenReturn(sourceIdentity); when(image.subscription()).thenReturn(subscription); when(image.initialTermId()).thenReturn(initialTermId); when(image.termBufferLength()).thenReturn(termBufferLength); when(image.mtuLength()).thenReturn(mtuLength); when(image.joiningPosition()).thenReturn(joiningPosition); return image; } }