
io.activej.crdt.storage.cluster.ClusterCrdtStorage Maven / Gradle / Ivy
Show all versions of activej-crdt Show documentation
/*
* Copyright (C) 2020 ActiveJ LLC.
*
* Licensed 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 io.activej.crdt.storage.cluster;
import io.activej.async.function.AsyncFunction;
import io.activej.async.function.AsyncSupplier;
import io.activej.async.process.AsyncCloseable;
import io.activej.async.service.ReactiveService;
import io.activej.common.ApplicationSettings;
import io.activej.common.Checks;
import io.activej.common.builder.AbstractBuilder;
import io.activej.common.collection.Try;
import io.activej.crdt.CrdtData;
import io.activej.crdt.CrdtException;
import io.activej.crdt.CrdtTombstone;
import io.activej.crdt.RemoteCrdtStorage;
import io.activej.crdt.function.CrdtFunction;
import io.activej.crdt.storage.ICrdtStorage;
import io.activej.crdt.storage.cluster.IDiscoveryService.PartitionScheme;
import io.activej.datastream.consumer.StreamConsumer;
import io.activej.datastream.processor.StreamSplitter;
import io.activej.datastream.processor.reducer.BinaryAccumulatorReducer;
import io.activej.datastream.processor.reducer.StreamReducer;
import io.activej.datastream.stats.BasicStreamStats;
import io.activej.datastream.stats.DetailedStreamStats;
import io.activej.datastream.stats.StreamStats;
import io.activej.datastream.supplier.StreamSupplier;
import io.activej.jmx.api.attribute.JmxAttribute;
import io.activej.jmx.api.attribute.JmxOperation;
import io.activej.jmx.stats.EventStats;
import io.activej.promise.Promise;
import io.activej.promise.Promises;
import io.activej.reactor.AbstractReactive;
import io.activej.reactor.Reactor;
import io.activej.reactor.jmx.ReactiveJmxBeanWithStats;
import org.jetbrains.annotations.VisibleForTesting;
import java.time.Duration;
import java.util.*;
import java.util.function.Function;
import static io.activej.crdt.util.Utils.onItem;
import static io.activej.reactor.Reactive.checkInReactorThread;
import static java.util.stream.Collectors.toMap;
@SuppressWarnings("rawtypes") // JMX
public final class ClusterCrdtStorage, S, P> extends AbstractReactive
implements ICrdtStorage, ReactiveService, ReactiveJmxBeanWithStats {
private static final boolean CHECKS = Checks.isEnabled(ClusterCrdtStorage.class);
public static final Duration DEFAULT_SMOOTHING_WINDOW = ApplicationSettings.getDuration(ClusterCrdtStorage.class, "smoothingWindow", Duration.ofMinutes(1));
private final IDiscoveryService discoveryService;
private final CrdtFunction crdtFunction;
private final Map
> crdtStorages = new LinkedHashMap<>();
private PartitionScheme
currentPartitionScheme;
private boolean forceStart;
private boolean stopped;
// region JMX
private boolean detailedStats;
private final BasicStreamStats> uploadStats = StreamStats.basic();
private final DetailedStreamStats> uploadStatsDetailed = StreamStats.detailed();
private final BasicStreamStats> downloadStats = StreamStats.basic();
private final DetailedStreamStats> downloadStatsDetailed = StreamStats.detailed();
private final BasicStreamStats> takeStats = StreamStats.basic();
private final DetailedStreamStats> takeStatsDetailed = StreamStats.detailed();
private final BasicStreamStats> removeStats = StreamStats.basic();
private final DetailedStreamStats> removeStatsDetailed = StreamStats.detailed();
private final BasicStreamStats> repartitionUploadStats = StreamStats.basic();
private final DetailedStreamStats> repartitionUploadStatsDetailed = StreamStats.detailed();
private final EventStats uploadedItems = EventStats.create(DEFAULT_SMOOTHING_WINDOW);
private final EventStats downloadedItems = EventStats.create(DEFAULT_SMOOTHING_WINDOW);
private final EventStats takenItems = EventStats.create(DEFAULT_SMOOTHING_WINDOW);
private final EventStats removedItems = EventStats.create(DEFAULT_SMOOTHING_WINDOW);
private final EventStats repartitionedItems = EventStats.create(DEFAULT_SMOOTHING_WINDOW);
// endregion
private ClusterCrdtStorage(Reactor reactor, IDiscoveryService discoveryService, CrdtFunction crdtFunction) {
super(reactor);
this.discoveryService = discoveryService;
this.crdtFunction = crdtFunction;
}
public static , S, P> ClusterCrdtStorage create(
Reactor reactor, IDiscoveryService discoveryService, CrdtFunction crdtFunction
) {
return ClusterCrdtStorage.builder(reactor, discoveryService, crdtFunction).build();
}
public static , S, P> ClusterCrdtStorage.Builder builder(
Reactor reactor, IDiscoveryService discoveryService, CrdtFunction crdtFunction
) {
return new ClusterCrdtStorage(reactor, discoveryService, crdtFunction).new Builder();
}
public final class Builder extends AbstractBuilder> {
private Builder() {}
public Builder withForceStart(boolean forceStart) {
checkNotBuilt(this);
ClusterCrdtStorage.this.forceStart = forceStart;
return this;
}
@Override
protected ClusterCrdtStorage doBuild() {
return ClusterCrdtStorage.this;
}
}
/*
public CrdtStorageCluster withReplicationCount(int replicationCount) {
checkArgument(1 <= replicationCount, "Replication count cannot be less than one");
this.deadPartitionsThreshold = replicationCount - 1;
this.replicationCount = replicationCount;
this.partitions.setTopShards(replicationCount);
return this;
}
*/
@Override
public Promise> start() {
checkInReactorThread(this);
AsyncSupplier> discoverySupplier = discoveryService.discover();
return discoverySupplier.get()
.then(result -> {
updatePartitionScheme(result);
return ping()
.thenCallback((v, e, cb) -> {
if (e instanceof CrdtException && forceStart) {
cb.set(null);
return;
}
cb.set(v, e);
});
})
.whenResult(() -> Promises.repeat(() ->
discoverySupplier.get()
.map((result, e) -> {
if (stopped) return false;
if (e == null) {
updatePartitionScheme(result);
}
return true;
})
));
}
@Override
public Promise> stop() {
checkInReactorThread(this);
this.stopped = true;
return Promise.complete();
}
@Override
public Promise>> upload() {
if (CHECKS) checkInReactorThread(this);
PartitionScheme partitionScheme = this.currentPartitionScheme;
return execute(partitionScheme, ICrdtStorage::upload)
.then(map -> {
List
alive = new ArrayList<>(map.keySet());
Sharder sharder = partitionScheme.createSharder(alive);
if (sharder == null) {
CrdtException exception = new CrdtException("Incomplete cluster");
for (StreamConsumer> consumer : map.values()) {
consumer.closeEx(exception);
}
throw exception;
}
StreamSplitter, CrdtData> splitter = StreamSplitter.create(
(item, acceptors) -> {
int[] selected = sharder.shard(item.getKey());
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < selected.length; i++) {
acceptors[selected[i]].accept(item);
}
});
for (P partitionId : alive) {
splitter.newOutput().streamTo(map.get(partitionId));
}
return Promise.of(splitter.getInput()
.transformWith(detailedStats ? uploadStatsDetailed : uploadStats)
.transformWith(onItem(uploadedItems::recordEvent)));
});
}
@Override
public Promise>> download(long timestamp) {
if (CHECKS) checkInReactorThread(this);
return getData(storage -> storage.download(timestamp))
.map(supplier -> supplier
.transformWith(detailedStats ? downloadStatsDetailed : downloadStats)
.transformWith(onItem(downloadedItems::recordEvent)));
}
@Override
public Promise>> take() {
if (CHECKS) checkInReactorThread(this);
return getData(ICrdtStorage::take)
.map(supplier -> supplier
.transformWith(detailedStats ? takeStatsDetailed : takeStats)
.transformWith(onItem(takenItems::recordEvent)));
}
@Override
public Promise>> remove() {
if (CHECKS) checkInReactorThread(this);
PartitionScheme partitionScheme = currentPartitionScheme;
return execute(partitionScheme, ICrdtStorage::remove)
.map(map -> {
List
alive = new ArrayList<>(map.keySet());
Sharder sharder = partitionScheme.createSharder(alive);
if (sharder == null) {
CrdtException exception = new CrdtException("Incomplete cluster");
for (StreamConsumer> consumer : map.values()) {
consumer.closeEx(exception);
}
throw exception;
}
StreamSplitter, CrdtTombstone> splitter = StreamSplitter.create(
(item, acceptors) -> {
int[] selected = sharder.shard(item.getKey());
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < selected.length; i++) {
acceptors[selected[i]].accept(item);
}
});
for (P partitionId : alive) {
splitter.newOutput().streamTo(map.get(partitionId));
}
return splitter.getInput()
.transformWith(detailedStats ? removeStatsDetailed : removeStats)
.transformWith(onItem(removedItems::recordEvent));
});
}
public Promise repartition(P sourcePartitionId) {
if (CHECKS) checkInReactorThread(this);
PartitionScheme partitionScheme = this.currentPartitionScheme;
ICrdtStorage source = crdtStorages.get(sourcePartitionId);
class Tuple {
private final Try>> downloader;
private final Map>> uploaders;
public Tuple(Try>> downloader, Map>> uploaders) {
this.downloader = downloader;
this.uploaders = uploaders;
}
private void close() {
downloader.ifSuccess(AsyncCloseable::close);
uploaders.values().forEach(AsyncCloseable::close);
}
}
return Promises.toTuple(Tuple::new,
source.take().toTry(),
execute(partitionScheme, ICrdtStorage::upload))
.whenResult(tuple -> {
if (!tuple.uploaders.containsKey(sourcePartitionId)) {
tuple.close();
throw new CrdtException("Could not upload to local storage");
}
if (tuple.uploaders.size() == 1) {
tuple.close();
throw new CrdtException("Nowhere to upload");
}
if (tuple.downloader.isException()) {
tuple.close();
Exception e = tuple.downloader.getException();
throw new CrdtException("Could not download local data", e);
}
})
.then(tuple -> {
List
alive = new ArrayList<>(tuple.uploaders.keySet());
Sharder sharder = partitionScheme.createSharder(alive);
if (sharder == null) {
tuple.close();
return Promise.ofException(new CrdtException("Incomplete cluster"));
}
StreamSplitter, ?> splitter = StreamSplitter.create(
(item, acceptors) -> {
for (int idx : sharder.shard(item.getKey())) {
acceptors[idx].accept(item);
}
});
StreamConsumer> uploader = splitter.getInput();
StreamSupplier> downloader = tuple.downloader.get();
for (P partitionId : alive) {
//noinspection unchecked
((StreamSupplier>) splitter.newOutput())
.transformWith(detailedStats ? repartitionUploadStatsDetailed : repartitionUploadStats)
.transformWith(onItem(repartitionedItems::recordEvent))
.streamTo(tuple.uploaders.get(partitionId));
}
return downloader.streamTo(uploader);
});
}
@Override
public Promise ping() {
if (CHECKS) checkInReactorThread(this);
PartitionScheme partitionScheme = this.currentPartitionScheme;
return execute(partitionScheme, ICrdtStorage::ping)
.whenResult(map -> {
Sharder sharder = partitionScheme.createSharder(new ArrayList<>(map.keySet()));
if (sharder == null) {
throw new CrdtException("Incomplete cluster");
}
})
.toVoid();
}
private Promise