io.activej.fs.cluster.ClusterActiveFs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of activej-fs Show documentation
Show all versions of activej-fs Show documentation
Provides tools for building efficient, scalable local, remote or clustered file servers.
It utilizes ActiveJ CSP for fast and reliable file transfer.
/*
* 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.fs.cluster;
import io.activej.async.function.AsyncSupplier;
import io.activej.async.process.AsyncCloseable;
import io.activej.async.service.EventloopService;
import io.activej.bytebuf.ByteBuf;
import io.activej.common.api.WithInitializer;
import io.activej.common.collection.Try;
import io.activej.common.exception.MalformedDataException;
import io.activej.common.ref.RefBoolean;
import io.activej.csp.ChannelConsumer;
import io.activej.csp.ChannelSupplier;
import io.activej.csp.dsl.ChannelConsumerTransformer;
import io.activej.eventloop.Eventloop;
import io.activej.eventloop.jmx.EventloopJmxBeanEx;
import io.activej.fs.ActiveFs;
import io.activej.fs.FileMetadata;
import io.activej.fs.exception.FsIOException;
import io.activej.jmx.api.attribute.JmxAttribute;
import io.activej.jmx.api.attribute.JmxOperation;
import io.activej.promise.Promise;
import io.activej.promise.Promises;
import io.activej.promise.jmx.PromiseStats;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import static io.activej.common.Checks.checkArgument;
import static io.activej.common.collection.CollectionUtils.transformIterator;
import static io.activej.csp.dsl.ChannelConsumerTransformer.identity;
import static io.activej.fs.util.RemoteFsUtils.ofFixedSize;
import static io.activej.promise.Promises.first;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
/**
* An implementation of {@link ActiveFs} which operates on other partitions as a cluster.
* Contains some redundancy and fail-safety capabilities.
*
* This implementation inherits the most strict limitations of all the file systems in cluster,
* as well as defines several limitations over those specified in {@link ActiveFs} interface:
*
* - Uploaded files should be immutable
* - Deletion of files is not guaranteed
* - Based on previous limitation, moving a file also does not guarantees that source file will be deleted
* - Paths should not contain existing filenames as part of the path
*
*/
public final class ClusterActiveFs implements ActiveFs, WithInitializer, EventloopService, EventloopJmxBeanEx {
private static final Logger logger = LoggerFactory.getLogger(ClusterActiveFs.class);
private final FsPartitions partitions;
/**
* Maximum allowed number of dead partitions, if there are more dead partitions than this number,
* the cluster is considered malformed.
*/
private int deadPartitionsThreshold = 0;
/**
* Minimum number of uploads that have to succeed in order for upload to be considered successful.
*/
private int uploadTargetsMin = 1;
/**
* Initial number of uploads that are initiated.
*/
private int uploadTargetsMax = 1;
// region JMX
private final PromiseStats uploadStartPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats uploadFinishPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats appendStartPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats appendFinishPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats downloadStartPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats downloadFinishPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats listPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats infoPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats infoAllPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats copyPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats copyAllPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats movePromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats moveAllPromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats deletePromise = PromiseStats.create(Duration.ofMinutes(5));
private final PromiseStats deleteAllPromise = PromiseStats.create(Duration.ofMinutes(5));
// endregion
// region creators
private ClusterActiveFs(FsPartitions partitions) {
this.partitions = partitions;
}
public static ClusterActiveFs create(FsPartitions partitions) {
return new ClusterActiveFs(partitions);
}
/**
* Sets the replication count that determines how many copies of the file should persist over the cluster.
*/
public ClusterActiveFs withReplicationCount(int replicationCount) {
checkArgument(1 <= replicationCount && replicationCount <= partitions.getPartitions().size(),
"Replication count cannot be less than one or greater than number of partitions");
this.deadPartitionsThreshold = replicationCount - 1;
this.uploadTargetsMin = replicationCount;
this.uploadTargetsMax = replicationCount;
return this;
}
/**
* Sets the replication count as well as number of upload targets that determines the number of server where file will be uploaded.
*/
@SuppressWarnings("UnusedReturnValue")
public ClusterActiveFs withPersistenceOptions(int deadPartitionsThreshold, int uploadTargetsMin, int uploadTargetsMax) {
checkArgument(0 <= deadPartitionsThreshold && deadPartitionsThreshold < partitions.getPartitions().size(),
"Dead partitions threshold cannot be less than zero or greater than number of partitions");
checkArgument(0 <= uploadTargetsMin,
"Minimum number of upload targets should not be less than zero");
checkArgument(0 < uploadTargetsMax && uploadTargetsMin <= uploadTargetsMax && uploadTargetsMax <= partitions.getPartitions().size(),
"Maximum number of upload targets should be greater than zero, " +
"should not be less than minimum number of upload targets and" +
"should not exceed total number of partitions");
this.deadPartitionsThreshold = deadPartitionsThreshold;
this.uploadTargetsMin = uploadTargetsMin;
this.uploadTargetsMax = uploadTargetsMax;
return this;
}
// endregion
// region getters
@NotNull
@Override
public Eventloop getEventloop() {
return partitions.getEventloop();
}
// endregion
@Override
public Promise> upload(@NotNull String name) {
return doUpload(name, fs -> fs.upload(name), identity(), uploadStartPromise, uploadFinishPromise);
}
@Override
public Promise> upload(@NotNull String name, long size) {
return doUpload(name, fs -> fs.upload(name, size), ofFixedSize(size), uploadStartPromise, uploadFinishPromise);
}
@Override
public Promise> append(@NotNull String name, long offset) {
return doUpload(name, fs -> fs.append(name, offset), identity(), appendStartPromise, appendFinishPromise);
}
@Override
public Promise> download(@NotNull String name, long offset, long limit) {
return broadcast(
(id, fs) -> {
logger.trace("downloading file {} from {}", name, id);
return fs.download(name, offset, limit)
.whenException(e -> logger.warn("Failed to connect to a server with key " + id + " to download file " + name, e))
.map(supplier -> supplier
.withEndOfStream(eos -> eos
.thenEx(partitions.wrapDeath(id))));
},
AsyncCloseable::close)
.then(filterErrors(() -> ofFailure("Could not download file '" + name + "' from any server")))
.then(suppliers -> {
ChannelByteCombiner combiner = ChannelByteCombiner.create();
for (ChannelSupplier supplier : suppliers) {
combiner.addInput().set(supplier);
}
return Promise.of(combiner.getOutput().getSupplier());
})
.whenComplete(downloadStartPromise.recordStats());
}
@Override
public Promise copy(@NotNull String name, @NotNull String target) {
return ActiveFs.super.copy(name, target)
.whenComplete(copyPromise.recordStats());
}
@Override
public Promise copyAll(Map sourceToTarget) {
return ActiveFs.super.copyAll(sourceToTarget)
.whenComplete(copyAllPromise.recordStats());
}
@Override
public Promise move(@NotNull String name, @NotNull String target) {
return ActiveFs.super.move(name, target)
.whenComplete(movePromise.recordStats());
}
@Override
public Promise moveAll(Map sourceToTarget) {
return ActiveFs.super.moveAll(sourceToTarget)
.whenComplete(moveAllPromise.recordStats());
}
@Override
public Promise delete(@NotNull String name) {
return broadcast(fs -> fs.delete(name))
.whenComplete(deletePromise.recordStats())
.toVoid();
}
@Override
public Promise deleteAll(Set toDelete) {
return broadcast(fs -> fs.deleteAll(toDelete))
.whenComplete(deleteAllPromise.recordStats())
.toVoid();
}
@Override
public Promise