/*
* ToroDB
* Copyright © 2014 8Kdata Technology (www.8kdata.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.torodb.mongodb.repl.topology;
import static com.torodb.common.util.ListeningFutureToCompletableFuture.toCompletableFuture;
import com.google.common.util.concurrent.ListenableScheduledFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.torodb.core.concurrent.ConcurrentToolsFactory;
import com.torodb.mongodb.commands.pojos.ReplicaSetConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.time.Duration;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Singleton;
/**
* This class wrappes the single thread {@link ListeningScheduledExecutorService} that controls the
* access to {@link TopologyCoordinator} methods.
*/
@Singleton
public class TopologyExecutor {
private static final Logger LOGGER = LogManager.getLogger(TopologyExecutor.class);
private final TopologyCoordinator coord;
private final ListeningScheduledExecutorService executor;
private final OnAnyVersion onAnyVersion;
private final OnCurrentVersion onCurrentVersion;
private final VersionChangeListener versionChangeListener;
private final Set<VersionChangeListener> versionListeners = Collections.newSetFromMap(
new WeakHashMap<>());
private volatile int version = -1;
public TopologyExecutor(ConcurrentToolsFactory concurrentToolsFactory,
Duration maxSyncSourceLag, Duration slaveDelay) {
this.executor = MoreExecutors.listeningDecorator(
concurrentToolsFactory.createScheduledExecutorServiceWithMaxThreads("topology-executor", 1)
);
this.coord = new TopologyCoordinator(maxSyncSourceLag, slaveDelay);
this.versionChangeListener = this::onVersionChange;
this.coord.addVersionChangeListener(versionChangeListener);
this.onAnyVersion = new OnAnyVersion(executor, coord);
this.onCurrentVersion = new OnCurrentVersion(executor, coord, this::getVersion);
}
private int getVersion() {
return version;
}
void addVersionChangeListener(VersionChangeListener listener) {
versionListeners.add(listener);
}
@GuardedBy("executor")
private void onVersionChange(TopologyCoordinator coord, ReplicaSetConfig oldConfig) {
LOGGER.debug("Changing version from {} to {}", version,
coord.getRsConfig().getConfigVersion());
version = coord.getRsConfig().getConfigVersion();
versionListeners.forEach(listener -> listener.onVersionChange(coord, oldConfig));
}
VersionExecutor onAnyVersion() {
return onAnyVersion;
}
VersionExecutor onCurrentVersion() {
return onCurrentVersion;
}
interface VersionExecutor {
public abstract CompletableFuture<?> consumeAsync(Consumer<TopologyCoordinator> callback);
public abstract <R> CompletableFuture<R> mapAsync(Function<TopologyCoordinator, R> callback);
/**
* Schedules a callback to be executed once a given delay elapses.
*
* @param callback
* @param delay the delay on which the task will be executed. Sub-millisecond part will be
* ignored
* @return
*/
public abstract CompletableFuture<?> scheduleOnce(Consumer<TopologyCoordinator> callback,
Duration delay);
public abstract <E> CompletableFuture<?> andThenAcceptAsync(CompletionStage<E> stage,
BiConsumer<TopologyCoordinator, E> consumer);
public abstract <E, U> CompletableFuture<U> andThenApplyAsync(CompletionStage<E> stage,
BiFunction<TopologyCoordinator, E, U> function);
}
private static class OnAnyVersion implements VersionExecutor {
private final ListeningScheduledExecutorService executor;
private final TopologyCoordinator coord;
public OnAnyVersion(ListeningScheduledExecutorService executor, TopologyCoordinator coord) {
this.executor = executor;
this.coord = coord;
}
@Override
public CompletableFuture<?> consumeAsync(Consumer<TopologyCoordinator> callback) {
return CompletableFuture.runAsync(() -> callback.accept(coord), executor);
}
@Override
public <R> CompletableFuture<R> mapAsync(Function<TopologyCoordinator, R> callback) {
return CompletableFuture.supplyAsync(() -> callback.apply(coord), executor);
}
@Override
public CompletableFuture<?> scheduleOnce(Consumer<TopologyCoordinator> callback,
Duration delay) {
ListenableScheduledFuture<?> listeningFut = executor.schedule(
() -> callback.accept(coord), delay.toMillis(), TimeUnit.MILLISECONDS);
return toCompletableFuture(listeningFut);
}
@Override
public <E> CompletableFuture<?> andThenAcceptAsync(CompletionStage<E> stage,
BiConsumer<TopologyCoordinator, E> consumer) {
return stage.thenAcceptAsync(e -> consumer.accept(coord, e), executor).toCompletableFuture();
}
@Override
public <E, U> CompletableFuture<U> andThenApplyAsync(CompletionStage<E> stage,
BiFunction<TopologyCoordinator, E, U> function) {
return stage.thenApplyAsync(e -> function.apply(coord, e), executor).toCompletableFuture();
}
}
private static class OnCurrentVersion implements VersionExecutor {
private final ListeningScheduledExecutorService executor;
private final TopologyCoordinator coord;
private final IntSupplier versionSupplier;
public OnCurrentVersion(ListeningScheduledExecutorService executor, TopologyCoordinator coord,
IntSupplier versionSupplier) {
this.executor = executor;
this.coord = coord;
this.versionSupplier = versionSupplier;
}
private void checkVersion(int originalVersion) {
int currentVersion = versionSupplier.getAsInt();
if (originalVersion != currentVersion) {
throw new CancellationException("Replication configuration changed from "
+ originalVersion + " to " + currentVersion + " since the task was "
+ "scheduled");
}
}
@Override
public CompletableFuture<?> consumeAsync(Consumer<TopologyCoordinator> callback) {
final int originalVersion = versionSupplier.getAsInt();
return CompletableFuture.runAsync(() -> {
checkVersion(originalVersion);
callback.accept(coord);
}, executor);
}
@Override
public <R> CompletableFuture<R> mapAsync(Function<TopologyCoordinator, R> callback) {
final int originalVersion = versionSupplier.getAsInt();
return CompletableFuture.supplyAsync(() -> {
checkVersion(originalVersion);
return callback.apply(coord);
}, executor);
}
@Override
public CompletableFuture<?> scheduleOnce(Consumer<TopologyCoordinator> callback,
Duration delay) {
final int originalVersion = versionSupplier.getAsInt();
ListenableScheduledFuture<?> listeningFut = executor.schedule(() -> {
checkVersion(originalVersion);
callback.accept(coord);
}, delay.toMillis(), TimeUnit.MILLISECONDS);
return toCompletableFuture(listeningFut);
}
@Override
public <E> CompletableFuture<?> andThenAcceptAsync(CompletionStage<E> stage,
BiConsumer<TopologyCoordinator, E> consumer) {
final int originalVersion = versionSupplier.getAsInt();
return stage.thenAcceptAsync(e -> {
checkVersion(originalVersion);
consumer.accept(coord, e);
}, executor)
.toCompletableFuture();
}
@Override
public <E, U> CompletableFuture<U> andThenApplyAsync(CompletionStage<E> stage,
BiFunction<TopologyCoordinator, E, U> function) {
final int originalVersion = versionSupplier.getAsInt();
return stage.thenApplyAsync(e -> {
checkVersion(originalVersion);
return function.apply(coord, e);
}, executor)
.toCompletableFuture();
}
}
}