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

io.activej.crdt.wal.WalUploader Maven / Gradle / Ivy

/*
 * 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.wal;

import io.activej.async.function.AsyncRunnable;
import io.activej.common.ApplicationSettings;
import io.activej.common.builder.AbstractBuilder;
import io.activej.common.exception.TruncatedDataException;
import io.activej.crdt.CrdtData;
import io.activej.crdt.function.CrdtFunction;
import io.activej.crdt.primitives.CrdtType;
import io.activej.crdt.storage.ICrdtStorage;
import io.activej.crdt.util.CrdtDataBinarySerializer;
import io.activej.csp.file.ChannelFileReader;
import io.activej.csp.process.frame.ChannelFrameDecoder;
import io.activej.csp.supplier.ChannelSuppliers;
import io.activej.datastream.csp.ChannelDeserializer;
import io.activej.datastream.processor.reducer.Reducer;
import io.activej.datastream.processor.reducer.StreamReducer;
import io.activej.datastream.processor.transformer.sort.StreamSorter;
import io.activej.datastream.processor.transformer.sort.StreamSorterStorage;
import io.activej.datastream.supplier.StreamDataAcceptor;
import io.activej.jmx.api.attribute.JmxAttribute;
import io.activej.jmx.api.attribute.JmxOperation;
import io.activej.jmx.stats.ValueStats;
import io.activej.promise.Promise;
import io.activej.promise.jmx.PromiseStats;
import io.activej.reactor.AbstractReactive;
import io.activej.reactor.Reactor;
import io.activej.reactor.jmx.ReactiveJmxBeanWithStats;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.Function;

import static io.activej.async.function.AsyncRunnables.coalesce;
import static io.activej.common.function.FunctionEx.identity;
import static io.activej.crdt.util.Utils.deleteWalFiles;
import static io.activej.crdt.util.Utils.getWalFiles;
import static io.activej.crdt.wal.FileWriteAheadLog.FRAME_FORMAT;
import static io.activej.reactor.Reactive.checkInReactorThread;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.toList;

public final class WalUploader, S> extends AbstractReactive
	implements ReactiveJmxBeanWithStats {

	private static final Logger logger = LoggerFactory.getLogger(WalUploader.class);

	private static final int DEFAULT_SORT_ITEMS_IN_MEMORY = ApplicationSettings.getInt(WalUploader.class, "sortItemsInMemory", 100_000);
	private static final Duration SMOOTHING_WINDOW = ApplicationSettings.getDuration(WalUploader.class, "smoothingWindow", Duration.ofMinutes(5));

	private final AsyncRunnable uploadToStorage = coalesce(this::doUploadToStorage);

	private final Executor executor;
	private final Path path;
	private final CrdtFunction function;
	private final CrdtDataBinarySerializer serializer;
	private final ICrdtStorage storage;

	private final PromiseStats uploadPromise = PromiseStats.create(SMOOTHING_WINDOW);
	private final ValueStats totalFilesUploaded = ValueStats.create(SMOOTHING_WINDOW);
	private final ValueStats totalFilesUploadedSize = ValueStats.builder(SMOOTHING_WINDOW)
		.withUnit("bytes")
		.build();
	private boolean detailedMonitoring;

	private @Nullable Path sortDir;
	private int sortItemsInMemory = DEFAULT_SORT_ITEMS_IN_MEMORY;

	private WalUploader(Reactor reactor, Executor executor, Path path, CrdtFunction function, CrdtDataBinarySerializer serializer, ICrdtStorage storage) {
		super(reactor);
		this.executor = executor;
		this.path = path;
		this.function = function;
		this.serializer = serializer;
		this.storage = storage;
	}

	public static , S> WalUploader create(Reactor reactor, Executor executor, Path path, CrdtFunction function, CrdtDataBinarySerializer serializer, ICrdtStorage storage) {
		return builder(reactor, executor, path, function, serializer, storage).build();
	}

	public static , S extends CrdtType> WalUploader create(Reactor reactor, Executor executor, Path path, CrdtDataBinarySerializer serializer, ICrdtStorage storage) {
		return builder(reactor, executor, path, serializer, storage).build();
	}

	public static , S> WalUploader.Builder builder(Reactor reactor, Executor executor, Path path, CrdtFunction function, CrdtDataBinarySerializer serializer, ICrdtStorage storage) {
		return new WalUploader<>(reactor, executor, path, function, serializer, storage).new Builder();
	}

	public static , S extends CrdtType> WalUploader.Builder builder(Reactor reactor, Executor executor, Path path, CrdtDataBinarySerializer serializer, ICrdtStorage storage) {
		return new WalUploader<>(reactor, executor, path, CrdtFunction.ofCrdtType(), serializer, storage).new Builder();
	}

	public final class Builder extends AbstractBuilder> {
		private Builder() {}

		public Builder withSortDir(Path sortDir) {
			checkNotBuilt(this);
			WalUploader.this.sortDir = sortDir;
			return this;
		}

		public Builder withSortItemsInMemory(int sortItemsInMemory) {
			checkNotBuilt(this);
			WalUploader.this.sortItemsInMemory = sortItemsInMemory;
			return this;
		}

		@Override
		protected WalUploader doBuild() {
			return WalUploader.this;
		}
	}

	public Promise uploadToStorage() {
		checkInReactorThread(this);
		return uploadToStorage.run()
			.whenComplete(uploadPromise.recordStats());
	}

	private Promise doUploadToStorage() {
		return getWalFiles(executor, path)
			.then(this::uploadWaLFiles);
	}

	private Promise uploadWaLFiles(List walFiles) throws IOException {
		if (walFiles.isEmpty()) {
			logger.info("Nothing to upload");
			return Promise.complete();
		}

		if (logger.isInfoEnabled()) {
			logger.info("Uploading write ahead logs {}", walFiles.stream().map(Path::getFileName).collect(toList()));
		}

		Path sortDir;
		try {
			sortDir = createSortDir();
		} catch (IOException e) {
			logger.warn("Failed to create temporary sort directory", e);
			throw e;
		}

		return createReducer(walFiles)
			.getOutput()
			.transformWith(createSorter(sortDir))
			.streamTo(storage.upload())
			.whenComplete(() -> cleanup(sortDir))
			.whenResult(() -> {
				totalFilesUploaded.recordValue(walFiles.size());
				if (detailedMonitoring) {
					for (Path walFile : walFiles) {
						try {
							totalFilesUploadedSize.recordValue(Files.size(walFile));
						} catch (IOException e) {
							logger.warn("Could not get the size of uploaded file {}", walFile);
						}
					}
				}
			})
			.then(() -> deleteWalFiles(executor, walFiles));
	}

	private Path createSortDir() throws IOException {
		if (this.sortDir != null) {
			return this.sortDir;
		} else {
			return Files.createTempDirectory("crdt-wal-sort-dir");
		}
	}

	private StreamSorter> createSorter(Path sortDir) {
		StreamSorterStorage> sorterStorage = StreamSorterStorage.create(reactor, executor, serializer, FRAME_FORMAT, sortDir);
		Function, K> keyFn = CrdtData::getKey;
		return StreamSorter.create(sorterStorage, keyFn, naturalOrder(), false, sortItemsInMemory);
	}

	private void cleanup(Path sortDir) {
		if (this.sortDir == null) {
			try {
				Files.deleteIfExists(sortDir);
			} catch (IOException ignored) {
			}
		}
	}

	private StreamReducer, CrdtData> createReducer(List files) {
		StreamReducer, CrdtData> reducer = StreamReducer.create();

		for (Path file : files) {
			ChannelSuppliers.ofPromise(ChannelFileReader.open(executor, file))
				.transformWith(ChannelFrameDecoder.create(FRAME_FORMAT))
				.withEndOfStream(eos -> eos
					.map(identity(),
						e -> {
							if (e instanceof TruncatedDataException) {
								logger.warn("Write ahead log {} was truncated", file);
								return null;
							}
							throw e;
						}))
				.transformWith(ChannelDeserializer.create(serializer))
				.streamTo(reducer.newInput(CrdtData::getKey, new WalReducer()));
		}

		return reducer;
	}

	// region JMX
	@JmxAttribute
	public PromiseStats getUploadPromise() {
		return uploadPromise;
	}

	@JmxAttribute
	public ValueStats getTotalFilesUploaded() {
		return totalFilesUploaded;
	}

	@JmxAttribute
	public ValueStats getTotalFilesUploadedSize() {
		return totalFilesUploadedSize;
	}

	@JmxAttribute
	public boolean isDetailedMonitoring() {
		return detailedMonitoring;
	}

	@JmxOperation
	public void startDetailedMonitoring() {
		detailedMonitoring = true;
	}

	@JmxOperation
	public void stopDetailedMonitoring() {
		detailedMonitoring = false;
	}
	// endregion

	public final class WalReducer implements Reducer, CrdtData, CrdtData> {
		@Override
		public CrdtData onFirstItem(StreamDataAcceptor> stream, K key, CrdtData firstValue) {
			return firstValue;
		}

		@Override
		public CrdtData onNextItem(StreamDataAcceptor> stream, K key, CrdtData nextValue, CrdtData accumulator) {
			long timestamp = Math.max(accumulator.getTimestamp(), nextValue.getTimestamp());
			S merged = function.merge(accumulator.getState(), accumulator.getTimestamp(), nextValue.getState(), nextValue.getTimestamp());
			return new CrdtData<>(key, timestamp, merged);
		}

		@Override
		public void onComplete(StreamDataAcceptor> stream, K key, CrdtData accumulator) {
			stream.accept(accumulator);
		}
	}
}