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

io.activej.ot.uplink.OTUplinkStorage Maven / Gradle / Ivy

Go to download

Implementation of operational transformation technology. Allows building collaborative software systems.

There is a newer version: 6.0-rc2
Show 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.ot.uplink;

import io.activej.ot.TransformResult;
import io.activej.ot.system.OTSystem;
import io.activej.ot.uplink.OTUplinkStorage.Storage.SyncData;
import io.activej.promise.Promise;
import io.activej.promise.Promises;
import io.activej.promise.SettablePromise;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static io.activej.common.Utils.concat;
import static io.activej.promise.PromisePredicates.isResultOrException;
import static io.activej.promise.Promises.retry;
import static java.util.Collections.emptyList;

@SuppressWarnings("WeakerAccess")
public final class OTUplinkStorage implements OTUplink> {

	public static final long FIRST_COMMIT_ID = 1L;
	public static final int NO_LEVEL = 0;

	public static final class ProtoCommit {
		private final long id;
		private final List diffs;

		private ProtoCommit(long id, List diffs) {
			this.id = id;
			this.diffs = diffs;
		}

		public long getId() {
			return id;
		}

		public List getDiffs() {
			return diffs;
		}
	}

	public interface Storage {
		Promise init(long commitId, List snapshot, K uplinkCommitId, long uplinkLevel);

		Promise<@Nullable FetchData> getSnapshot();

		Promise getHead();

		default Promise> fetch(long commitId) {
			List diffs = new ArrayList<>();
			return getHead()
					.then(headCommitId ->
							Promises.loop(commitId + 1L,
											i -> i <= headCommitId,
											i -> getCommit(i)
													.map(commit -> {
														diffs.addAll(commit.getDiffs());
														return i + 1;
													}))
									.map($ -> new FetchData<>(headCommitId, NO_LEVEL, diffs)));
		}

		default Promise> poll(long currentCommitId) {
			return fetch(currentCommitId);
		}

		Promise> getCommit(long commitId);

		Promise add(long commitId, List diffs);

		final class SyncData {
			private final long commitId;
			private final K uplinkCommitId;
			private final long uplinkLevel;
			private final List uplinkDiffs;
			private final @Nullable Object protoCommit;

			public SyncData(long id, K uplinkCommitId, long uplinkLevel, List uplinkDiffs, @Nullable Object protoCommit) {
				this.commitId = id;
				this.uplinkCommitId = uplinkCommitId;
				this.uplinkLevel = uplinkLevel;
				this.uplinkDiffs = uplinkDiffs;
				this.protoCommit = protoCommit;
			}

			public long getCommitId() {
				return commitId;
			}

			public K getUplinkCommitId() {
				return uplinkCommitId;
			}

			public long getUplinkLevel() {
				return uplinkLevel;
			}

			public List getUplinkDiffs() {
				return uplinkDiffs;
			}

			public @Nullable Object getProtoCommit() {
				return protoCommit;
			}

			public boolean isSyncing() {
				return protoCommit != null;
			}
		}

		Promise> getSyncData();

		default Promise isSyncing() {
			return getSyncData().map(SyncData::isSyncing);
		}

		Promise startSync(long headCommitId, K uplinkCommitId, Object protoCommit);

		Promise completeSync(long commitId, List diffs, K uplinkCommitId, long uplinkLevel, List uplinkDiffs);
	}

	private final Storage storage;

	private final OTSystem otSystem;
	private final OTUplink uplink;

	private OTUplinkStorage(Storage storage, OTSystem otSystem, OTUplink uplink) {
		this.otSystem = otSystem;
		this.storage = storage;
		//noinspection unchecked
		this.uplink = (OTUplink) uplink;
	}

	public Promise sync() {
		return startSync()
				.then(syncData -> uplink.push(syncData.getProtoCommit())
						.then(uplinkFetchedData -> Promise.ofCallback(cb ->
								completeSync(syncData.getCommitId(), new ArrayList<>(), uplinkFetchedData.getCommitId(), uplinkFetchedData.getLevel(), uplinkFetchedData.getDiffs(), cb))));
	}

	Promise> startSync() {
		return storage.getSyncData()
				.thenIf(syncData -> syncData.getProtoCommit() == null,
						syncData -> storage.fetch(syncData.getCommitId())
								.then(fetchedData -> {
									long headCommitId = fetchedData.getCommitId();
									List diffs = fetchedData.getDiffs();
									return uplink.createProtoCommit(syncData.getUplinkCommitId(), concat(syncData.getUplinkDiffs(), diffs), 0)
											.then(protoCommit ->
													storage.startSync(headCommitId, syncData.getUplinkCommitId(), protoCommit)
															.map($ -> new SyncData<>(headCommitId, syncData.getUplinkCommitId(), syncData.getUplinkLevel(), diffs, protoCommit)));
								}));
	}

	void completeSync(long commitId, List accumulatedDiffs, K uplinkCommitId, long uplinkLevel, List uplinkDiffs, SettablePromise cb) {
		storage.fetch(commitId)
				.whenResult(fetchData -> {
					TransformResult transformResult = otSystem.transform(uplinkDiffs, fetchData.getDiffs());

					accumulatedDiffs.addAll(transformResult.left);
					storage.completeSync(fetchData.getCommitId(), accumulatedDiffs, uplinkCommitId, uplinkLevel, transformResult.right)
							.whenResult(ok -> {
								if (ok) {
									cb.set(null);
								} else {
									completeSync(commitId, accumulatedDiffs, uplinkCommitId, uplinkLevel, transformResult.right, cb);
								}
							})
							.whenException(cb::setException);
				})
				.whenException(cb::setException);
	}

	@Override
	public Promise> checkout() {
		//noinspection ConstantConditions
		return retry(
				isResultOrException(Objects::nonNull),
				() -> storage.getSnapshot()
						.thenIfNull(() -> uplink.checkout()
								.then(uplinkSnapshotData -> storage.init(FIRST_COMMIT_ID, uplinkSnapshotData.getDiffs(), uplinkSnapshotData.getCommitId(), uplinkSnapshotData.getLevel())
										.mapIfElse(ok -> ok,
												$ -> new FetchData<>(FIRST_COMMIT_ID, NO_LEVEL, uplinkSnapshotData.getDiffs()),
												$ -> null))))
				.then(snapshotData -> storage.fetch(snapshotData.getCommitId())
						.map(fetchData ->
								new FetchData<>(fetchData.getCommitId(), NO_LEVEL, concat(snapshotData.getDiffs(), fetchData.getDiffs()))));
	}

	@Override
	public Promise> createProtoCommit(Long parentCommitId, List diffs, long parentLevel) {
		return Promise.of(new ProtoCommit<>(parentCommitId, diffs));
	}

	@Override
	public Promise> push(ProtoCommit protoCommit) {
		return Promise.ofCallback(cb -> doPush(protoCommit.getId(), protoCommit.getDiffs(), emptyList(), cb));
	}

	void doPush(long commitId, List diffs, List fetchedDiffs, SettablePromise> cb) {
		storage.add(commitId, diffs)
				.whenResult(ok -> {
					if (ok) {
						cb.set(new FetchData<>(commitId + 1, NO_LEVEL, fetchedDiffs));
					} else {
						storage.fetch(commitId)
								.whenResult(fetchData -> {
									TransformResult transformResult = otSystem.transform(fetchData.getDiffs(), diffs);
									doPush(fetchData.getCommitId(), transformResult.left, concat(fetchedDiffs, transformResult.right), cb);
								})
								.whenException(cb::setException);
					}
				})
				.whenException(cb::setException);
	}

	@Override
	public Promise> fetch(Long currentCommitId) {
		return storage.fetch(currentCommitId);
	}

	@Override
	public Promise> poll(Long currentCommitId) {
		return storage.poll(currentCommitId);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy