/*
* 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.archiver.codecs.RecordingDescriptorDecoder;
import io.aeron.protocol.DataHeaderFlyweight;
import org.agrona.*;
import org.agrona.concurrent.UnsafeBuffer;
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import static io.aeron.archiver.ArchiveUtil.*;
import static io.aeron.logbuffer.FrameDescriptor.FRAME_ALIGNMENT;
import static io.aeron.logbuffer.FrameDescriptor.PADDING_FRAME_TYPE;
import static java.nio.channels.FileChannel.MapMode.READ_ONLY;
import static java.nio.file.StandardOpenOption.READ;
class RecordingFragmentReader implements AutoCloseable
{
private final long recordingId;
private final File archiveDir;
private final int termBufferLength;
private final long joiningPosition;
private final long fullLength;
private final long fromPosition;
private final long replayLength;
private final int segmentFileLength;
private int segmentFileIndex;
private FileChannel currentDataChannel = null;
private UnsafeBuffer termMappedUnsafeBuffer = null;
private int recordingTermStartOffset;
private int fragmentOffset;
private long transmitted = 0;
private final DataHeaderFlyweight headerFlyweight = new DataHeaderFlyweight();
RecordingFragmentReader(final long recordingId, final File archiveDir) throws IOException
{
this.recordingId = recordingId;
this.archiveDir = archiveDir;
final String recordingMetaFileName = recordingMetaFileName(recordingId);
// TODO: Use metadata from catalog
final File recordingMetaFile = new File(archiveDir, recordingMetaFileName);
final RecordingDescriptorDecoder metaDecoder = recordingMetaFileFormatDecoder(recordingMetaFile);
termBufferLength = metaDecoder.termBufferLength();
joiningPosition = metaDecoder.joiningPosition();
segmentFileLength = metaDecoder.segmentFileLength();
fullLength = ArchiveUtil.recordingFileFullLength(metaDecoder);
IoUtil.unmap(metaDecoder.buffer().byteBuffer());
fromPosition = joiningPosition;
replayLength = fullLength;
initCursorState();
}
RecordingFragmentReader(
final long recordingId,
final File archiveDir,
final long position,
final long length) throws IOException
{
this.recordingId = recordingId;
this.archiveDir = archiveDir;
this.fromPosition = position;
this.replayLength = length;
final String recordingMetaFileName = recordingMetaFileName(recordingId);
final File recordingMetaFile = new File(archiveDir, recordingMetaFileName);
// TODO: Can this be done without mapping and unmapping?
final RecordingDescriptorDecoder metaDecoder = recordingMetaFileFormatDecoder(recordingMetaFile);
termBufferLength = metaDecoder.termBufferLength();
joiningPosition = metaDecoder.joiningPosition();
segmentFileLength = metaDecoder.segmentFileLength();
fullLength = ArchiveUtil.recordingFileFullLength(metaDecoder);
IoUtil.unmap(metaDecoder.buffer().byteBuffer());
initCursorState();
}
private void initCursorState() throws IOException
{
segmentFileIndex = segmentFileIndex(joiningPosition, fromPosition, segmentFileLength);
final long recordingOffset = fromPosition & (segmentFileLength - 1);
recordingTermStartOffset = (int) (recordingOffset - (recordingOffset & (termBufferLength - 1)));
openRecordingFile();
termMappedUnsafeBuffer = new UnsafeBuffer(
currentDataChannel.map(READ_ONLY, recordingTermStartOffset, termBufferLength));
// TODO: align first fragment
fragmentOffset = (int) (recordingOffset & (termBufferLength - 1));
}
int controlledPoll(final SimplifiedControlledPoll fragmentHandler, final int fragmentLimit)
throws IOException
{
if (isDone())
{
return 0;
}
int polled = 0;
// read to end of term or requested data
while (fragmentOffset < termBufferLength && !isDone() && polled < fragmentLimit)
{
final int fragmentOffset = this.fragmentOffset;
headerFlyweight.wrap(termMappedUnsafeBuffer, this.fragmentOffset, DataHeaderFlyweight.HEADER_LENGTH);
final int frameLength = headerFlyweight.frameLength();
if (frameLength <= 0)
{
throw new IllegalStateException("Broken frame with length <= 0: " + headerFlyweight);
}
final int alignedLength = BitUtil.align(frameLength, FRAME_ALIGNMENT);
// cursor moves forward, importantly an exception from onFragment will not block progress
transmitted += alignedLength;
this.fragmentOffset += alignedLength;
if (headerFlyweight.headerType() == PADDING_FRAME_TYPE)
{
continue;
}
final int fragmentDataOffset = fragmentOffset + DataHeaderFlyweight.DATA_OFFSET;
final int fragmentDataLength = frameLength - DataHeaderFlyweight.HEADER_LENGTH;
if (!fragmentHandler.onFragment(
termMappedUnsafeBuffer,
fragmentDataOffset,
fragmentDataLength,
headerFlyweight))
{
// rollback the cursor progress
transmitted -= alignedLength;
this.fragmentOffset -= alignedLength;
return polled;
}
// only count data fragments
polled++;
}
if (!isDone() && fragmentOffset == termBufferLength)
{
fragmentOffset = 0;
recordingTermStartOffset += termBufferLength;
// rotate file
if (recordingTermStartOffset == segmentFileLength)
{
closeRecordingFile();
segmentFileIndex++;
openRecordingFile();
recordingTermStartOffset = 0;
}
else
{
IoUtil.unmap(termMappedUnsafeBuffer.byteBuffer());
}
// TODO: Is a mapping created per term? Would it not be more efficient to do it per segment?
// rotate term
final MappedByteBuffer mappedByteBuffer = currentDataChannel.map(
READ_ONLY, recordingTermStartOffset, termBufferLength);
termMappedUnsafeBuffer.wrap(mappedByteBuffer);
}
return polled;
}
private void closeRecordingFile()
{
IoUtil.unmap(termMappedUnsafeBuffer.byteBuffer());
CloseHelper.close(currentDataChannel);
}
private void openRecordingFile() throws IOException
{
final String recordingDataFileName = recordingDataFileName(recordingId, segmentFileIndex);
final File recordingDataFile = new File(archiveDir, recordingDataFileName);
if (!recordingDataFile.exists())
{
throw new IOException(recordingDataFile.getAbsolutePath() + " not found");
}
currentDataChannel = FileChannel.open(recordingDataFile.toPath(), READ);
}
boolean isDone()
{
return transmitted >= replayLength;
}
public void close()
{
closeRecordingFile();
}
interface SimplifiedControlledPoll
{
/**
* Called by the {@link RecordingFragmentReader}. Implementors need only process DATA fragments.
*
* @return true if fragment processed, false to abort.
*/
boolean onFragment(
DirectBuffer fragmentBuffer,
int fragmentOffset,
int fragmentLength,
DataHeaderFlyweight header);
}
}