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

io.zeebe.logstreams.impl.LogBlockIndexWriter Maven / Gradle / Ivy

There is a newer version: 0.16.4
Show newest version
/*
 * 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.impl;

import static io.zeebe.logstreams.impl.LogEntryDescriptor.getPosition;
import static io.zeebe.logstreams.log.LogStreamUtil.INVALID_ADDRESS;
import static io.zeebe.logstreams.spi.LogStorage.OP_RESULT_INSUFFICIENT_BUFFER_CAPACITY;
import static io.zeebe.logstreams.spi.LogStorage.OP_RESULT_INVALID_ADDR;

import io.zeebe.logstreams.impl.log.index.LogBlockIndex;
import io.zeebe.logstreams.spi.LogStorage;
import io.zeebe.util.allocation.AllocatedBuffer;
import io.zeebe.util.allocation.BufferAllocators;
import io.zeebe.util.metrics.Metric;
import io.zeebe.util.metrics.MetricsManager;
import io.zeebe.util.sched.Actor;
import io.zeebe.util.sched.ActorCondition;
import io.zeebe.util.sched.channel.ActorConditions;
import io.zeebe.util.sched.future.ActorFuture;
import java.nio.ByteBuffer;
import java.time.Duration;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.status.Position;
import org.slf4j.Logger;

/** Read committed events from the log storage and append them to the block index. */
public class LogBlockIndexWriter extends Actor {
  public static final Logger LOG = Loggers.LOGSTREAMS_LOGGER;

  /**
   * The default deviation is 10%. That means for blocks which are filled 90% a block index will be
   * created.
   */
  public static final float DEFAULT_DEVIATION = 0.1f;

  private Runnable currentRunnable;

  private final Runnable runCurrentWork = this::runCurrentWork;
  private final Runnable readLogStorage = this::readLogStorage;
  private final Runnable addCurrentBlockToIndex = this::addCurrentBlockToIndex;
  private final Runnable createSnapshot = this::createSnapshot;

  private final String name;
  private final LogStorage logStorage;
  private final LogBlockIndex blockIndex;
  private final MetricsManager metricsManager;

  /** Defines the block size for which an index will be created. */
  private final int indexBlockSize;

  /**
   * The deviation which will be used in calculation of the index block size. It defines the
   * allowable tolerance. That means if the deviation is set to 0.1f (10%), an index will be created
   * if the block is 90 % filled.
   */
  private final float deviation;

  private final CompleteEventsInBlockProcessor completeEventsProcessor =
      new CompleteEventsInBlockProcessor();

  private long nextAddress = INVALID_ADDRESS;

  private int currentBlockSize = 0;
  private long currentBlockAddress = INVALID_ADDRESS;
  private long currentBlockEventPosition = 0;

  private long lastBlockAddress = 0;
  private long lastBlockEventPosition = 0;

  private final UnsafeBuffer buffer = new UnsafeBuffer(0, 0);
  private int bufferSize;
  private ByteBuffer ioBuffer;
  private AllocatedBuffer allocatedBuffer;

  private final Position commitPosition;
  private final ActorConditions onCommitPositionUpdatedConditions;
  private ActorCondition onCommitCondition;

  private final Duration snapshotInterval;
  private long snapshotEventPosition = -1;

  private Metric snapshotsCreated;

  public LogBlockIndexWriter(
      String name,
      LogStreamBuilder builder,
      LogStorage logStorage,
      LogBlockIndex blockIndex,
      MetricsManager metricsManager) {
    this.name = name;
    this.logStorage = logStorage;
    this.blockIndex = blockIndex;
    this.metricsManager = metricsManager;
    this.commitPosition = builder.getCommitPosition();
    this.onCommitPositionUpdatedConditions = builder.getOnCommitPositionUpdatedConditions();

    this.deviation = builder.getDeviation();
    this.indexBlockSize = (int) (builder.getIndexBlockSize() * (1f - deviation));
    this.snapshotInterval = builder.getSnapshotPeriod();
    this.bufferSize = builder.getReadBlockSize();

    this.allocatedBuffer = BufferAllocators.allocateDirect(bufferSize);
    this.ioBuffer = allocatedBuffer.getRawBuffer();
    this.buffer.wrap(ioBuffer);
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  protected void onActorStarting() {
    snapshotsCreated =
        metricsManager
            .newMetric("logstream_blockidx_snapshots")
            .type("counter")
            .label("logName", getName())
            .create();

    try {
      blockIndex.recoverFromSnapshot();
    } catch (Exception e) {
      LOG.debug(
          "Failed to recover from snapshot with error {}. Rebuilding block index.", e.getMessage());
    }

    try {
      blockIndex.openDb();

      final long snapshotPosition = blockIndex.getLastPosition();
      final long snapshotBlockAddress = blockIndex.lookupBlockAddress(snapshotPosition);

      if (snapshotBlockAddress >= logStorage.getFirstBlockAddress()) {
        nextAddress = snapshotBlockAddress;
        lastBlockAddress = snapshotBlockAddress;
        lastBlockEventPosition = snapshotPosition;
        snapshotEventPosition = snapshotPosition;
      } else {
        LOG.warn("Can't find address of snapshot position. Rebuilding block index.");
      }

      if (nextAddress == INVALID_ADDRESS) {
        nextAddress = logStorage.getFirstBlockAddress();
        lastBlockAddress = 0;
      }
    } catch (Exception e) {
      LOG.error("Failed to recover block index", e);
      throw new RuntimeException("Failed to recover block index", e);
    }
  }

  @Override
  protected void onActorStarted() {
    this.onCommitCondition = actor.onCondition("log-index-on-commit", runCurrentWork);
    onCommitPositionUpdatedConditions.registerConsumer(onCommitCondition);

    actor.runAtFixedRate(snapshotInterval, createSnapshot);

    if (nextAddress > 0) {
      currentRunnable = readLogStorage;
      runCurrentWork();
    } else {
      // the log is empty
      currentRunnable =
          () -> {
            // when the first position is committed
            // - then start reading on the head of the log
            nextAddress = logStorage.getFirstBlockAddress();

            currentRunnable = readLogStorage;
            runCurrentWork();
          };
    }
  }

  private void runCurrentWork() {
    actor.submit(currentRunnable);
  }

  private void readLogStorage() {
    ioBuffer.clear();

    final long currentAddress = nextAddress;
    final long result = logStorage.read(ioBuffer, currentAddress, completeEventsProcessor);

    if (result > currentAddress) {
      nextAddress = result;

      addToCurrentBlock(currentAddress, ioBuffer.position());

    } else if (result == OP_RESULT_INSUFFICIENT_BUFFER_CAPACITY) {
      increaseBufferSize();
      runCurrentWork();
    } else if (result == OP_RESULT_INVALID_ADDR) {
      LOG.warn("Can't read from illegal address: {}", currentAddress);

      nextAddress = lastBlockAddress;
      resetCurrentBlock();
    }
  }

  private void addToCurrentBlock(long currentAddress, int readBytes) {
    if (currentBlockAddress == INVALID_ADDRESS) {
      currentBlockAddress = currentAddress;
      currentBlockEventPosition = getPosition(buffer, 0);
    }

    currentBlockSize += readBytes;

    if (currentBlockSize >= indexBlockSize) {
      addCurrentBlockToIndex();
    } else {
      // block is not filled enough
      // - read more events into buffer
      runCurrentWork();
    }
  }

  private void addCurrentBlockToIndex() {
    if (isCurrentBlockCommitted()) {
      if (currentBlockAddress > lastBlockAddress) {
        LOG.trace(
            "Add block to index with position {} and address {}.",
            currentBlockEventPosition,
            currentBlockAddress);

        blockIndex.addBlock(currentBlockEventPosition, currentBlockAddress);

        lastBlockAddress = currentBlockAddress;
        lastBlockEventPosition = currentBlockEventPosition;
      }

      resetCurrentBlock();

      currentRunnable = readLogStorage;
    } else {
      // try again when commit position is updated
      currentRunnable = addCurrentBlockToIndex;
    }

    runCurrentWork();
  }

  private boolean isCurrentBlockCommitted() {
    return commitPosition.getVolatile() >= completeEventsProcessor.getLastReadEventPosition();
  }

  private void resetCurrentBlock() {
    currentBlockAddress = INVALID_ADDRESS;
    currentBlockEventPosition = 0;
    currentBlockSize = 0;
  }

  private void increaseBufferSize() {
    bufferSize *= 2;

    allocatedBuffer.close();

    allocatedBuffer = BufferAllocators.allocateDirect(bufferSize);
    ioBuffer = allocatedBuffer.getRawBuffer();
    buffer.wrap(ioBuffer);
  }

  private void createSnapshot() {
    try {
      if (lastBlockEventPosition > 0 && lastBlockEventPosition > snapshotEventPosition) {
        // flush the log to ensure that the snapshot doesn't contains indexes of unwritten events
        logStorage.flush();

        snapshotEventPosition = lastBlockEventPosition;
        blockIndex.writeSnapshot(snapshotEventPosition);

        LOG.trace("Created snapshot of block index {}.", name);
        snapshotsCreated.incrementOrdered();
      }
    } catch (Exception e) {
      LOG.warn("Failed to create snapshot of block index {}", name, e);
    }
  }

  public ActorFuture closeAsync() {
    return actor.close();
  }

  @Override
  protected void onActorClosing() {
    resetCurrentBlock();
    allocatedBuffer.close();
    onCommitPositionUpdatedConditions.removeConsumer(onCommitCondition);
    snapshotsCreated.close();
  }

  public Metric getSnapshotsCreated() {
    return snapshotsCreated;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy