io.datakernel.crdt.CrdtStorageCluster Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of datakernel-crdt Show documentation
Show all versions of datakernel-crdt Show documentation
Conflict-free replicated data type implementation for DataKernel Framework.
/*
* Copyright (C) 2015-2019 SoftIndex 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.datakernel.crdt;
import io.datakernel.async.service.EventloopService;
import io.datakernel.common.Initializable;
import io.datakernel.common.collection.Try;
import io.datakernel.common.exception.StacklessException;
import io.datakernel.crdt.primitives.CrdtType;
import io.datakernel.datastream.StreamConsumer;
import io.datakernel.datastream.StreamSupplier;
import io.datakernel.datastream.processor.MultiSharder;
import io.datakernel.datastream.processor.ShardingStreamSplitter;
import io.datakernel.datastream.processor.StreamReducerSimple;
import io.datakernel.datastream.processor.StreamReducers.BinaryAccumulatorReducer;
import io.datakernel.datastream.processor.StreamSplitter;
import io.datakernel.datastream.stats.StreamStats;
import io.datakernel.datastream.stats.StreamStatsBasic;
import io.datakernel.datastream.stats.StreamStatsDetailed;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.eventloop.jmx.EventloopJmxMBeanEx;
import io.datakernel.jmx.api.JmxAttribute;
import io.datakernel.jmx.api.JmxOperation;
import io.datakernel.promise.Promise;
import io.datakernel.promise.Promises;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.Function;
import static io.datakernel.async.util.LogUtils.toLogger;
import static java.util.stream.Collectors.toList;
public final class CrdtStorageCluster, K extends Comparable, S> implements CrdtStorage, Initializable>, EventloopService, EventloopJmxMBeanEx {
private static final Logger logger = LoggerFactory.getLogger(CrdtStorageCluster.class);
private final Eventloop eventloop;
private final Map> clients;
private final Map> aliveClients;
private final Map> deadClients;
private final CrdtFunction function;
private final RendezvousHashSharder shardingFunction;
private List orderedIds;
private int replicationCount = 1;
private CrdtFilter filter = $ -> true;
// region JMX
private boolean detailedStats;
private final StreamStatsBasic> uploadStats = StreamStats.basic();
private final StreamStatsDetailed> uploadStatsDetailed = StreamStats.detailed();
private final StreamStatsBasic> downloadStats = StreamStats.basic();
private final StreamStatsDetailed> downloadStatsDetailed = StreamStats.detailed();
private final StreamStatsBasic removeStats = StreamStats.basic();
private final StreamStatsDetailed removeStatsDetailed = StreamStats.detailed();
// endregion
// region creators
private CrdtStorageCluster(Eventloop eventloop, Map> clients, CrdtFunction function) {
this.eventloop = eventloop;
this.clients = clients;
this.aliveClients = new LinkedHashMap<>(clients); // to keep order for indexed sharding
this.deadClients = new HashMap<>();
this.function = function;
shardingFunction = RendezvousHashSharder.create(orderedIds = new ArrayList<>(aliveClients.keySet()), replicationCount);
}
public static , K extends Comparable, S> CrdtStorageCluster create(
Eventloop eventloop, Map> clients, CrdtFunction crdtFunction
) {
return new CrdtStorageCluster<>(eventloop, new HashMap<>(clients), crdtFunction);
}
public static , K extends Comparable, S extends CrdtType> CrdtStorageCluster create(
Eventloop eventloop, Map> clients
) {
return new CrdtStorageCluster<>(eventloop, new HashMap<>(clients), CrdtFunction.ofCrdtType());
}
public CrdtStorageCluster withPartition(I partitionId, CrdtStorage client) {
clients.put(partitionId, client);
aliveClients.put(partitionId, client);
recompute();
return this;
}
public CrdtStorageCluster withReplicationCount(int replicationCount) {
this.replicationCount = replicationCount;
recompute();
return this;
}
public CrdtStorageCluster withFilter(CrdtFilter filter) {
this.filter = filter;
return this;
}
// endregion
// region getters
public Map> getClients() {
return Collections.unmodifiableMap(clients);
}
public Map> getAliveClients() {
return Collections.unmodifiableMap(aliveClients);
}
public Map> getDeadClients() {
return Collections.unmodifiableMap(deadClients);
}
public List getOrderedIds() {
return Collections.unmodifiableList(orderedIds);
}
public MultiSharder getShardingFunction() {
return shardingFunction;
}
// endregion
@NotNull
@Override
public Eventloop getEventloop() {
return eventloop;
}
public Promise checkAllPartitions() {
return Promises.all(clients.entrySet().stream()
.map(entry -> {
I id = entry.getKey();
return entry.getValue()
.ping()
.mapEx(($, e) -> {
if (e == null) {
markAlive(id);
} else {
markDead(id, e);
}
return null;
});
}))
.whenComplete(toLogger(logger, "checkAllPartitions"));
}
public Promise checkDeadPartitions() {
return Promises.all(deadClients.entrySet().stream()
.map(e -> e.getValue()
.ping()
.mapEx(($, exc) -> {
if (exc == null) {
markAlive(e.getKey());
}
return null;
})))
.whenComplete(toLogger(logger, "checkDeadPartitions"));
}
private void markAlive(I partitionId) {
CrdtStorage removed = deadClients.remove(partitionId);
if (removed != null) {
aliveClients.put(partitionId, removed);
recompute();
logger.info("Marked partition {} as alive", partitionId);
}
}
public void markDead(I partitionId, Throwable err) {
CrdtStorage removed = aliveClients.remove(partitionId);
if (removed != null) {
deadClients.put(partitionId, removed);
recompute();
logger.warn("Marked partition {} as dead", partitionId, err);
}
}
private void recompute() {
shardingFunction.recompute(orderedIds = new ArrayList<>(aliveClients.keySet()), replicationCount);
}
private Promise> connect(Function, Promise> method) {
return Promises.toList(
aliveClients.entrySet().stream()
.map(entry ->
method.apply(entry.getValue())
.whenException(err -> markDead(entry.getKey(), err))
.toTry()))
.then(tries -> {
List successes = tries.stream()
.filter(Try::isSuccess)
.map(Try::get)
.collect(toList());
if (successes.isEmpty()) {
return Promise.ofException(new StacklessException(CrdtStorageCluster.class, "No successful connections"));
}
return Promise.of(successes);
});
}
@Override
public Promise>> upload() {
return connect(CrdtStorage::upload)
.then(successes -> {
ShardingStreamSplitter, K> shplitter = ShardingStreamSplitter.create(shardingFunction, CrdtData::getKey);
successes.forEach(consumer -> shplitter.newOutput().streamTo(consumer));
return Promise.of(shplitter.getInput()
.transformWith(detailedStats ? uploadStats : uploadStatsDetailed)
.withLateBinding());
});
}
@Override
public Promise>> download(long timestamp) {
return connect(storage -> storage.download(timestamp))
.then(successes -> {
StreamReducerSimple, CrdtData, CrdtData> reducer =
StreamReducerSimple.create(CrdtData::getKey, Comparator.naturalOrder(),
new BinaryAccumulatorReducer>((a, b) -> new CrdtData<>(a.getKey(), function.merge(a.getState(), b.getState())))
.withFilter(data -> filter.test(data.getState())));
successes.forEach(producer -> producer.streamTo(reducer.newInput()));
return Promise.of(reducer.getOutput()
.transformWith(detailedStats ? downloadStats : downloadStatsDetailed)
.withLateBinding());
});
}
@Override
public Promise> remove() {
return connect(CrdtStorage::remove)
.then(successes -> {
StreamSplitter splitter = StreamSplitter.create();
successes.forEach(consumer -> splitter.newOutput().streamTo(consumer));
return Promise.of(splitter.getInput()
.transformWith(detailedStats ? removeStats : removeStatsDetailed)
.withLateBinding());
});
}
@Override
public Promise ping() {
return Promise.complete(); // Promises.all(aliveClients.values().stream().map(CrdtClient::ping));
}
@NotNull
@Override
public Promise start() {
return Promise.complete();
}
@NotNull
@Override
public Promise stop() {
return Promise.complete();
}
// region JMX
@JmxAttribute
public int getReplicationCount() {
return replicationCount;
}
@JmxAttribute
public void setReplicationCount(int replicationCount) {
withReplicationCount(replicationCount);
}
@JmxAttribute
public int getAlivePartitionCount() {
return aliveClients.size();
}
@JmxAttribute
public int getDeadPartitionCount() {
return deadClients.size();
}
@JmxAttribute
public String[] getAlivePartitions() {
return aliveClients.keySet().stream()
.map(Object::toString)
.toArray(String[]::new);
}
@JmxAttribute
public String[] getDeadPartitions() {
return deadClients.keySet().stream()
.map(Object::toString)
.toArray(String[]::new);
}
@JmxOperation
public void startDetailedMonitoring() {
detailedStats = true;
}
@JmxOperation
public void stopDetailedMonitoring() {
detailedStats = false;
}
@JmxAttribute
public StreamStatsBasic getUploadStats() {
return uploadStats;
}
@JmxAttribute
public StreamStatsDetailed getUploadStatsDetailed() {
return uploadStatsDetailed;
}
@JmxAttribute
public StreamStatsBasic getDownloadStats() {
return downloadStats;
}
@JmxAttribute
public StreamStatsDetailed getDownloadStatsDetailed() {
return downloadStatsDetailed;
}
@JmxAttribute
public StreamStatsBasic getRemoveStats() {
return removeStats;
}
@JmxAttribute
public StreamStatsDetailed getRemoveStatsDetailed() {
return removeStatsDetailed;
}
// endregion
}