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

io.camunda.zeebe.snapshots.impl.FileBasedTransientSnapshot Maven / Gradle / Ivy

There is a newer version: 8.6.0-rc2
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.snapshots.impl;

import io.camunda.zeebe.scheduler.ConcurrencyControl;
import io.camunda.zeebe.scheduler.future.ActorFuture;
import io.camunda.zeebe.scheduler.future.CompletableActorFuture;
import io.camunda.zeebe.snapshots.CRC32CChecksumProvider;
import io.camunda.zeebe.snapshots.MutableChecksumsSFV;
import io.camunda.zeebe.snapshots.PersistedSnapshot;
import io.camunda.zeebe.snapshots.SnapshotException.SnapshotNotFoundException;
import io.camunda.zeebe.snapshots.SnapshotId;
import io.camunda.zeebe.snapshots.TransientSnapshot;
import io.camunda.zeebe.util.FileUtil;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents a pending snapshot, that is a snapshot in the process of being written and has not yet
 * been committed to the store.
 */
public final class FileBasedTransientSnapshot implements TransientSnapshot {
  private static final Logger LOGGER = LoggerFactory.getLogger(FileBasedTransientSnapshot.class);

  private final Path directory;
  private final ConcurrencyControl actor;
  private final FileBasedSnapshotStoreImpl snapshotStore;
  private final FileBasedSnapshotId snapshotId;
  private final ActorFuture takenFuture = new CompletableActorFuture<>();
  private boolean isValid = false;
  private PersistedSnapshot snapshot;
  private MutableChecksumsSFV checksum;
  private final CRC32CChecksumProvider checksumProvider;
  private long lastFollowupEventPosition = Long.MAX_VALUE;

  FileBasedTransientSnapshot(
      final FileBasedSnapshotId snapshotId,
      final Path directory,
      final FileBasedSnapshotStoreImpl snapshotStore,
      final ConcurrencyControl actor,
      final CRC32CChecksumProvider checksumProvider) {
    this.snapshotId = snapshotId;
    this.snapshotStore = snapshotStore;
    this.directory = directory;
    this.actor = actor;
    this.checksumProvider = checksumProvider;
  }

  @Override
  public ActorFuture take(final Consumer takeSnapshot) {
    actor.run(() -> takeInternal(takeSnapshot));
    return takenFuture;
  }

  @Override
  public TransientSnapshot withLastFollowupEventPosition(final long lastFollowupEventPosition) {
    actor.run(() -> this.lastFollowupEventPosition = lastFollowupEventPosition);
    return this;
  }

  private void takeInternal(final Consumer takeSnapshot) {
    final var snapshotMetrics = snapshotStore.getSnapshotMetrics();

    try (final var ignored = snapshotMetrics.startTimer()) {
      try {
        takeSnapshot.accept(getPath());
        if (!directory.toFile().exists() || directory.toFile().listFiles().length == 0) {
          // If no snapshot files are created, snapshot is not valid
          abortInternal();
          takenFuture.completeExceptionally(
              new IllegalStateException(
                  String.format(
                      "Expected to find transient snapshot in directory %s, but the directory is empty or does not exists",
                      directory)));

        } else {
          checksum = SnapshotChecksum.calculateWithProvidedChecksums(directory, checksumProvider);

          snapshot = null;
          isValid = true;
          takenFuture.complete(null);
        }

      } catch (final Exception exception) {
        LOGGER.warn("Unexpected exception on taking snapshot ({})", snapshotId, exception);
        abortInternal();
        takenFuture.completeExceptionally(exception);
      }
    }
  }

  @Override
  public ActorFuture abort() {
    final CompletableActorFuture abortFuture = new CompletableActorFuture<>();
    actor.run(
        () -> {
          abortInternal();
          abortFuture.complete(null);
        });
    return abortFuture;
  }

  @Override
  public ActorFuture persist() {
    final CompletableActorFuture future = new CompletableActorFuture<>();
    actor.call(
        () -> {
          persistInternal(future);
          return null;
        });
    return future;
  }

  @Override
  public SnapshotId snapshotId() {
    return snapshotId;
  }

  @Override
  public Path getPath() {
    return directory;
  }

  private void persistInternal(final CompletableActorFuture future) {
    if (snapshot != null) {
      future.complete(snapshot);
      return;
    }

    if (!takenFuture.isDone() || takenFuture.isCompletedExceptionally()) {
      future.completeExceptionally(new IllegalStateException("Snapshot is not taken"));
      return;
    }

    if (!isValid) {
      future.completeExceptionally(
          new SnapshotNotFoundException("Snapshot may have been already deleted."));
      return;
    }

    try {
      final var metadata =
          new FileBasedSnapshotMetadata(
              FileBasedSnapshotStoreImpl.VERSION,
              snapshotId.getProcessedPosition(),
              snapshotId.getExportedPosition(),
              lastFollowupEventPosition);
      writeMetadataAndUpdateChecksum(metadata);
      snapshot = snapshotStore.persistNewSnapshot(snapshotId, checksum, metadata);
      future.complete(snapshot);
    } catch (final Exception e) {
      future.completeExceptionally(e);
    }

    snapshotStore.removePendingSnapshot(this);
  }

  private void writeMetadataAndUpdateChecksum(final FileBasedSnapshotMetadata metadata)
      throws IOException {
    final var metadataPath = directory.resolve(FileBasedSnapshotStoreImpl.METADATA_FILE_NAME);
    // Write metadata file along with snapshot files
    try (final var channel =
            FileChannel.open(
                metadataPath,
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE,
                StandardOpenOption.DSYNC);
        final var output = Channels.newOutputStream(channel)) {
      metadata.encode(output);
      checksum.updateFromFile(metadataPath);
    }
  }

  private void abortInternal() {
    try {
      isValid = false;
      snapshot = null;
      LOGGER.debug("Aborting transient snapshot {}", this);
      FileUtil.deleteFolderIfExists(directory);
    } catch (final IOException e) {
      LOGGER.warn("Failed to delete pending snapshot {}", this, e);
    } finally {
      snapshotStore.removePendingSnapshot(this);
    }
  }

  @Override
  public String toString() {
    return "FileBasedTransientSnapshot{"
        + "directory="
        + directory
        + ", snapshotId="
        + snapshotId
        + ", checksum="
        + checksum
        + '}';
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy