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

org.eclipse.ditto.internal.utils.ddata.DistributedData Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2017 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.ditto.internal.utils.ddata;

import static java.util.Objects.requireNonNull;

import java.text.MessageFormat;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.IntStream;

import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig;

import org.apache.pekko.actor.AbstractExtensionId;
import org.apache.pekko.actor.ActorRef;
import org.apache.pekko.actor.ActorRefFactory;
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.actor.ExtendedActorSystem;
import org.apache.pekko.actor.Extension;
import org.apache.pekko.cluster.ddata.Key;
import org.apache.pekko.cluster.ddata.ReplicatedData;
import org.apache.pekko.cluster.ddata.Replicator;
import org.apache.pekko.cluster.ddata.ReplicatorSettings;
import org.apache.pekko.pattern.Patterns;
import scala.concurrent.duration.FiniteDuration;

/**
 * Supertype of typed interfaces for distributed data. Each instance corresponds to one distributed data object.
 * Each instance starts its own replicator so that it can have its own configuration regarding e. g. roles of cluster
 * members to which the data gets replicated.
 *
 * @param  type of replicated data.
 */
public abstract class DistributedData implements Extension {

    /**
     * Default timeout of read operations.
     */
    protected final Duration readTimeout;

    /**
     * Default timeout of write operations.
     */
    protected final Duration writeTimeout;

    /**
     * Reference of the distributed data replicator.
     */
    protected final ActorRef replicator;

    protected final int numberOfShards;

    private final Executor ddataExecutor;

    private final DistributedDataConfig config;

    /**
     * Create a wrapper of distributed data replicator.
     *
     * @param config specific config for this replicator.
     * @param factory creator of this replicator.
     * @throws NullPointerException if {@code configReader} is {@code null}.
     */
    protected DistributedData(final DistributedDataConfig config, final ActorRefFactory factory,
            final Executor ddataExecutor) {
        requireNonNull(config, "The DistributedDataConfig must not be null!");
        replicator = createReplicator(config, factory);
        this.ddataExecutor = ddataExecutor;
        readTimeout = config.getReadTimeout();
        writeTimeout = config.getWriteTimeout();
        numberOfShards = config.getNumberOfShards();
        this.config = config;
    }

    /**
     * Create a distributed data replicator in an actor system.
     *
     * @param config distributed data configuration reader.
     * @param factory creator of this replicator.
     * @return reference to the created replicator.
     */
    private static ActorRef createReplicator(final DistributedDataConfig config, final ActorRefFactory factory) {

        final PekkoReplicatorConfig pekkoReplicatorConfig = config.getPekkoReplicatorConfig();
        return factory.actorOf(Replicator.props(ReplicatorSettings.apply(pekkoReplicatorConfig.getCompleteConfig())),
                pekkoReplicatorConfig.getName());
    }

    /**
     * Create a distributed data config with Pekko's default options.
     *
     * @param replicatorName the name of the replicator.
     * @param replicatorRole the cluster role of members with replicas of the distributed collection.
     * @return a new config object.
     * @throws NullPointerException if any argument is {@code null}.
     */
    public static DistributedDataConfig createConfig(final ActorSystem actorSystem,
            final CharSequence replicatorName, final CharSequence replicatorRole) {

        return DefaultDistributedDataConfig.of(DefaultScopedConfig.dittoScoped(actorSystem.settings().config()),
                replicatorName, replicatorRole);
    }

    /**
     * Creates/gets a key for the passed {@code shardNumber}.
     *
     * @param shardNumber the number of the shard to append to the key.
     * @return key of the distributed collection. Should be unique among collections of the same type.
     */
    protected abstract Key getKey(int shardNumber);

    /**
     * @return initial value of the distributed data.
     */
    protected abstract R getInitialValue();

    /**
     * Asynchronously retrieves the replicated data.
     *
     * @param key the key to get the replicated data for.
     * @param readConsistency how many replicas to consult.
     * @return future of the replicated data that completes exceptionally on error.
     */
    public CompletionStage> get(final Key key, final Replicator.ReadConsistency readConsistency) {
        final Replicator.Get replicatorGet = new Replicator.Get<>(key, readConsistency);
        return Patterns.ask(replicator, replicatorGet, getAskTimeout(readConsistency.timeout(), readTimeout))
                .thenApplyAsync(reply -> this.handleGetResponse(reply, key), ddataExecutor);
    }

    /**
     * Modify the replicated data.
     *
     * @param key the key to update.
     * @param writeConsistency how many replicas to update.
     * @param updateFunction what to do to the replicas.
     * @return future that completes when the update completes, exceptionally when any error is encountered.
     */
    public CompletionStage update(final Key key, final Replicator.WriteConsistency writeConsistency,
            final Function updateFunction) {

        final Replicator.Update replicatorUpdate =
                new Replicator.Update<>(key, getInitialValue(), writeConsistency, updateFunction);
        return Patterns.ask(replicator, replicatorUpdate, getAskTimeout(writeConsistency.timeout(), writeTimeout))
                .thenApplyAsync(reply -> this.handleUpdateResponse(reply, key), ddataExecutor);
    }

    /**
     * Request updates when the distributed data changes.
     *
     * @param subscriber whom to notify of changes.
     */
    public void subscribeForChanges(final ActorRef subscriber) {
        IntStream.range(0, numberOfShards)
                .forEach(i -> replicator.tell(new Replicator.Subscribe<>(getKey(i), subscriber), ActorRef.noSender()));
    }

    /**
     * @return reference to the distributed data replicator.
     */
    public ActorRef getReplicator() {
        return replicator;
    }

    /**
     * Get the config of this distributed data.
     *
     * @return The config.
     * @since 3.0.0
     */
    public DistributedDataConfig getConfig() {
        return config;
    }

    private Void handleUpdateResponse(final Object reply, final Key key) {
        if (reply instanceof Replicator.UpdateSuccess) {
            return null;
        } else {
            final String errorMessage =
                    MessageFormat.format("Expect Replicator.UpdateSuccess for key ''{2}'' from ''{1}'', Got: ''{0}''",
                            reply, replicator, key);
            throw new IllegalArgumentException(errorMessage);
        }
    }

    @SuppressWarnings("unchecked")
    private Optional handleGetResponse(final Object reply, final Key key) {
        if (reply instanceof Replicator.GetSuccess) {
            final Replicator.GetSuccess getSuccess = (Replicator.GetSuccess) reply;
            return Optional.of(getSuccess.dataValue());
        } else if (reply instanceof Replicator.NotFound) {
            return Optional.empty();
        } else {
            final String errorMessage =
                    MessageFormat.format("Expect Replicator.GetResponse for key ''{2}'' from ''{1}'', Got: ''{0}''",
                            reply, replicator, key);
            throw new IllegalArgumentException(errorMessage);
        }
    }

    /**
     * Compute timeout from a given read/write consistency and a default. If the timeout from the read/write
     * consistency is positive, then it is taken. Otherwise the default timeout is taken.
     *
     * @param defaultTimeout timeout from a read/write consistency.
     * @param configuredTimeout default timeout.
     * @return the timeout.
     */
    private static Duration getAskTimeout(final FiniteDuration defaultTimeout, final Duration configuredTimeout) {
        if (configuredTimeout.isNegative()) {
            return Duration.ofMillis(defaultTimeout.toMillis());
        } else {
            return configuredTimeout;
        }
    }

    /**
     * Extension provider for Ditto distributed data.
     *
     * @param  type of distributed data.
     * @param  type of the actor system extension to handle the distributed data.
     */
    public abstract static class AbstractDDataProvider>
            extends AbstractExtensionId {

        /**
         * Constructor available for subclasses only.
         */
        protected AbstractDDataProvider() {
        }

        @Override
        public abstract T createExtension(ExtendedActorSystem system);

        /**
         * Lookup an extension provider to load an extension from config on actor system startup.
         * Required by ExtensionIdProvider; not used by Ditto.
         *
         * @return this object.
         */
        public AbstractDDataProvider lookup() {
            return this;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy