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

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

The newest version!
/*
 * 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.async.function.AsyncRunnables;
import io.activej.async.service.ReactiveService;
import io.activej.common.Checks;
import io.activej.common.builder.AbstractBuilder;
import io.activej.common.time.CurrentTimeProvider;
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.datastream.supplier.StreamSuppliers;
import io.activej.promise.Promise;
import io.activej.reactor.AbstractReactive;
import io.activej.reactor.Reactor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.TreeMap;

import static io.activej.async.util.LogUtils.Level.INFO;
import static io.activej.async.util.LogUtils.toLogger;
import static io.activej.reactor.Reactive.checkInReactorThread;

public final class InMemoryWriteAheadLog, S> extends AbstractReactive
	implements IWriteAheadLog, ReactiveService {

	private static final Logger logger = LoggerFactory.getLogger(InMemoryWriteAheadLog.class);
	private static final boolean CHECKS = Checks.isEnabled(InMemoryWriteAheadLog.class);

	private Map> map = new TreeMap<>();

	private final CrdtFunction function;
	private final ICrdtStorage storage;

	private final AsyncRunnable flush = AsyncRunnables.coalesce(this::doFlush);

	private CurrentTimeProvider now = CurrentTimeProvider.ofSystem();

	private InMemoryWriteAheadLog(Reactor reactor, CrdtFunction function, ICrdtStorage storage) {
		super(reactor);
		this.function = function;
		this.storage = storage;
	}

	public static , S> InMemoryWriteAheadLog create(Reactor reactor, CrdtFunction function, ICrdtStorage storage) {
		return builder(reactor, function, storage).build();
	}

	public static , S extends CrdtType> InMemoryWriteAheadLog create(Reactor reactor, ICrdtStorage storage) {
		return builder(reactor, CrdtFunction.ofCrdtType(), storage).build();
	}

	public static , S> InMemoryWriteAheadLog.Builder builder(Reactor reactor, CrdtFunction function, ICrdtStorage storage) {
		return new InMemoryWriteAheadLog<>(reactor, function, storage).new Builder();
	}

	public static , S extends CrdtType> InMemoryWriteAheadLog.Builder builder(Reactor reactor, ICrdtStorage storage) {
		return new InMemoryWriteAheadLog<>(reactor, CrdtFunction.ofCrdtType(), storage).new Builder();
	}

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

		public Builder withCurrentTimeProvider(CurrentTimeProvider now) {
			checkNotBuilt(this);
			InMemoryWriteAheadLog.this.now = now;
			return this;
		}

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

	@Override
	public Promise put(K key, S value) {
		if (CHECKS) checkInReactorThread(this);
		if (logger.isTraceEnabled()) {
			logger.trace("{} value for key {}", map.containsKey(key) ? "Merging" : "Putting new", key);
		}
		doPut(key, new CrdtData<>(key, now.currentTimeMillis(), value));
		return Promise.complete();
	}

	@Override
	public Promise flush() {
		if (CHECKS) checkInReactorThread(this);
		return flush.run();
	}

	private Promise doFlush() {
		if (map.isEmpty()) {
			logger.info("Nothing to flush");
			return Promise.complete();
		}

		Map> map = this.map;
		this.map = new TreeMap<>();

		return storage.upload()
			.then(consumer -> StreamSuppliers.ofIterable(map.values())
				.streamTo(consumer))
			.whenException(e -> map.forEach(this::doPut))
			.whenComplete(toLogger(logger, INFO, INFO, "flush", map.size()));
	}

	@Override
	public Promise start() {
		checkInReactorThread(this);
		return Promise.complete();
	}

	@Override
	public Promise stop() {
		checkInReactorThread(this);
		return flush();
	}

	private void doPut(K key, CrdtData value) {
		map.merge(key, value, (a, b) -> {
			long timestamp = Math.max(a.getTimestamp(), b.getTimestamp());
			S merged = function.merge(a.getState(), a.getTimestamp(), b.getState(), b.getTimestamp());
			return new CrdtData<>(key, timestamp, merged);
		});
	}
}