All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.zeebe.logstreams.log.BufferedLogStreamReader Maven / Gradle / Ivy

/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Licensed under the Zeebe Community License 1.0. You may not use this file
 * except in compliance with the Zeebe Community License 1.0.
 */
package io.zeebe.logstreams.log;

import io.zeebe.logstreams.impl.CompleteEventsInBlockProcessor;
import io.zeebe.logstreams.impl.LogEntryDescriptor;
import io.zeebe.logstreams.impl.LoggedEventImpl;
import io.zeebe.logstreams.spi.LogStorage;
import io.zeebe.util.allocation.AllocatedBuffer;
import io.zeebe.util.allocation.BufferAllocator;
import io.zeebe.util.allocation.DirectBufferAllocator;
import java.nio.ByteBuffer;
import java.util.NoSuchElementException;
import java.util.function.LongUnaryOperator;
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;

  // configuration
  private final CompleteEventsInBlockProcessor completeEventsInBlockProcessor =
      new CompleteEventsInBlockProcessor();
  private final LoggedEventImpl nextEvent = new LoggedEventImpl();
  // event returned to caller (important: has to be preserved even after compact/buffer resize)
  private final LoggedEventImpl returnedEvent = new LoggedEventImpl();
  // buffer
  private final BufferAllocator bufferAllocator = new DirectBufferAllocator();
  private final DirectBuffer directBuffer = new UnsafeBuffer(0, 0);
  // wrapped logstream
  private LogStorage logStorage;
  // state
  private IteratorState state;
  private long nextLogStorageReadAddress;
  private long lastReadAddress;
  private AllocatedBuffer allocatedBuffer;
  private ByteBuffer byteBuffer;
  private int bufferOffset;

  public BufferedLogStreamReader(final LogStream logStream) {
    this();
    wrap(logStream);
  }

  public BufferedLogStreamReader() {
    state = IteratorState.WRAP_NOT_CALLED;
  }

  @Override
  public void wrap(final LogStream log) {
    wrap(log, FIRST_POSITION);
  }

  @Override
  public void wrap(final LogStream log, final long position) {
    wrap(log.getLogStorage(), position);
  }

  @Override
  public boolean seekToNextEvent(long position) {

    if (position <= -1) {
      seekToFirstEvent();
      return true;
    }

    final boolean found = seek(position);
    if (found && hasNext()) {
      next();
      return true;
    }

    return false;
  }

  @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 = logStorage.getFirstBlockAddress();
    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);
  }

  public long seekToEnd() {
    // invalidate events first as the buffer content may change
    invalidateBufferAndOffsets();

    final long blockAddress = logStorage.getFirstBlockAddress();
    if (blockAddress < 0) {
      // no block found => empty log
      state = IteratorState.EMPTY_LOG_STREAM;
      return -1;
    } else {
      readLastBlockIntoBuffer();
      return completeEventsInBlockProcessor.getLastReadEventPosition();
    }
  }

  @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 long lastReadAddress() {
    return lastReadAddress;
  }

  @Override
  public boolean isClosed() {
    return allocatedBuffer == null;
  }

  public void wrap(final LogStorage logStorage) {
    wrap(logStorage, FIRST_POSITION);
  }

  public void wrap(final LogStorage logStorage, final long position) {
    this.logStorage = logStorage;

    if (isClosed()) {
      allocateBuffer(DEFAULT_INITIAL_BUFFER_CAPACITY);
    }

    seek(position);
  }

  @Override
  public void close() {
    if (allocatedBuffer != null) {
      allocatedBuffer.close();
      allocatedBuffer = null;
      byteBuffer = null;
      directBuffer.wrap(0, 0);
      bufferOffset = 0;

      logStorage = null;

      state = IteratorState.WRAP_NOT_CALLED;
    }
  }

  @Override
  public boolean hasNext() {
    switch (state) {
      case EVENT_AVAILABLE:
        return true;
      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");
      default:
        throw new IllegalStateException("Unknown reader state " + state.name());
    }

    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()
        && (allocatedBuffer != null && 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 void readLastBlockIntoBuffer() {
    executeReadMethod(
        Long.MAX_VALUE,
        readAddress -> logStorage.readLastBlock(byteBuffer, completeEventsInBlockProcessor));
  }

  private boolean readBlockIntoBuffer(final long blockAddress) {
    return executeReadMethod(
        blockAddress,
        readAddress -> logStorage.read(byteBuffer, readAddress, completeEventsInBlockProcessor));
  }

  private boolean executeReadMethod(final long blockAddress, LongUnaryOperator readMethod) {
    if (byteBuffer.remaining() < LogEntryDescriptor.HEADER_BLOCK_LENGTH) {
      compactBuffer();
    }

    final long result = readMethod.applyAsLong(blockAddress);

    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 executeReadMethod(blockAddress, readMethod);
    } 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.lastReadAddress = blockAddress;
      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 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() {
    // Next Event is always committed.
    state = IteratorState.EVENT_AVAILABLE;
  }

  enum IteratorState {
    WRAP_NOT_CALLED,
    EMPTY_LOG_STREAM,
    EVENT_AVAILABLE,
    NOT_ENOUGH_DATA,
    EVENT_NOT_COMMITTED,
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy