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

com.scalar.db.util.groupcommit.GroupCommitter Maven / Gradle / Ivy

Go to download

A universal transaction manager that achieves database-agnostic transactions and distributed transactions that span multiple databases

The newest version!
package com.scalar.db.util.groupcommit;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.Uninterruptibles;
import com.scalar.db.util.groupcommit.KeyManipulator.Keys;
import java.io.Closeable;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A group committer which is responsible for the group and slot managements and emits ready groups.
 * This class receives 4 generic types as keys. But it's likely only one or a few key types are
 * enough. The reason to use the 4 keys is to detect wrong key usages at compile time.
 *
 * @param  A key type to NormalGroup which contains multiple slots and is
 *     group-committed.
 * @param  A key type to slot in NormalGroup which can contain a value ready to commit.
 * @param  A key type to DelayedGroup which contains a single slot and is
 *     singly-committed.
 * @param  A parent-key type that Emitter can interpret.
 * @param  A full-key type that Emitter can interpret.
 * @param  A value type to be set to a slot.
 */
@ThreadSafe
public class GroupCommitter
    implements Closeable {
  private static final Logger logger = LoggerFactory.getLogger(GroupCommitter.class);

  // Background workers
  private final GroupSizeFixWorker<
          PARENT_KEY, CHILD_KEY, FULL_KEY, EMIT_PARENT_KEY, EMIT_FULL_KEY, V>
      groupSizeFixWorker;
  private final DelayedSlotMoveWorker<
          PARENT_KEY, CHILD_KEY, FULL_KEY, EMIT_PARENT_KEY, EMIT_FULL_KEY, V>
      delayedSlotMoveWorker;
  private final GroupCleanupWorker<
          PARENT_KEY, CHILD_KEY, FULL_KEY, EMIT_PARENT_KEY, EMIT_FULL_KEY, V>
      groupCleanupWorker;

  // Monitor
  @Nullable private final GroupCommitMonitor groupCommitMonitor;

  // This contains logics of how to treat keys.
  private final KeyManipulator
      keyManipulator;

  private final GroupManager
      groupManager;

  private final AtomicBoolean closing = new AtomicBoolean();

  /**
   * @param label A label used for thread name.
   * @param config A configuration.
   * @param keyManipulator A key manipulator that contains logics how to treat keys.
   */
  public GroupCommitter(
      String label,
      GroupCommitConfig config,
      KeyManipulator
          keyManipulator) {
    logger.info("Starting GroupCommitter. Label: {}, Config: {}", label, config);
    this.keyManipulator = keyManipulator;
    this.groupManager = createGroupManager(config, keyManipulator);
    this.groupCleanupWorker = createGroupCleanupWorker(label, config, groupManager);
    this.delayedSlotMoveWorker =
        createDelayedSlotMoveWorker(label, config, groupManager, groupCleanupWorker);
    this.groupSizeFixWorker =
        createGroupSizeFixWorker(
            label, config, groupManager, delayedSlotMoveWorker, groupCleanupWorker);

    if (config.metricsMonitorLogEnabled()) {
      this.groupCommitMonitor = createGroupCommitMonitor(label);
    } else {
      this.groupCommitMonitor = null;
    }
  }

  GroupCommitMetrics getMetrics() {
    return new GroupCommitMetrics(
        groupSizeFixWorker.size(),
        delayedSlotMoveWorker.size(),
        groupCleanupWorker.size(),
        groupManager.sizeOfNormalGroupMap(),
        groupManager.sizeOfDelayedGroupMap());
  }

  /**
   * Set an emitter which contains implementation to emit values. Ideally, this should be passed to
   * the constructor, but the instantiation timings of {@link GroupCommitter} and {@link Emittable}
   * can be different. That's why this API exists.
   *
   * @param emitter An emitter.
   */
  public void setEmitter(Emittable emitter) {
    groupManager.setEmitter(emitter);
  }

  /**
   * Reserves a new slot in the current {@link NormalGroup}. The slot may be moved to a {@link
   * DelayedGroup} later.
   *
   * @param childKey A child key.
   * @return The full key associated with the reserved slot.
   */
  public FULL_KEY reserve(CHILD_KEY childKey) {
    if (closing.get()) {
      throw new GroupCommitException(
          String.format(
              "Reserving a new slot isn't allowed since already closed. Child key: %s", childKey));
    }

    while (true) {
      FULL_KEY fullKey = groupManager.reserveNewSlot(childKey);
      if (fullKey != null) {
        return fullKey;
      }
      logger.debug(
          "Failed to reserve a new value slot since the group was already size-fixed. Retrying. Key: {}",
          childKey);
    }
  }

  /**
   * Marks the slot associated with the specified key READY and then, waits until the group which
   * contains the slot is emitted.
   *
   * @param fullKey A full key associated with the slot already reserved with {@link
   *     GroupCommitter#reserve}.
   * @param value A value to be set to the slot. It will be committed with other values contained in
   *     slots of the same group.
   * @throws GroupCommitException when group commit fails
   */
  public void ready(FULL_KEY fullKey, V value) throws GroupCommitException {
    Keys keys = keyManipulator.keysFromFullKey(fullKey);
    boolean failed = false;
    while (true) {
      Group group =
          groupManager.getGroup(keys);
      if (group.putValueToSlotAndWait(keys.childKey, value)) {
        return;
      }
      // Failing to put a value to the slot can happen only when the slot is moved from the original
      // NormalGroup to a new DelayedGroup. So, only a single retry must be enough.
      if (failed) {
        throw new GroupCommitConflictException(
            String.format(
                "Failed to put a value to the slot. The slot might be already removed before this operation. Group: %s, Full key: %s, Value: %s",
                group, fullKey, value));
      }
      failed = true;
      logger.debug(
          "The state of the group has been changed. Retrying. Group: {}, Keys: {}", group, keys);
    }
  }

  /**
   * Removes the slot from the group. This method needs to be called if the group commit for the key
   * is canceled and {@link GroupCommitter#ready} won't be called for it.
   *
   * @param fullKey A full key to specify the slot.
   */
  public void remove(FULL_KEY fullKey) {
    Keys keys = keyManipulator.keysFromFullKey(fullKey);
    if (!groupManager.removeSlotFromGroup(keys)) {
      logger.debug(
          "Failed to remove the slot. Slots in a group that is already done can be automatically removed. Full key: {}",
          fullKey);
    }
  }

  /**
   * Closes the resources. The ExecutorServices are created as daemon, so calling this method isn't
   * needed. But for testing, this should be called for resources.
   */
  @Override
  public void close() {
    logger.info("Closing GroupCommitter");
    closing.set(true);
    long loggingIntervalMillis = 10000;
    Instant nextLoggingAt = Instant.now().plusMillis(loggingIntervalMillis);
    do {
      Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS);
      GroupCommitMetrics groupCommitMetrics = getMetrics();
      if (!groupCommitMetrics.hasRemaining()) {
        logger.info("No ongoing group remains. Closing all the resources");
        if (groupCommitMonitor != null) {
          groupCommitMonitor.close();
        }
        groupSizeFixWorker.close();
        delayedSlotMoveWorker.close();
        groupCleanupWorker.close();
        break;
      }
      Instant now = Instant.now();
      if (nextLoggingAt.isBefore(now)) {
        logger.info("Ongoing slot still remains. Metrics: {}", groupCommitMetrics);
        nextLoggingAt = Instant.now().plusMillis(loggingIntervalMillis);
      }
    } while (true);
  }

  @VisibleForTesting
  GroupManager
      createGroupManager(
          GroupCommitConfig config,
          KeyManipulator
              keyManipulator) {
    return new GroupManager<>(config, keyManipulator);
  }

  @VisibleForTesting
  GroupCleanupWorker
      createGroupCleanupWorker(
          String label,
          GroupCommitConfig config,
          GroupManager
              groupManager) {
    GroupCleanupWorker worker =
        new GroupCleanupWorker<>(label, config.timeoutCheckIntervalMillis(), groupManager);
    groupManager.setGroupCleanupWorker(worker);
    return worker;
  }

  @VisibleForTesting
  DelayedSlotMoveWorker
      createDelayedSlotMoveWorker(
          String label,
          GroupCommitConfig config,
          GroupManager
              groupManager,
          GroupCleanupWorker
              groupCleanupWorker) {
    return new DelayedSlotMoveWorker<>(
        label, config.timeoutCheckIntervalMillis(), groupManager, groupCleanupWorker);
  }

  @VisibleForTesting
  GroupSizeFixWorker
      createGroupSizeFixWorker(
          String label,
          GroupCommitConfig config,
          GroupManager
              groupManager,
          DelayedSlotMoveWorker
              delayedSlotMoveWorker,
          GroupCleanupWorker
              groupCleanupWorker) {
    GroupSizeFixWorker worker =
        new GroupSizeFixWorker<>(
            label, config.timeoutCheckIntervalMillis(), delayedSlotMoveWorker, groupCleanupWorker);
    groupManager.setGroupSizeFixWorker(worker);
    return worker;
  }

  @VisibleForTesting
  GroupCommitMonitor createGroupCommitMonitor(String label) {
    return new GroupCommitMonitor(label, this::getMetrics);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy