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

io.camunda.zeebe.broker.system.partitions.impl.StateControllerImpl Maven / Gradle / Ivy

There is a newer version: 8.7.0-alpha1
Show newest version
/*
 * 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 Camunda License 1.0. You may not use this file
 * except in compliance with the Camunda License 1.0.
 */
package io.camunda.zeebe.broker.system.partitions.impl;

import static java.util.Objects.requireNonNull;

import io.camunda.zeebe.broker.system.partitions.AtomixRecordEntrySupplier;
import io.camunda.zeebe.broker.system.partitions.NoEntryAtSnapshotPosition;
import io.camunda.zeebe.broker.system.partitions.StateController;
import io.camunda.zeebe.db.ZeebeDb;
import io.camunda.zeebe.db.ZeebeDbException;
import io.camunda.zeebe.db.ZeebeDbFactory;
import io.camunda.zeebe.logstreams.impl.Loggers;
import io.camunda.zeebe.scheduler.ConcurrencyControl;
import io.camunda.zeebe.scheduler.future.ActorFuture;
import io.camunda.zeebe.snapshots.ConstructableSnapshotStore;
import io.camunda.zeebe.snapshots.PersistedSnapshot;
import io.camunda.zeebe.snapshots.SnapshotException.StateClosedException;
import io.camunda.zeebe.snapshots.TransientSnapshot;
import io.camunda.zeebe.util.FileUtil;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.ToLongFunction;
import org.slf4j.Logger;

/** Controls how snapshot/recovery operations are performed */
@SuppressWarnings("rawtypes")
public class StateControllerImpl implements StateController {

  private static final Logger LOG = Loggers.SNAPSHOT_LOGGER;

  private final Path runtimeDirectory;

  private final ZeebeDbFactory zeebeDbFactory;

  private final ToLongFunction exporterPositionSupplier;

  private final AtomixRecordEntrySupplier entrySupplier;

  private ZeebeDb db;

  private final ConstructableSnapshotStore constructableSnapshotStore;
  private final ConcurrencyControl concurrencyControl;

  public StateControllerImpl(
      final ZeebeDbFactory zeebeDbFactory,
      final ConstructableSnapshotStore constructableSnapshotStore,
      final Path runtimeDirectory,
      final AtomixRecordEntrySupplier entrySupplier,
      final ToLongFunction exporterPositionSupplier,
      final ConcurrencyControl concurrencyControl) {
    this.constructableSnapshotStore = requireNonNull(constructableSnapshotStore);
    this.runtimeDirectory = requireNonNull(runtimeDirectory);
    this.zeebeDbFactory = requireNonNull(zeebeDbFactory);
    this.exporterPositionSupplier = requireNonNull(exporterPositionSupplier);
    this.entrySupplier = requireNonNull(entrySupplier);
    this.concurrencyControl = requireNonNull(concurrencyControl);
  }

  @Override
  public ActorFuture takeTransientSnapshot(
      final long lowerBoundSnapshotPosition) {
    final ActorFuture future = concurrencyControl.createFuture();
    concurrencyControl.run(() -> takeTransientSnapshotInternal(lowerBoundSnapshotPosition, future));
    return future;
  }

  @Override
  public ActorFuture recover() {
    final ActorFuture future = concurrencyControl.createFuture();
    concurrencyControl.run(() -> recoverInternal(future));
    return future;
  }

  @Override
  public ActorFuture closeDb() {
    final ActorFuture future = concurrencyControl.createFuture();
    concurrencyControl.run(() -> closeDbInternal(future));
    return future;
  }

  private void closeDbInternal(final ActorFuture future) {
    try {
      if (db != null) {
        final var dbToClose = db;
        db = null;
        dbToClose.close();

        LOG.debug("Closed database from '{}'.", runtimeDirectory);
      }

      tryDeletingRuntimeDirectory();
      future.complete(null);
    } catch (final Exception e) {
      future.completeExceptionally(e);
    }
  }

  private void recoverInternal(final ActorFuture future) {
    try {
      FileUtil.deleteFolderIfExists(runtimeDirectory);
    } catch (final IOException e) {
      future.completeExceptionally(
          new RuntimeException(
              "Failed to delete runtime folder. Cannot recover from snapshot.", e));
    }

    constructableSnapshotStore
        .getLatestSnapshot()
        .ifPresent(snapshot -> recoverFromSnapshot(future, snapshot));
    openDb(future);
  }

  private void recoverFromSnapshot(
      final ActorFuture future, final PersistedSnapshot snapshot) {
    LOG.debug("Recovering state from available snapshot: {}", snapshot);

    try (final var db = zeebeDbFactory.openSnapshotOnlyDb(snapshot.getPath().toFile())) {
      db.createSnapshot(runtimeDirectory.toFile());
    } catch (final Exception e) {
      future.completeExceptionally(
          new ZeebeDbException(
              String.format("Failed to recover from snapshot %s", snapshot.getId()), e));
    }
  }

  private void takeTransientSnapshotInternal(
      final long lowerBoundSnapshotPosition, final ActorFuture future) {
    if (!isDbOpened()) {
      final String error =
          String.format(
              "Expected to take snapshot for last processed position %d, but database was closed.",
              lowerBoundSnapshotPosition);
      future.completeExceptionally(new StateClosedException(error));
      return;
    }

    final NextSnapshotId nextSnapshotId;
    try {
      nextSnapshotId = tryFindNextSnapshotId(lowerBoundSnapshotPosition);
    } catch (final NoEntryAtSnapshotPosition e) {
      future.completeExceptionally(e);
      return;
    }

    final var transientSnapshot =
        constructableSnapshotStore.newTransientSnapshot(
            nextSnapshotId.index,
            nextSnapshotId.term,
            nextSnapshotId.processedPosition,
            nextSnapshotId.exportedPosition);

    if (transientSnapshot.isLeft()) {
      future.completeExceptionally(transientSnapshot.getLeft());
    } else {
      takeSnapshot(transientSnapshot.get(), future);
    }
  }

  private NextSnapshotId tryFindNextSnapshotId(final long lastProcessedPosition)
      throws NoEntryAtSnapshotPosition {
    final var exportedPosition = exporterPositionSupplier.applyAsLong(db);
    if (exportedPosition == -1) {
      final var latestSnapshot = constructableSnapshotStore.getLatestSnapshot();
      if (latestSnapshot.isPresent()) {
        // re-use index and term from the latest snapshot to ensure that the records from there are
        // not compacted until they get exported.
        final var persistedSnapshot = latestSnapshot.get();
        return new NextSnapshotId(
            persistedSnapshot.getIndex(), persistedSnapshot.getTerm(), lastProcessedPosition, 0);
      }

      return new NextSnapshotId(0, 0, lastProcessedPosition, 0);
    }

    final var snapshotPosition = Math.min(exportedPosition, lastProcessedPosition);
    final var logEntry = entrySupplier.getPreviousIndexedEntry(snapshotPosition);

    if (logEntry.isPresent()) {
      return new NextSnapshotId(
          logEntry.get().index(), logEntry.get().term(), lastProcessedPosition, exportedPosition);
    }

    // No log entry for snapshot position - try to use the index and term of the last snapshot to
    // take new one
    final var latestSnapshot = constructableSnapshotStore.getLatestSnapshot();
    if (latestSnapshot.isPresent()) {
      LOG.warn(
          "No log entry for next snapshot position {}, using index and term from previous snapshot",
          snapshotPosition);
      return new NextSnapshotId(
          latestSnapshot.get().getIndex(),
          latestSnapshot.get().getTerm(),
          lastProcessedPosition,
          exportedPosition);
    }

    throw new NoEntryAtSnapshotPosition(
        String.format(
            "Failed to take snapshot. Expected to find an indexed entry for determined snapshot position %d (processedPosition = %d, exportedPosition=%d) or previous snapshot, but found neither.",
            snapshotPosition, lastProcessedPosition, exportedPosition));
  }

  @SuppressWarnings("rawtypes")
  private void openDb(final ActorFuture future) {
    try {
      if (db == null) {
        db = zeebeDbFactory.createDb(runtimeDirectory.toFile());
        LOG.debug("Opened database from '{}'.", runtimeDirectory);
        future.complete(db);
      }
    } catch (final Exception error) {
      future.completeExceptionally(new RuntimeException("Failed to open database", error));
    }
  }

  private void tryDeletingRuntimeDirectory() {
    try {
      FileUtil.deleteFolderIfExists(runtimeDirectory);
    } catch (final Exception e) {
      LOG.debug("Failed to delete runtime directory when closing", e);
    }
  }

  @Override
  public void close() throws Exception {
    closeDb();
  }

  boolean isDbOpened() {
    return db != null;
  }

  private void takeSnapshot(
      final TransientSnapshot snapshot,
      final ActorFuture transientSnapshotFuture) {
    final var snapshotTaken =
        snapshot.take(
            snapshotDir -> {
              if (db == null) {
                throw new StateClosedException(
                    "Expected to take a snapshot, but no database was opened");
              }

              LOG.debug("Taking temporary snapshot into {}.", snapshotDir);
              db.createSnapshot(snapshotDir.toFile());
            });

    snapshotTaken.onComplete(
        (ok, error) -> {
          if (error != null) {
            transientSnapshotFuture.completeExceptionally(error);
          } else {
            transientSnapshotFuture.complete(snapshot);
          }
        });
  }

  private record NextSnapshotId(
      long index, long term, long processedPosition, long exportedPosition) {}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy