com.scalar.db.util.groupcommit.GroupCommitter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scalardb Show documentation
Show all versions of scalardb Show documentation
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);
}
}