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

org.apache.hudi.client.AbstractHoodieWriteClient Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hudi.client;

import org.apache.hudi.avro.model.HoodieCleanMetadata;
import org.apache.hudi.avro.model.HoodieCleanerPlan;
import org.apache.hudi.avro.model.HoodieClusteringPlan;
import org.apache.hudi.avro.model.HoodieCompactionPlan;
import org.apache.hudi.avro.model.HoodieRestoreMetadata;
import org.apache.hudi.avro.model.HoodieRollbackMetadata;
import org.apache.hudi.avro.model.HoodieRollbackPlan;
import org.apache.hudi.callback.HoodieWriteCommitCallback;
import org.apache.hudi.callback.common.HoodieWriteCommitCallbackMessage;
import org.apache.hudi.callback.util.HoodieCommitCallbackFactory;
import org.apache.hudi.client.embedded.EmbeddedTimelineService;
import org.apache.hudi.client.heartbeat.HeartbeatUtils;
import org.apache.hudi.client.transaction.TransactionManager;
import org.apache.hudi.client.utils.TransactionUtils;
import org.apache.hudi.common.HoodiePendingRollbackInfo;
import org.apache.hudi.common.engine.HoodieEngineContext;
import org.apache.hudi.common.model.HoodieCommitMetadata;
import org.apache.hudi.common.model.HoodieFailedWritesCleaningPolicy;
import org.apache.hudi.common.model.HoodieKey;
import org.apache.hudi.common.model.HoodieRecordPayload;
import org.apache.hudi.common.model.HoodieWriteStat;
import org.apache.hudi.common.model.TableServiceType;
import org.apache.hudi.common.model.WriteOperationType;
import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
import org.apache.hudi.common.table.timeline.HoodieInstant;
import org.apache.hudi.common.table.timeline.HoodieInstant.State;
import org.apache.hudi.common.table.timeline.HoodieTimeline;
import org.apache.hudi.common.util.CleanerUtils;
import org.apache.hudi.common.util.ClusteringUtils;
import org.apache.hudi.common.util.CommitUtils;
import org.apache.hudi.common.util.Option;
import org.apache.hudi.common.util.ValidationUtils;
import org.apache.hudi.common.util.collection.Pair;
import org.apache.hudi.config.HoodieClusteringConfig;
import org.apache.hudi.config.HoodieCompactionConfig;
import org.apache.hudi.config.HoodieWriteConfig;
import org.apache.hudi.exception.HoodieCommitException;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.exception.HoodieIOException;
import org.apache.hudi.exception.HoodieRestoreException;
import org.apache.hudi.exception.HoodieRollbackException;
import org.apache.hudi.exception.HoodieSavepointException;
import org.apache.hudi.index.HoodieIndex;
import org.apache.hudi.metadata.HoodieTableMetadataWriter;
import org.apache.hudi.metrics.HoodieMetrics;
import org.apache.hudi.table.BulkInsertPartitioner;
import org.apache.hudi.table.HoodieTable;
import org.apache.hudi.table.HoodieTimelineArchiveLog;
import org.apache.hudi.table.action.HoodieWriteMetadata;
import org.apache.hudi.table.action.rollback.RollbackUtils;
import org.apache.hudi.table.action.savepoint.SavepointHelpers;
import org.apache.hudi.table.marker.WriteMarkersFactory;

import com.codahale.metrics.Timer;
import org.apache.hadoop.conf.Configuration;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Abstract Write Client providing functionality for performing commit, index updates and rollback
 * Reused for regular write operations like upsert/insert/bulk-insert.. as well as bootstrap
 *
 * @param  Sub type of HoodieRecordPayload
 * @param  Type of inputs
 * @param  Type of keys
 * @param  Type of outputs
 */
public abstract class AbstractHoodieWriteClient extends AbstractHoodieClient {

  protected static final String LOOKUP_STR = "lookup";
  private static final long serialVersionUID = 1L;
  private static final Logger LOG = LogManager.getLogger(AbstractHoodieWriteClient.class);

  protected final transient HoodieMetrics metrics;
  private final transient HoodieIndex index;

  protected transient Timer.Context writeTimer = null;
  protected transient Timer.Context compactionTimer;
  protected transient Timer.Context clusteringTimer;

  private transient WriteOperationType operationType;
  private transient HoodieWriteCommitCallback commitCallback;
  protected transient AsyncCleanerService asyncCleanerService;
  protected final TransactionManager txnManager;
  protected Option>> lastCompletedTxnAndMetadata = Option.empty();

  /**
   * Create a write client, with new hudi index.
   * @param context HoodieEngineContext
   * @param writeConfig instance of HoodieWriteConfig
   */
  @Deprecated
  public AbstractHoodieWriteClient(HoodieEngineContext context, HoodieWriteConfig writeConfig) {
    this(context, writeConfig, Option.empty());
  }

  /**
   * Create a write client, allows to specify all parameters.
   * @param context         HoodieEngineContext
   * @param writeConfig instance of HoodieWriteConfig
   * @param timelineService Timeline Service that runs as part of write client.
   */
  @Deprecated
  public AbstractHoodieWriteClient(HoodieEngineContext context, HoodieWriteConfig writeConfig,
                                   Option timelineService) {
    super(context, writeConfig, timelineService);
    this.metrics = new HoodieMetrics(config);
    this.index = createIndex(writeConfig);
    this.txnManager = new TransactionManager(config, fs);
  }

  protected abstract HoodieIndex createIndex(HoodieWriteConfig writeConfig);

  public void setOperationType(WriteOperationType operationType) {
    this.operationType = operationType;
  }

  public WriteOperationType getOperationType() {
    return this.operationType;
  }

  /**
   * Commit changes performed at the given instantTime marker.
   */
  public boolean commit(String instantTime, O writeStatuses) {
    return commit(instantTime, writeStatuses, Option.empty());
  }

  /**
   *
   * Commit changes performed at the given instantTime marker.
   */
  public boolean commit(String instantTime, O writeStatuses, Option> extraMetadata) {
    HoodieTableMetaClient metaClient = createMetaClient(false);
    String actionType = metaClient.getCommitActionType();
    return commit(instantTime, writeStatuses, extraMetadata, actionType, Collections.emptyMap());
  }

  public abstract boolean commit(String instantTime, O writeStatuses, Option> extraMetadata,
                                 String commitActionType, Map> partitionToReplacedFileIds);

  public boolean commitStats(String instantTime, List stats, Option> extraMetadata,
                             String commitActionType) {
    return commitStats(instantTime, stats, extraMetadata, commitActionType, Collections.emptyMap());
  }

  public boolean commitStats(String instantTime, List stats, Option> extraMetadata,
                             String commitActionType, Map> partitionToReplaceFileIds) {
    // Skip the empty commit if not allowed
    if (!config.allowEmptyCommit() && stats.isEmpty()) {
      return true;
    }
    LOG.info("Committing " + instantTime + " action " + commitActionType);
    // Create a Hoodie table which encapsulated the commits and files visible
    HoodieTable table = createTable(config, hadoopConf);
    HoodieCommitMetadata metadata = CommitUtils.buildMetadata(stats, partitionToReplaceFileIds,
        extraMetadata, operationType, config.getWriteSchema(), commitActionType);
    HoodieInstant inflightInstant = new HoodieInstant(State.INFLIGHT, table.getMetaClient().getCommitActionType(), instantTime);
    HeartbeatUtils.abortIfHeartbeatExpired(instantTime, table, heartbeatClient, config);
    this.txnManager.beginTransaction(Option.of(inflightInstant),
        lastCompletedTxnAndMetadata.isPresent() ? Option.of(lastCompletedTxnAndMetadata.get().getLeft()) : Option.empty());
    try {
      preCommit(inflightInstant, metadata);
      commit(table, commitActionType, instantTime, metadata, stats);
      postCommit(table, metadata, instantTime, extraMetadata);
      LOG.info("Committed " + instantTime);
      releaseResources();
    } catch (IOException e) {
      throw new HoodieCommitException("Failed to complete commit " + config.getBasePath() + " at time " + instantTime, e);
    } finally {
      this.txnManager.endTransaction(Option.of(inflightInstant));
    }
    // do this outside of lock since compaction, clustering can be time taking and we don't need a lock for the entire execution period
    runTableServicesInline(table, metadata, extraMetadata);
    emitCommitMetrics(instantTime, metadata, commitActionType);
    // callback if needed.
    if (config.writeCommitCallbackOn()) {
      if (null == commitCallback) {
        commitCallback = HoodieCommitCallbackFactory.create(config);
      }
      commitCallback.call(new HoodieWriteCommitCallbackMessage(instantTime, config.getTableName(), config.getBasePath(), stats));
    }
    return true;
  }

  protected void commit(HoodieTable table, String commitActionType, String instantTime, HoodieCommitMetadata metadata,
                      List stats) throws IOException {
    LOG.info("Committing " + instantTime + " action " + commitActionType);
    HoodieActiveTimeline activeTimeline = table.getActiveTimeline();
    // Finalize write
    finalizeWrite(table, instantTime, stats);
    // update Metadata table
    writeTableMetadata(table, instantTime, commitActionType, metadata);
    activeTimeline.saveAsComplete(new HoodieInstant(true, commitActionType, instantTime),
        Option.of(metadata.toJsonString().getBytes(StandardCharsets.UTF_8)));
  }

  protected HoodieTable createTable(HoodieWriteConfig config, Configuration hadoopConf) {
    return createTable(config, hadoopConf, false);
  }

  protected abstract HoodieTable createTable(HoodieWriteConfig config, Configuration hadoopConf, boolean refreshTimeline);

  void emitCommitMetrics(String instantTime, HoodieCommitMetadata metadata, String actionType) {
    try {

      if (writeTimer != null) {
        long durationInMs = metrics.getDurationInMs(writeTimer.stop());
        metrics.updateCommitMetrics(HoodieActiveTimeline.parseDateFromInstantTime(instantTime).getTime(), durationInMs,
            metadata, actionType);
        writeTimer = null;
      }
    } catch (ParseException e) {
      throw new HoodieCommitException("Failed to complete commit " + config.getBasePath() + " at time " + instantTime
          + "Instant time is not of valid format", e);
    }
  }

  /**
   * Any pre-commit actions like conflict resolution goes here.
   * @param inflightInstant instant of inflight operation.
   * @param metadata commit metadata for which pre commit is being invoked.
   */
  protected void preCommit(HoodieInstant inflightInstant, HoodieCommitMetadata metadata) {
    // To be overridden by specific engines to perform conflict resolution if any.
  }

  /**
   * Write the HoodieCommitMetadata to metadata table if available.
   * @param table {@link HoodieTable} of interest.
   * @param instantTime instant time of the commit.
   * @param actionType action type of the commit.
   * @param metadata instance of {@link HoodieCommitMetadata}.
   */
  protected void writeTableMetadata(HoodieTable table, String instantTime, String actionType, HoodieCommitMetadata metadata) {
    context.setJobStatus(this.getClass().getSimpleName(), "Committing to metadata table");
    table.getMetadataWriter(instantTime).ifPresent(w -> ((HoodieTableMetadataWriter) w).update(metadata, instantTime,
        table.isTableServiceAction(actionType)));
  }

  /**
   * Filter out HoodieRecords that already exists in the output folder. This is useful in deduplication.
   *
   * @param hoodieRecords Input Hoodie records.
   * @return A subset of hoodieRecords, with existing records filtered out.
   */
  public abstract I filterExists(I hoodieRecords);

  /**
   * Main API to run bootstrap to hudi.
   */
  public void bootstrap(Option> extraMetadata) {
    // TODO : MULTIWRITER -> check if failed bootstrap files can be cleaned later
    if (config.getWriteConcurrencyMode().supportsOptimisticConcurrencyControl()) {
      throw new HoodieException("Cannot bootstrap the table in multi-writer mode");
    }
    HoodieTable table = getTableAndInitCtx(WriteOperationType.UPSERT, HoodieTimeline.METADATA_BOOTSTRAP_INSTANT_TS);
    rollbackFailedBootstrap();
    table.bootstrap(context, extraMetadata);
  }

  /**
   * Main API to rollback failed bootstrap.
   */
  public void rollbackFailedBootstrap() {
    LOG.info("Rolling back pending bootstrap if present");
    HoodieTable table = createTable(config, hadoopConf, config.isMetadataTableEnabled());
    HoodieTimeline inflightTimeline = table.getMetaClient().getCommitsTimeline().filterPendingExcludingCompaction();
    Option instant = Option.fromJavaOptional(
        inflightTimeline.getReverseOrderedInstants().map(HoodieInstant::getTimestamp).findFirst());
    if (instant.isPresent() && HoodieTimeline.compareTimestamps(instant.get(), HoodieTimeline.LESSER_THAN_OR_EQUALS,
        HoodieTimeline.FULL_BOOTSTRAP_INSTANT_TS)) {
      LOG.info("Found pending bootstrap instants. Rolling them back");
      table.rollbackBootstrap(context, HoodieActiveTimeline.createNewInstantTime());
      LOG.info("Finished rolling back pending bootstrap");
    }
  }

  /**
   * Upsert a batch of new records into Hoodie table at the supplied instantTime.
   *
   * @param records hoodieRecords to upsert
   * @param instantTime Instant time of the commit
   * @return WriteStatus to inspect errors and counts
   */
  public abstract O upsert(I records, final String instantTime);

  /**
   * Upserts the given prepared records into the Hoodie table, at the supplied instantTime.
   * 

* This implementation requires that the input records are already tagged, and de-duped if needed. * * @param preppedRecords Prepared HoodieRecords to upsert * @param instantTime Instant time of the commit * @return Collection of WriteStatus to inspect errors and counts */ public abstract O upsertPreppedRecords(I preppedRecords, final String instantTime); /** * Inserts the given HoodieRecords, into the table. This API is intended to be used for normal writes. *

* This implementation skips the index check and is able to leverage benefits such as small file handling/blocking * alignment, as with upsert(), by profiling the workload * * @param records HoodieRecords to insert * @param instantTime Instant time of the commit * @return Collection of WriteStatus to inspect errors and counts */ public abstract O insert(I records, final String instantTime); /** * Inserts the given prepared records into the Hoodie table, at the supplied instantTime. *

* This implementation skips the index check, skips de-duping and is able to leverage benefits such as small file * handling/blocking alignment, as with insert(), by profiling the workload. The prepared HoodieRecords should be * de-duped if needed. * * @param preppedRecords HoodieRecords to insert * @param instantTime Instant time of the commit * @return Collection of WriteStatus to inspect errors and counts */ public abstract O insertPreppedRecords(I preppedRecords, final String instantTime); /** * Loads the given HoodieRecords, as inserts into the table. This is suitable for doing big bulk loads into a Hoodie * table for the very first time (e.g: converting an existing table to Hoodie). *

* This implementation uses sortBy (which does range partitioning based on reservoir sampling) and attempts to control * the numbers of files with less memory compared to the {@link AbstractHoodieWriteClient#insert(I, String)} * * @param records HoodieRecords to insert * @param instantTime Instant time of the commit * @return Collection of WriteStatus to inspect errors and counts */ public abstract O bulkInsert(I records, final String instantTime); /** * Loads the given HoodieRecords, as inserts into the table. This is suitable for doing big bulk loads into a Hoodie * table for the very first time (e.g: converting an existing table to Hoodie). *

* This implementation uses sortBy (which does range partitioning based on reservoir sampling) and attempts to control * the numbers of files with less memory compared to the {@link AbstractHoodieWriteClient#insert(I, String)}. Optionally * it allows users to specify their own partitioner. If specified then it will be used for repartitioning records. See * {@link BulkInsertPartitioner}. * * @param records HoodieRecords to insert * @param instantTime Instant time of the commit * @param userDefinedBulkInsertPartitioner If specified then it will be used to partition input records before they are inserted * into hoodie. * @return Collection of WriteStatus to inspect errors and counts */ public abstract O bulkInsert(I records, final String instantTime, Option> userDefinedBulkInsertPartitioner); /** * Loads the given HoodieRecords, as inserts into the table. This is suitable for doing big bulk loads into a Hoodie * table for the very first time (e.g: converting an existing table to Hoodie). The input records should contain no * duplicates if needed. *

* This implementation uses sortBy (which does range partitioning based on reservoir sampling) and attempts to control * the numbers of files with less memory compared to the {@link AbstractHoodieWriteClient#insert(I, String)}. Optionally * it allows users to specify their own partitioner. If specified then it will be used for repartitioning records. See * {@link BulkInsertPartitioner}. * * @param preppedRecords HoodieRecords to insert * @param instantTime Instant time of the commit * @param bulkInsertPartitioner If specified then it will be used to partition input records before they are inserted * into hoodie. * @return Collection of WriteStatus to inspect errors and counts */ public abstract O bulkInsertPreppedRecords(I preppedRecords, final String instantTime, Option> bulkInsertPartitioner); /** * Deletes a list of {@link HoodieKey}s from the Hoodie table, at the supplied instantTime {@link HoodieKey}s will be * de-duped and non existent keys will be removed before deleting. * * @param keys {@link List} of {@link HoodieKey}s to be deleted * @param instantTime Commit time handle * @return Collection of WriteStatus to inspect errors and counts */ public abstract O delete(K keys, final String instantTime); /** * Common method containing steps to be performed before write (upsert/insert/... * @param instantTime * @param writeOperationType * @param metaClient */ protected void preWrite(String instantTime, WriteOperationType writeOperationType, HoodieTableMetaClient metaClient) { setOperationType(writeOperationType); this.lastCompletedTxnAndMetadata = TransactionUtils.getLastCompletedTxnInstantAndMetadata(metaClient); if (null == this.asyncCleanerService) { this.asyncCleanerService = AsyncCleanerService.startAsyncCleaningIfEnabled(this); } else { this.asyncCleanerService.start(null); } } /** * Common method containing steps to be performed after write (upsert/insert/..) operations including auto-commit. * @param result Commit Action Result * @param instantTime Instant Time * @param hoodieTable Hoodie Table * @return Write Status */ protected abstract O postWrite(HoodieWriteMetadata result, String instantTime, HoodieTable hoodieTable); /** * Post Commit Hook. Derived classes use this method to perform post-commit processing * * @param table table to commit on * @param metadata Commit Metadata corresponding to committed instant * @param instantTime Instant Time * @param extraMetadata Additional Metadata passed by user */ protected void postCommit(HoodieTable table, HoodieCommitMetadata metadata, String instantTime, Option> extraMetadata) { try { // Delete the marker directory for the instant. WriteMarkersFactory.get(config.getMarkersType(), table, instantTime) .quietDeleteMarkerDir(context, config.getMarkersDeleteParallelism()); autoCleanOnCommit(); if (config.isAutoArchive()) { archive(table); } } finally { this.heartbeatClient.stop(instantTime); } } protected void runTableServicesInline(HoodieTable table, HoodieCommitMetadata metadata, Option> extraMetadata) { if (config.inlineTableServices()) { if (config.isMetadataTableEnabled()) { table.getHoodieView().sync(); } // Do an inline compaction if enabled if (config.inlineCompactionEnabled()) { runAnyPendingCompactions(table); metadata.addMetadata(HoodieCompactionConfig.INLINE_COMPACT.key(), "true"); inlineCompact(extraMetadata); } else { metadata.addMetadata(HoodieCompactionConfig.INLINE_COMPACT.key(), "false"); } // Do an inline clustering if enabled if (config.inlineClusteringEnabled()) { runAnyPendingClustering(table); metadata.addMetadata(HoodieClusteringConfig.INLINE_CLUSTERING.key(), "true"); inlineCluster(extraMetadata); } else { metadata.addMetadata(HoodieClusteringConfig.INLINE_CLUSTERING.key(), "false"); } } } protected void runAnyPendingCompactions(HoodieTable table) { table.getActiveTimeline().getWriteTimeline().filterPendingCompactionTimeline().getInstants() .forEach(instant -> { LOG.info("Running previously failed inflight compaction at instant " + instant); compact(instant.getTimestamp(), true); }); } protected void runAnyPendingClustering(HoodieTable table) { table.getActiveTimeline().filterPendingReplaceTimeline().getInstants().forEach(instant -> { Option> instantPlan = ClusteringUtils.getClusteringPlan(table.getMetaClient(), instant); if (instantPlan.isPresent()) { LOG.info("Running pending clustering at instant " + instantPlan.get().getLeft()); cluster(instant.getTimestamp(), true); } }); } /** * Handle auto clean during commit. * */ protected void autoCleanOnCommit() { if (config.isAutoClean()) { // Call clean to cleanup if there is anything to cleanup after the commit, if (config.isAsyncClean()) { LOG.info("Cleaner has been spawned already. Waiting for it to finish"); AsyncCleanerService.waitForCompletion(asyncCleanerService); LOG.info("Cleaner has finished"); } else { // Do not reuse instantTime for clean as metadata table requires all changes to have unique instant timestamps. LOG.info("Auto cleaning is enabled. Running cleaner now"); clean(true); } } } /** * Run any pending compactions. */ public void runAnyPendingCompactions() { runAnyPendingCompactions(createTable(config, hadoopConf, config.isMetadataTableEnabled())); } /** * Create a savepoint based on the latest commit action on the timeline. * * @param user - User creating the savepoint * @param comment - Comment for the savepoint */ public void savepoint(String user, String comment) { HoodieTable table = createTable(config, hadoopConf, config.isMetadataTableEnabled()); if (table.getCompletedCommitsTimeline().empty()) { throw new HoodieSavepointException("Could not savepoint. Commit timeline is empty"); } String latestCommit = table.getCompletedCommitsTimeline().lastInstant().get().getTimestamp(); LOG.info("Savepointing latest commit " + latestCommit); savepoint(latestCommit, user, comment); } /** * Savepoint a specific commit instant time. Latest version of data files as of the passed in instantTime * will be referenced in the savepoint and will never be cleaned. The savepointed commit will never be rolledback or archived. *

* This gives an option to rollback the state to the savepoint anytime. Savepoint needs to be manually created and * deleted. *

* Savepoint should be on a commit that could not have been cleaned. * * @param instantTime - commit that should be savepointed * @param user - User creating the savepoint * @param comment - Comment for the savepoint */ public void savepoint(String instantTime, String user, String comment) { HoodieTable table = createTable(config, hadoopConf, config.isMetadataTableEnabled()); table.savepoint(context, instantTime, user, comment); } /** * Delete a savepoint that was created. Once the savepoint is deleted, the commit can be rolledback and cleaner may * clean up data files. * * @param savepointTime - delete the savepoint * @return true if the savepoint was deleted successfully */ public void deleteSavepoint(String savepointTime) { HoodieTable table = createTable(config, hadoopConf, config.isMetadataTableEnabled()); SavepointHelpers.deleteSavepoint(table, savepointTime); } /** * Restore the data to the savepoint. * * WARNING: This rolls back recent commits and deleted data files and also pending compactions after savepoint time. * Queries accessing the files will mostly fail. This is expected to be a manual operation and no concurrent write or * compaction is expected to be running * * @param savepointTime - savepoint time to rollback to * @return true if the savepoint was restored to successfully */ public void restoreToSavepoint(String savepointTime) { HoodieTable table = createTable(config, hadoopConf, config.isMetadataTableEnabled()); SavepointHelpers.validateSavepointPresence(table, savepointTime); restoreToInstant(savepointTime); SavepointHelpers.validateSavepointRestore(table, savepointTime); } @Deprecated public boolean rollback(final String commitInstantTime) throws HoodieRollbackException { HoodieTable table = createTable(config, hadoopConf); Option pendingRollbackInfo = getPendingRollbackInfo(table.getMetaClient(), commitInstantTime); return rollback(commitInstantTime, pendingRollbackInfo, false); } /** * @Deprecated * Rollback the inflight record changes with the given commit time. This * will be removed in future in favor of {@link AbstractHoodieWriteClient#restoreToInstant(String)} * Adding this api for backwards compatability. * @param commitInstantTime Instant time of the commit * @param skipLocking if this is triggered by another parent transaction, locking can be skipped. * @throws HoodieRollbackException if rollback cannot be performed successfully */ @Deprecated public boolean rollback(final String commitInstantTime, boolean skipLocking) throws HoodieRollbackException { return rollback(commitInstantTime, Option.empty(), skipLocking); } /** * @Deprecated * Rollback the inflight record changes with the given commit time. This * will be removed in future in favor of {@link AbstractHoodieWriteClient#restoreToInstant(String)} * * @param commitInstantTime Instant time of the commit * @param pendingRollbackInfo pending rollback instant and plan if rollback failed from previous attempt. * @param skipLocking if this is triggered by another parent transaction, locking can be skipped. * @throws HoodieRollbackException if rollback cannot be performed successfully */ @Deprecated public boolean rollback(final String commitInstantTime, Option pendingRollbackInfo, boolean skipLocking) throws HoodieRollbackException { LOG.info("Begin rollback of instant " + commitInstantTime); final String rollbackInstantTime = pendingRollbackInfo.map(entry -> entry.getRollbackInstant().getTimestamp()).orElse(HoodieActiveTimeline.createNewInstantTime()); final Timer.Context timerContext = this.metrics.getRollbackCtx(); try { HoodieTable table = createTable(config, hadoopConf); Option commitInstantOpt = Option.fromJavaOptional(table.getActiveTimeline().getCommitsTimeline().getInstants() .filter(instant -> HoodieActiveTimeline.EQUALS.test(instant.getTimestamp(), commitInstantTime)) .findFirst()); if (commitInstantOpt.isPresent()) { LOG.info("Scheduling Rollback at instant time :" + rollbackInstantTime); Option rollbackPlanOption = pendingRollbackInfo.map(entry -> Option.of(entry.getRollbackPlan())).orElse(table.scheduleRollback(context, rollbackInstantTime, commitInstantOpt.get(), false, config.shouldRollbackUsingMarkers())); if (rollbackPlanOption.isPresent()) { // execute rollback HoodieRollbackMetadata rollbackMetadata = table.rollback(context, rollbackInstantTime, commitInstantOpt.get(), true, skipLocking); if (timerContext != null) { long durationInMs = metrics.getDurationInMs(timerContext.stop()); metrics.updateRollbackMetrics(durationInMs, rollbackMetadata.getTotalFilesDeleted()); } return true; } else { throw new HoodieRollbackException("Failed to rollback " + config.getBasePath() + " commits " + commitInstantTime); } } else { LOG.warn("Cannot find instant " + commitInstantTime + " in the timeline, for rollback"); return false; } } catch (Exception e) { throw new HoodieRollbackException("Failed to rollback " + config.getBasePath() + " commits " + commitInstantTime, e); } } /** * NOTE : This action requires all writers (ingest and compact) to a table to be stopped before proceeding. Revert * the (inflight/committed) record changes for all commits after the provided instant time. * * @param instantTime Instant time to which restoration is requested */ public HoodieRestoreMetadata restoreToInstant(final String instantTime) throws HoodieRestoreException { LOG.info("Begin restore to instant " + instantTime); final String restoreInstantTime = HoodieActiveTimeline.createNewInstantTime(); Timer.Context timerContext = metrics.getRollbackCtx(); try { HoodieTable table = createTable(config, hadoopConf, config.isMetadataTableEnabled()); HoodieRestoreMetadata restoreMetadata = table.restore(context, restoreInstantTime, instantTime); if (timerContext != null) { final long durationInMs = metrics.getDurationInMs(timerContext.stop()); final long totalFilesDeleted = restoreMetadata.getHoodieRestoreMetadata().values().stream() .flatMap(Collection::stream) .mapToLong(HoodieRollbackMetadata::getTotalFilesDeleted) .sum(); metrics.updateRollbackMetrics(durationInMs, totalFilesDeleted); } return restoreMetadata; } catch (Exception e) { throw new HoodieRestoreException("Failed to restore to " + instantTime, e); } } /** * Clean up any stale/old files/data lying around (either on file storage or index storage) based on the * configurations and CleaningPolicy used. (typically files that no longer can be used by a running query can be * cleaned) */ public HoodieCleanMetadata clean(String cleanInstantTime) throws HoodieIOException { return clean(cleanInstantTime, true, false); } /** * Clean up any stale/old files/data lying around (either on file storage or index storage) based on the * configurations and CleaningPolicy used. (typically files that no longer can be used by a running query can be * cleaned) * @param cleanInstantTime instant time for clean. * @param skipLocking if this is triggered by another parent transaction, locking can be skipped. * @return instance of {@link HoodieCleanMetadata}. */ public HoodieCleanMetadata clean(String cleanInstantTime, boolean skipLocking) throws HoodieIOException { return clean(cleanInstantTime, true, skipLocking); } /** * Clean up any stale/old files/data lying around (either on file storage or index storage) based on the * configurations and CleaningPolicy used. (typically files that no longer can be used by a running query can be * cleaned). This API provides the flexibility to schedule clean instant asynchronously via * {@link AbstractHoodieWriteClient#scheduleTableService(String, Option, TableServiceType)} and disable inline scheduling * of clean. * @param cleanInstantTime instant time for clean. * @param scheduleInline true if needs to be scheduled inline. false otherwise. * @param skipLocking if this is triggered by another parent transaction, locking can be skipped. */ public HoodieCleanMetadata clean(String cleanInstantTime, boolean scheduleInline, boolean skipLocking) throws HoodieIOException { if (scheduleInline) { scheduleTableServiceInternal(cleanInstantTime, Option.empty(), TableServiceType.CLEAN); } LOG.info("Cleaner started"); final Timer.Context timerContext = metrics.getCleanCtx(); LOG.info("Cleaned failed attempts if any"); CleanerUtils.rollbackFailedWrites(config.getFailedWritesCleanPolicy(), HoodieTimeline.CLEAN_ACTION, () -> rollbackFailedWrites(skipLocking)); HoodieCleanMetadata metadata = createTable(config, hadoopConf).clean(context, cleanInstantTime, skipLocking); if (timerContext != null && metadata != null) { long durationMs = metrics.getDurationInMs(timerContext.stop()); metrics.updateCleanMetrics(durationMs, metadata.getTotalFilesDeleted()); LOG.info("Cleaned " + metadata.getTotalFilesDeleted() + " files" + " Earliest Retained Instant :" + metadata.getEarliestCommitToRetain() + " cleanerElapsedMs" + durationMs); } return metadata; } public HoodieCleanMetadata clean() { return clean(false); } /** * Triggers clean for the table. This refers to Clean up any stale/old files/data lying around (either on file storage or index storage) based on the * * configurations and CleaningPolicy used. * @param skipLocking if this is triggered by another parent transaction, locking can be skipped. * @return instance of {@link HoodieCleanMetadata}. */ public HoodieCleanMetadata clean(boolean skipLocking) { return clean(HoodieActiveTimeline.createNewInstantTime(), skipLocking); } /** * Trigger archival for the table. This ensures that the number of commits do not explode * and keep increasing unbounded over time. * @param table table to commit on. */ protected void archive(HoodieTable table) { try { // We cannot have unbounded commit files. Archive commits if we have to archive HoodieTimelineArchiveLog archiveLog = new HoodieTimelineArchiveLog(config, table); archiveLog.archiveIfRequired(context); } catch (IOException ioe) { throw new HoodieIOException("Failed to archive", ioe); } } /** * Trigger archival for the table. This ensures that the number of commits do not explode * and keep increasing unbounded over time. */ public void archive() { // Create a Hoodie table which encapsulated the commits and files visible HoodieTable table = createTable(config, hadoopConf); archive(table); } /** * Provides a new commit time for a write operation (insert/update/delete). */ public String startCommit() { CleanerUtils.rollbackFailedWrites(config.getFailedWritesCleanPolicy(), HoodieTimeline.COMMIT_ACTION, () -> rollbackFailedWrites()); String instantTime = HoodieActiveTimeline.createNewInstantTime(); HoodieTableMetaClient metaClient = createMetaClient(true); startCommit(instantTime, metaClient.getCommitActionType(), metaClient); return instantTime; } /** * Provides a new commit time for a write operation (insert/update/delete/insert_overwrite/insert_overwrite_table) without specified action. * @param instantTime Instant time to be generated */ public void startCommitWithTime(String instantTime) { HoodieTableMetaClient metaClient = createMetaClient(true); startCommitWithTime(instantTime, metaClient.getCommitActionType(), metaClient); } /** * Completes a new commit time for a write operation (insert/update/delete/insert_overwrite/insert_overwrite_table) with specified action. */ public void startCommitWithTime(String instantTime, String actionType) { HoodieTableMetaClient metaClient = createMetaClient(true); startCommitWithTime(instantTime, actionType, metaClient); } /** * Completes a new commit time for a write operation (insert/update/delete) with specified action. */ private void startCommitWithTime(String instantTime, String actionType, HoodieTableMetaClient metaClient) { CleanerUtils.rollbackFailedWrites(config.getFailedWritesCleanPolicy(), HoodieTimeline.COMMIT_ACTION, () -> rollbackFailedWrites()); startCommit(instantTime, actionType, metaClient); } private void startCommit(String instantTime, String actionType, HoodieTableMetaClient metaClient) { LOG.info("Generate a new instant time: " + instantTime + " action: " + actionType); // if there are pending compactions, their instantTime must not be greater than that of this instant time metaClient.getActiveTimeline().filterPendingCompactionTimeline().lastInstant().ifPresent(latestPending -> ValidationUtils.checkArgument( HoodieTimeline.compareTimestamps(latestPending.getTimestamp(), HoodieTimeline.LESSER_THAN, instantTime), "Latest pending compaction instant time must be earlier than this instant time. Latest Compaction :" + latestPending + ", Ingesting at " + instantTime)); if (config.getFailedWritesCleanPolicy().isLazy()) { this.heartbeatClient.start(instantTime); } metaClient.getActiveTimeline().createNewInstant(new HoodieInstant(HoodieInstant.State.REQUESTED, actionType, instantTime)); } /** * Schedules a new compaction instant. * @param extraMetadata Extra Metadata to be stored */ public Option scheduleCompaction(Option> extraMetadata) throws HoodieIOException { String instantTime = HoodieActiveTimeline.createNewInstantTime(); return scheduleCompactionAtInstant(instantTime, extraMetadata) ? Option.of(instantTime) : Option.empty(); } /** * Schedules a new compaction instant with passed-in instant time. * @param instantTime Compaction Instant Time * @param extraMetadata Extra Metadata to be stored */ public boolean scheduleCompactionAtInstant(String instantTime, Option> extraMetadata) throws HoodieIOException { return scheduleTableService(instantTime, extraMetadata, TableServiceType.COMPACT).isPresent(); } /** * Performs Compaction for the workload stored in instant-time. * * @param compactionInstantTime Compaction Instant Time * @return Collection of WriteStatus to inspect errors and counts */ public O compact(String compactionInstantTime) { return compact(compactionInstantTime, config.shouldAutoCommit()); } /** * Commit a compaction operation. Allow passing additional meta-data to be stored in commit instant file. * * @param compactionInstantTime Compaction Instant Time * @param writeStatuses Collection of WriteStatus to inspect errors and counts * @param extraMetadata Extra Metadata to be stored */ public abstract void commitCompaction(String compactionInstantTime, O writeStatuses, Option> extraMetadata) throws IOException; /** * Commit Compaction and track metrics. */ protected abstract void completeCompaction(HoodieCommitMetadata metadata, O writeStatuses, HoodieTable table, String compactionCommitTime); /** * Get inflight time line exclude compaction and clustering. * @param metaClient * @return */ private HoodieTimeline getInflightTimelineExcludeCompactionAndClustering(HoodieTableMetaClient metaClient) { HoodieTimeline inflightTimelineWithReplaceCommit = metaClient.getCommitsTimeline().filterPendingExcludingCompaction(); HoodieTimeline inflightTimelineExcludeClusteringCommit = inflightTimelineWithReplaceCommit.filter(instant -> { if (instant.getAction().equals(HoodieTimeline.REPLACE_COMMIT_ACTION)) { Option> instantPlan = ClusteringUtils.getClusteringPlan(metaClient, instant); return !instantPlan.isPresent(); } else { return true; } }); return inflightTimelineExcludeClusteringCommit; } private Option getPendingRollbackInfo(HoodieTableMetaClient metaClient, String commitToRollback) { return getPendingRollbackInfos(metaClient).getOrDefault(commitToRollback, Option.empty()); } /** * Fetch map of pending commits to be rolled-back to {@link HoodiePendingRollbackInfo}. * @param metaClient instance of {@link HoodieTableMetaClient} to use. * @return map of pending commits to be rolled-back instants to Rollback Instant and Rollback plan Pair. */ protected Map> getPendingRollbackInfos(HoodieTableMetaClient metaClient) { List instants = metaClient.getActiveTimeline().filterPendingRollbackTimeline().getInstants().collect(Collectors.toList()); Map> infoMap = new HashMap<>(); for (HoodieInstant instant : instants) { try { HoodieRollbackPlan rollbackPlan = RollbackUtils.getRollbackPlan(metaClient, instant); String action = rollbackPlan.getInstantToRollback().getAction(); if (!HoodieTimeline.COMPACTION_ACTION.equals(action)) { boolean isClustering = HoodieTimeline.REPLACE_COMMIT_ACTION.equals(action) && ClusteringUtils.getClusteringPlan(metaClient, instant).isPresent(); if (!isClustering) { String instantToRollback = rollbackPlan.getInstantToRollback().getCommitTime(); infoMap.putIfAbsent(instantToRollback, Option.of(new HoodiePendingRollbackInfo(instant, rollbackPlan))); } } } catch (IOException e) { LOG.warn("Fetching rollback plan failed for " + infoMap + ", skip the plan", e); } } return infoMap; } /** * Rollback all failed writes. */ public Boolean rollbackFailedWrites() { return rollbackFailedWrites(false); } /** * Rollback all failed writes. * @param skipLocking if this is triggered by another parent transaction, locking can be skipped. */ public Boolean rollbackFailedWrites(boolean skipLocking) { HoodieTable table = createTable(config, hadoopConf); List instantsToRollback = getInstantsToRollback(table.getMetaClient(), config.getFailedWritesCleanPolicy(), Option.empty()); Map> pendingRollbacks = getPendingRollbackInfos(table.getMetaClient()); instantsToRollback.forEach(entry -> pendingRollbacks.putIfAbsent(entry, Option.empty())); rollbackFailedWrites(pendingRollbacks, skipLocking); return true; } protected void rollbackFailedWrites(Map> instantsToRollback, boolean skipLocking) { // sort in reverse order of commit times LinkedHashMap> reverseSortedRollbackInstants = instantsToRollback.entrySet() .stream().sorted((i1, i2) -> i2.getKey().compareTo(i1.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); for (Map.Entry> entry : reverseSortedRollbackInstants.entrySet()) { if (HoodieTimeline.compareTimestamps(entry.getKey(), HoodieTimeline.LESSER_THAN_OR_EQUALS, HoodieTimeline.FULL_BOOTSTRAP_INSTANT_TS)) { // do we need to handle failed rollback of a bootstrap rollbackFailedBootstrap(); HeartbeatUtils.deleteHeartbeatFile(fs, basePath, entry.getKey(), config); break; } else { rollback(entry.getKey(), entry.getValue(), skipLocking); HeartbeatUtils.deleteHeartbeatFile(fs, basePath, entry.getKey(), config); } } } protected List getInstantsToRollback(HoodieTableMetaClient metaClient, HoodieFailedWritesCleaningPolicy cleaningPolicy, Option curInstantTime) { Stream inflightInstantsStream = getInflightTimelineExcludeCompactionAndClustering(metaClient) .getReverseOrderedInstants(); if (cleaningPolicy.isEager()) { return inflightInstantsStream.map(HoodieInstant::getTimestamp).filter(entry -> { if (curInstantTime.isPresent()) { return !entry.equals(curInstantTime.get()); } else { return true; } }).collect(Collectors.toList()); } else if (cleaningPolicy.isLazy()) { return inflightInstantsStream.filter(instant -> { try { return heartbeatClient.isHeartbeatExpired(instant.getTimestamp()); } catch (IOException io) { throw new HoodieException("Failed to check heartbeat for instant " + instant, io); } }).map(HoodieInstant::getTimestamp).collect(Collectors.toList()); } else if (cleaningPolicy.isNever()) { return Collections.EMPTY_LIST; } else { throw new IllegalArgumentException("Invalid Failed Writes Cleaning Policy " + config.getFailedWritesCleanPolicy()); } } /** * Ensures compaction instant is in expected state and performs Compaction for the workload stored in instant-time. * * @param compactionInstantTime Compaction Instant Time * @return Collection of Write Status */ protected abstract O compact(String compactionInstantTime, boolean shouldComplete); /** * Performs a compaction operation on a table, serially before or after an insert/upsert action. */ protected Option inlineCompact(Option> extraMetadata) { Option compactionInstantTimeOpt = scheduleCompaction(extraMetadata); compactionInstantTimeOpt.ifPresent(compactInstantTime -> { // inline compaction should auto commit as the user is never given control compact(compactInstantTime, true); }); return compactionInstantTimeOpt; } /** * Schedules a new clustering instant. * @param extraMetadata Extra Metadata to be stored */ public Option scheduleClustering(Option> extraMetadata) throws HoodieIOException { String instantTime = HoodieActiveTimeline.createNewInstantTime(); return scheduleClusteringAtInstant(instantTime, extraMetadata) ? Option.of(instantTime) : Option.empty(); } /** * Schedules a new clustering instant with passed-in instant time. * @param instantTime clustering Instant Time * @param extraMetadata Extra Metadata to be stored */ public boolean scheduleClusteringAtInstant(String instantTime, Option> extraMetadata) throws HoodieIOException { return scheduleTableService(instantTime, extraMetadata, TableServiceType.CLUSTER).isPresent(); } /** * Schedules a new cleaning instant. * @param extraMetadata Extra Metadata to be stored */ protected Option scheduleCleaning(Option> extraMetadata) throws HoodieIOException { String instantTime = HoodieActiveTimeline.createNewInstantTime(); return scheduleCleaningAtInstant(instantTime, extraMetadata) ? Option.of(instantTime) : Option.empty(); } /** * Schedules a new cleaning instant with passed-in instant time. * @param instantTime cleaning Instant Time * @param extraMetadata Extra Metadata to be stored */ protected boolean scheduleCleaningAtInstant(String instantTime, Option> extraMetadata) throws HoodieIOException { return scheduleTableService(instantTime, extraMetadata, TableServiceType.CLEAN).isPresent(); } /** * Ensures clustering instant is in expected state and performs clustering for the plan stored in metadata. * @param clusteringInstant Clustering Instant Time * @return Collection of Write Status */ public abstract HoodieWriteMetadata cluster(String clusteringInstant, boolean shouldComplete); /** * Schedule table services such as clustering, compaction & cleaning. * * @param extraMetadata Metadata to pass onto the scheduled service instant * @param tableServiceType Type of table service to schedule * @return */ public Option scheduleTableService(Option> extraMetadata, TableServiceType tableServiceType) { String instantTime = HoodieActiveTimeline.createNewInstantTime(); return scheduleTableService(instantTime, extraMetadata, tableServiceType); } /** * Schedule table services such as clustering, compaction & cleaning. * * @param extraMetadata Metadata to pass onto the scheduled service instant * @param tableServiceType Type of table service to schedule * @return */ public Option scheduleTableService(String instantTime, Option> extraMetadata, TableServiceType tableServiceType) { // A lock is required to guard against race conditions between an on-going writer and scheduling a table service. final Option inflightInstant = Option.of(new HoodieInstant(HoodieInstant.State.REQUESTED, tableServiceType.getAction(), instantTime)); try { this.txnManager.beginTransaction(inflightInstant, Option.empty()); LOG.info("Scheduling table service " + tableServiceType); return scheduleTableServiceInternal(instantTime, extraMetadata, tableServiceType); } finally { this.txnManager.endTransaction(inflightInstant); } } private Option scheduleTableServiceInternal(String instantTime, Option> extraMetadata, TableServiceType tableServiceType) { switch (tableServiceType) { case CLUSTER: LOG.info("Scheduling clustering at instant time :" + instantTime); Option clusteringPlan = createTable(config, hadoopConf, config.isMetadataTableEnabled()) .scheduleClustering(context, instantTime, extraMetadata); return clusteringPlan.isPresent() ? Option.of(instantTime) : Option.empty(); case COMPACT: LOG.info("Scheduling compaction at instant time :" + instantTime); Option compactionPlan = createTable(config, hadoopConf, config.isMetadataTableEnabled()) .scheduleCompaction(context, instantTime, extraMetadata); return compactionPlan.isPresent() ? Option.of(instantTime) : Option.empty(); case CLEAN: LOG.info("Scheduling cleaning at instant time :" + instantTime); Option cleanerPlan = createTable(config, hadoopConf, config.isMetadataTableEnabled()) .scheduleCleaning(context, instantTime, extraMetadata); return cleanerPlan.isPresent() ? Option.of(instantTime) : Option.empty(); default: throw new IllegalArgumentException("Invalid TableService " + tableServiceType); } } /** * Executes a clustering plan on a table, serially before or after an insert/upsert action. */ protected Option inlineCluster(Option> extraMetadata) { Option clusteringInstantOpt = scheduleClustering(extraMetadata); clusteringInstantOpt.ifPresent(clusteringInstant -> { // inline cluster should auto commit as the user is never given control cluster(clusteringInstant, true); }); return clusteringInstantOpt; } protected void rollbackInflightClustering(HoodieInstant inflightInstant, HoodieTable table) { String commitTime = HoodieActiveTimeline.createNewInstantTime(); table.scheduleRollback(context, commitTime, inflightInstant, false, config.shouldRollbackUsingMarkers()); table.rollback(context, commitTime, inflightInstant, false, false); table.getActiveTimeline().revertReplaceCommitInflightToRequested(inflightInstant); } /** * Finalize Write operation. * * @param table HoodieTable * @param instantTime Instant Time * @param stats Hoodie Write Stat */ protected void finalizeWrite(HoodieTable table, String instantTime, List stats) { try { final Timer.Context finalizeCtx = metrics.getFinalizeCtx(); table.finalizeWrite(context, instantTime, stats); if (finalizeCtx != null) { Option durationInMs = Option.of(metrics.getDurationInMs(finalizeCtx.stop())); durationInMs.ifPresent(duration -> { LOG.info("Finalize write elapsed time (milliseconds): " + duration); metrics.updateFinalizeWriteMetrics(duration, stats.size()); }); } } catch (HoodieIOException ioe) { throw new HoodieCommitException("Failed to complete commit " + instantTime + " due to finalize errors.", ioe); } } public HoodieMetrics getMetrics() { return metrics; } public HoodieIndex getIndex() { return index; } /** * Get HoodieTable and init {@link Timer.Context}. * * @param operationType write operation type * @param instantTime current inflight instant time * @return HoodieTable */ protected abstract HoodieTable getTableAndInitCtx(WriteOperationType operationType, String instantTime); /** * Sets write schema from last instant since deletes may not have schema set in the config. */ protected void setWriteSchemaForDeletes(HoodieTableMetaClient metaClient) { try { HoodieActiveTimeline activeTimeline = metaClient.getActiveTimeline(); Option lastInstant = activeTimeline.filterCompletedInstants().filter(s -> s.getAction().equals(metaClient.getCommitActionType()) || s.getAction().equals(HoodieActiveTimeline.REPLACE_COMMIT_ACTION)) .lastInstant(); if (lastInstant.isPresent()) { HoodieCommitMetadata commitMetadata = HoodieCommitMetadata.fromBytes( activeTimeline.getInstantDetails(lastInstant.get()).get(), HoodieCommitMetadata.class); if (commitMetadata.getExtraMetadata().containsKey(HoodieCommitMetadata.SCHEMA_KEY)) { config.setSchema(commitMetadata.getExtraMetadata().get(HoodieCommitMetadata.SCHEMA_KEY)); } else { throw new HoodieIOException("Latest commit does not have any schema in commit metadata"); } } else { throw new HoodieIOException("Deletes issued without any prior commits"); } } catch (IOException e) { throw new HoodieIOException("IOException thrown while reading last commit metadata", e); } } /** * Called after each write, to release any resources used. */ protected void releaseResources() { // do nothing here } @Override public void close() { // release AsyncCleanerService AsyncCleanerService.forceShutdown(asyncCleanerService); asyncCleanerService = null; // Stop timeline-server if running super.close(); // Calling this here releases any resources used by your index, so make sure to finish any related operations // before this point this.index.close(); this.heartbeatClient.stop(); this.txnManager.close(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy