io.zeebe.logstreams.log.BufferedLogStreamReader Maven / Gradle / Ivy
/*
* Copyright © 2017 camunda services GmbH ([email protected])
*
* 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.zeebe.logstreams.log;
import java.nio.ByteBuffer;
import java.util.NoSuchElementException;
import io.zeebe.logstreams.impl.CompleteEventsInBlockProcessor;
import io.zeebe.logstreams.impl.LogEntryDescriptor;
import io.zeebe.logstreams.impl.LoggedEventImpl;
import io.zeebe.logstreams.impl.log.index.LogBlockIndex;
import io.zeebe.logstreams.spi.LogStorage;
import io.zeebe.logstreams.spi.ReadResultProcessor;
import io.zeebe.util.allocation.AllocatedBuffer;
import io.zeebe.util.allocation.BufferAllocator;
import io.zeebe.util.allocation.DirectBufferAllocator;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;
public class BufferedLogStreamReader implements LogStreamReader
{
public static final int DEFAULT_INITIAL_BUFFER_CAPACITY = 32 * 1024;
public static final int MAX_BUFFER_CAPACITY = 128 * 1024 * 1024; // 128MB
private static final int UNINITIALIZED = -1;
private static final long FIRST_POSITION = Long.MIN_VALUE;
private static final long LAST_POSITION = Long.MAX_VALUE;
// configuration
private final boolean readUncommittedEntries;
private final ReadResultProcessor completeEventsInBlockProcessor = new CompleteEventsInBlockProcessor();
// wrapped logstream
private LogStream logStream;
private LogStorage logStorage;
private LogBlockIndex logBlockIndex;
// state
private IteratorState state;
private long nextLogStorageReadAddress;
private LoggedEventImpl nextEvent = new LoggedEventImpl();
// event returned to caller (important: has to be preserved even after compact/buffer resize)
private LoggedEventImpl returnedEvent = new LoggedEventImpl();
// buffer
private final BufferAllocator bufferAllocator = new DirectBufferAllocator();
private AllocatedBuffer allocatedBuffer;
private ByteBuffer byteBuffer;
private int bufferOffset;
private DirectBuffer directBuffer = new UnsafeBuffer(0, 0);
public BufferedLogStreamReader()
{
this(false);
}
public BufferedLogStreamReader(final LogStream logStream)
{
this();
wrap(logStream);
}
public BufferedLogStreamReader(final boolean readUncommittedEntries)
{
this.readUncommittedEntries = readUncommittedEntries;
state = IteratorState.WRAP_NOT_CALLED;
}
public BufferedLogStreamReader(final LogStream logStream, final boolean readUncommittedEntries)
{
this(readUncommittedEntries);
wrap(logStream);
}
@Override
public void wrap(final LogStream log)
{
wrap(log, FIRST_POSITION);
}
@Override
public void wrap(final LogStream log, final long position)
{
logStream = log;
wrap(log.getLogStorage(), log.getLogBlockIndex(), position);
}
public void wrap(final LogStorage logStorage, final LogBlockIndex logBlockIndex)
{
wrap(logStorage, logBlockIndex, FIRST_POSITION);
}
public void wrap(final LogStorage logStorage, final LogBlockIndex logBlockIndex, final long position)
{
this.logStorage = logStorage;
this.logBlockIndex = logBlockIndex;
if (isClosed())
{
allocateBuffer(DEFAULT_INITIAL_BUFFER_CAPACITY);
}
seek(position);
}
@Override
public boolean seek(final long position)
{
if (state == IteratorState.WRAP_NOT_CALLED)
{
throw new IllegalStateException("Iterator not initialized");
}
// invalidate events first as the buffer content may change
invalidateBufferAndOffsets();
final long blockAddress = lookUpBlockAddressForPosition(position);
if (blockAddress < 0)
{
// no block found => empty log
state = IteratorState.EMPTY_LOG_STREAM;
return false;
}
else
{
readBlockIntoBuffer(blockAddress);
readNextEvent();
return searchPositionInBuffer(position);
}
}
@Override
public void seekToFirstEvent()
{
seek(FIRST_POSITION);
}
@Override
public void seekToLastEvent()
{
seek(getLastPosition());
if (isNextEventInitialized())
{
checkIfNextEventIsCommitted();
}
}
@Override
public long getPosition()
{
// if an event was already returned use it's position otherwise use position of next event if available, kind of strange but seemed to be the old API
if (isReturnedEventInitialized())
{
return returnedEvent.getPosition();
}
switch (state)
{
case EVENT_AVAILABLE:
return nextEvent.getPosition();
default:
return UNINITIALIZED;
}
}
@Override
public boolean isClosed()
{
return allocatedBuffer == null;
}
@Override
public void close()
{
if (allocatedBuffer != null)
{
allocatedBuffer.close();
allocatedBuffer = null;
byteBuffer = null;
directBuffer.wrap(0, 0);
bufferOffset = 0;
logStream = null;
logStorage = null;
logBlockIndex = null;
state = IteratorState.WRAP_NOT_CALLED;
}
}
@Override
public boolean hasNext()
{
switch (state)
{
case EMPTY_LOG_STREAM:
seekToFirstEvent();
break;
case NOT_ENOUGH_DATA:
readNextAddress();
break;
case EVENT_NOT_COMMITTED:
checkIfNextEventIsCommitted();
break;
case WRAP_NOT_CALLED:
throw new IllegalStateException("Iterator not initialized");
}
return state == IteratorState.EVENT_AVAILABLE;
}
@Override
public LoggedEvent next()
{
switch (state)
{
case EVENT_AVAILABLE:
// wrap event for returning
wrapReturnedEvent(nextEvent.getFragmentOffset());
// find next event in log
readNextEvent();
return returnedEvent;
case WRAP_NOT_CALLED:
throw new IllegalStateException("Iterator not initialized");
default:
throw new NoSuchElementException("Api protocol violation: No next log entry available; You need to probe with hasNext() first.");
}
}
private void allocateBuffer(final int capacity)
{
if (!isClosed() && this.allocatedBuffer.capacity() == MAX_BUFFER_CAPACITY && capacity >= MAX_BUFFER_CAPACITY)
{
throw new RuntimeException("Next fragment requires more space then the maximal buffer capacity of " + BufferedLogStreamReader.MAX_BUFFER_CAPACITY);
}
final AllocatedBuffer newAllocatedBuffer = bufferAllocator.allocate(capacity);
final ByteBuffer newByteBuffer = newAllocatedBuffer.getRawBuffer();
if (!isClosed())
{
// copy remaining data to new buffer
// set position to minimal offset to preserve
// set limit to bufferOffset to remove everything afterwards as it will be read again next time
final int offsetToCopy = minimalOffsetToPreserve();
byteBuffer.position(offsetToCopy);
byteBuffer.limit(bufferOffset);
newByteBuffer.put(byteBuffer);
// update buffer and event offsets
bufferOffset -= offsetToCopy;
wrapReturnedEvent(returnedEvent.getFragmentOffset() - offsetToCopy);
wrapNextEvent(nextEvent.getFragmentOffset() - offsetToCopy);
}
else
{
// update buffer offset and invalidate events
invalidateBufferAndOffsets();
}
// replace old buffers by new ones
byteBuffer = newByteBuffer;
directBuffer.wrap(byteBuffer);
if (allocatedBuffer != null)
{
allocatedBuffer.close();
}
allocatedBuffer = newAllocatedBuffer;
}
private void compactBuffer()
{
// check if an event is wrapped and preserve it
if (isReturnedEventInitialized() || isNextEventInitialized())
{
final int offsetToCopy = minimalOffsetToPreserve();
// set position to last returned offset
byteBuffer.position(offsetToCopy);
// compact buffer to move old events to front
byteBuffer.compact();
// update buffer offset
bufferOffset -= offsetToCopy;
// update event offsets
if (isNextEventInitialized())
{
wrapNextEvent(nextEvent.getFragmentOffset() - offsetToCopy);
}
if (isReturnedEventInitialized())
{
wrapReturnedEvent(returnedEvent.getFragmentOffset() - offsetToCopy);
}
}
else
{
// otherwise just clear the buffer
invalidateBufferAndOffsets();
byteBuffer.clear();
}
}
private boolean readBlockIntoBuffer(final long blockAddress)
{
if (byteBuffer.remaining() < LogEntryDescriptor.HEADER_BLOCK_LENGTH)
{
compactBuffer();
}
final long result = logStorage.read(byteBuffer, blockAddress, completeEventsInBlockProcessor);
if (result == LogStorage.OP_RESULT_INSUFFICIENT_BUFFER_CAPACITY)
{
// it was not possible to read the block in the existing buffer => expand buffer
long nextCapacity = 2L * (long) byteBuffer.capacity();
nextCapacity = Math.min(nextCapacity, MAX_BUFFER_CAPACITY);
allocateBuffer((int) nextCapacity);
// retry to read the next block
return readBlockIntoBuffer(blockAddress);
}
else if (result == LogStorage.OP_RESULT_INVALID_ADDR)
{
throw new IllegalStateException("Invalid address to read from " + blockAddress);
}
else if (result == LogStorage.OP_RESULT_NO_DATA)
{
state = IteratorState.NOT_ENOUGH_DATA;
return false;
}
else
{
this.nextLogStorageReadAddress = result;
return true;
}
}
private boolean searchPositionInBuffer(final long position)
{
while (isNextUncommittedEventAvailable() && nextEvent.getPosition() < position)
{
readNextEvent();
}
if (nextEvent.getPosition() < position)
{
// not in buffered block, read next block and continue the search
return readNextAddress() && searchPositionInBuffer(position);
}
return nextEvent.getPosition() == position;
}
private boolean isNextUncommittedEventAvailable()
{
return state == IteratorState.EVENT_AVAILABLE || state == IteratorState.EVENT_NOT_COMMITTED;
}
private long lookUpBlockAddressForPosition(final long position)
{
long address = logBlockIndex.lookupBlockAddress(position);
if (address < 0)
{
// position not found in index fallback to first block
address = logStorage.getFirstBlockAddress();
}
return address;
}
private boolean readNextAddress()
{
final boolean blockFound = readBlockIntoBuffer(nextLogStorageReadAddress);
if (blockFound)
{
readNextEvent();
}
return blockFound;
}
private void readNextEvent()
{
// initially we assume there is not enough data
state = IteratorState.NOT_ENOUGH_DATA;
final int remaining = byteBuffer.position() - bufferOffset;
if (remaining > 0)
{
wrapNextEvent(bufferOffset);
bufferOffset += nextEvent.getFragmentLength();
checkIfNextEventIsCommitted();
}
else
{
readNextAddress();
}
}
private boolean isReturnedEventInitialized()
{
return returnedEvent.getFragmentOffset() >= 0;
}
private boolean isNextEventInitialized()
{
return nextEvent.getFragmentOffset() >= 0;
}
private int minimalOffsetToPreserve()
{
if (isReturnedEventInitialized())
{
return returnedEvent.getFragmentOffset();
}
else if (isNextEventInitialized())
{
return nextEvent.getFragmentOffset();
}
else
{
return bufferOffset;
}
}
private void invalidateBufferAndOffsets()
{
state = IteratorState.NOT_ENOUGH_DATA;
wrapNextEvent(UNINITIALIZED);
wrapReturnedEvent(UNINITIALIZED);
bufferOffset = 0;
if (!isClosed())
{
byteBuffer.clear();
}
}
private void wrapNextEvent(final int offset)
{
nextEvent.wrap(directBuffer, offset);
}
private void wrapReturnedEvent(final int offset)
{
returnedEvent.wrap(directBuffer, offset);
}
private void checkIfNextEventIsCommitted()
{
if (readUncommittedEntries || isNextEventCommitted())
{
state = IteratorState.EVENT_AVAILABLE;
}
else
{
state = IteratorState.EVENT_NOT_COMMITTED;
}
}
private boolean isNextEventCommitted()
{
return nextEvent.getPosition() <= getCommitPosition();
}
private long getCommitPosition()
{
return logStream.getCommitPosition();
}
private long getLastPosition()
{
if (readUncommittedEntries)
{
return LAST_POSITION;
}
else
{
return getCommitPosition();
}
}
enum IteratorState
{
WRAP_NOT_CALLED, EMPTY_LOG_STREAM, EVENT_AVAILABLE, NOT_ENOUGH_DATA, EVENT_NOT_COMMITTED,
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy