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

io.datakernel.crdt.CrdtStorageCluster Maven / Gradle / Ivy

There is a newer version: 3.1.0
Show newest version
/*
 * 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
}