/* * Copyright 2014-2020 Real Logic Limited. * * 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 * * https://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.archive; import io.aeron.logbuffer.FrameDescriptor; import io.aeron.logbuffer.LogBufferDescriptor; import io.aeron.protocol.DataHeaderFlyweight; import org.agrona.BitUtil; import org.agrona.IoUtil; import org.agrona.LangUtil; import org.agrona.concurrent.UnsafeBuffer; import java.io.File; import java.io.IOException; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileAttribute; import java.util.EnumSet; import static io.aeron.archive.Archive.segmentFileName; import static io.aeron.archive.client.AeronArchive.*; import static io.aeron.logbuffer.FrameDescriptor.FRAME_ALIGNMENT; import static io.aeron.protocol.DataHeaderFlyweight.RESERVED_VALUE_OFFSET; import static java.nio.ByteOrder.LITTLE_ENDIAN; import static java.nio.channels.FileChannel.MapMode.READ_ONLY; import static java.nio.file.StandardOpenOption.READ; class RecordingReader implements AutoCloseable { private static final EnumSet<StandardOpenOption> FILE_OPTIONS = EnumSet.of(READ); private static final FileAttribute<?>[] NO_ATTRIBUTES = new FileAttribute[0]; private final File archiveDir; private final long recordingId; private final int segmentLength; private final int termLength; private final UnsafeBuffer termBuffer; private MappedByteBuffer mappedSegmentBuffer; private final long replayLimit; private long replayPosition; private long segmentFilePosition; private int termOffset; private int termBaseSegmentOffset; private boolean isDone = false; RecordingReader( final RecordingSummary recordingSummary, final File archiveDir, final long position, final long length) { if (position < NULL_POSITION) { throw new IllegalArgumentException("invalid position: " + position); } if (length < NULL_LENGTH) { throw new IllegalArgumentException("invalid length: " + length); } this.archiveDir = archiveDir; this.termLength = recordingSummary.termBufferLength; this.segmentLength = recordingSummary.segmentFileLength; this.recordingId = recordingSummary.recordingId; final long startPosition = recordingSummary.startPosition; final long fromPosition = position == NULL_POSITION ? startPosition : position; final long maxLength = recordingSummary.stopPosition != NULL_POSITION ? recordingSummary.stopPosition - fromPosition : Long.MAX_VALUE - fromPosition; final long replayLength = length == NULL_LENGTH ? maxLength : Math.min(length, maxLength); if (replayLength < 0) { throw new IllegalArgumentException("length must be positive"); } final int positionBitsToShift = LogBufferDescriptor.positionBitsToShift(termLength); final long startTermBasePosition = startPosition - (startPosition & (termLength - 1)); final int segmentOffset = (int)(fromPosition - startTermBasePosition) & (segmentLength - 1); final int termId = ((int)(fromPosition >> positionBitsToShift) + recordingSummary.initialTermId); segmentFilePosition = segmentFileBasePosition(startPosition, fromPosition, termLength, segmentLength); openRecordingSegment(); termOffset = (int)(fromPosition & (termLength - 1)); termBaseSegmentOffset = segmentOffset - termOffset; termBuffer = new UnsafeBuffer(mappedSegmentBuffer, termBaseSegmentOffset, termLength); if (fromPosition > startPosition && (DataHeaderFlyweight.termOffset(termBuffer, termOffset) != termOffset || DataHeaderFlyweight.termId(termBuffer, termOffset) != termId || DataHeaderFlyweight.streamId(termBuffer, termOffset) != recordingSummary.streamId)) { close(); throw new IllegalArgumentException(fromPosition + " position not aligned to valid fragment"); } replayPosition = fromPosition; replayLimit = fromPosition + replayLength; } public void close() { closeRecordingSegment(); } long recordingId() { return recordingId; } long replayPosition() { return replayPosition; } boolean isDone() { return isDone; } int poll(final SimpleFragmentHandler fragmentHandler, final int fragmentLimit) { int fragments = 0; while (replayPosition < replayLimit && fragments < fragmentLimit) { if (termOffset == termLength) { nextTerm(); } final int frameOffset = termOffset; final UnsafeBuffer termBuffer = this.termBuffer; final int frameLength = FrameDescriptor.frameLength(termBuffer, frameOffset); if (frameLength <= 0) { isDone = true; closeRecordingSegment(); break; } final int frameType = FrameDescriptor.frameType(termBuffer, frameOffset); final byte flags = FrameDescriptor.frameFlags(termBuffer, frameOffset); final long reservedValue = termBuffer.getLong(frameOffset + RESERVED_VALUE_OFFSET, LITTLE_ENDIAN); final int alignedLength = BitUtil.align(frameLength, FRAME_ALIGNMENT); final int dataOffset = frameOffset + DataHeaderFlyweight.HEADER_LENGTH; final int dataLength = frameLength - DataHeaderFlyweight.HEADER_LENGTH; fragmentHandler.onFragment(termBuffer, dataOffset, dataLength, frameType, flags, reservedValue); replayPosition += alignedLength; termOffset += alignedLength; fragments++; if (replayPosition >= replayLimit) { isDone = true; closeRecordingSegment(); break; } } return fragments; } private void nextTerm() { termOffset = 0; termBaseSegmentOffset += termLength; if (termBaseSegmentOffset == segmentLength) { closeRecordingSegment(); segmentFilePosition += segmentLength; openRecordingSegment(); termBaseSegmentOffset = 0; } termBuffer.wrap(mappedSegmentBuffer, termBaseSegmentOffset, termLength); } private void closeRecordingSegment() { final MappedByteBuffer mappedSegmentBuffer = this.mappedSegmentBuffer; this.mappedSegmentBuffer = null; IoUtil.unmap(mappedSegmentBuffer); } private void openRecordingSegment() { final String segmentFileName = segmentFileName(recordingId, segmentFilePosition); final File segmentFile = new File(archiveDir, segmentFileName); if (!segmentFile.exists()) { throw new IllegalArgumentException("failed to open recording segment file " + segmentFileName); } try (FileChannel channel = FileChannel.open(segmentFile.toPath(), FILE_OPTIONS, NO_ATTRIBUTES)) { mappedSegmentBuffer = channel.map(READ_ONLY, 0, segmentLength); } catch (final IOException ex) { LangUtil.rethrowUnchecked(ex); } } }