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

com.linecorp.centraldogma.client.AbstractWatcher Maven / Gradle / Ivy

Go to download

Highly-available version-controlled service configuration repository based on Git, ZooKeeper and HTTP/2 (centraldogma-client)

There is a newer version: 0.70.0
Show newest version
/*
 * Copyright 2018 LINE Corporation
 *
 * LINE Corporation licenses this file to you 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:
 *
 *   https://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 com.linecorp.centraldogma.client;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.math.LongMath.saturatedAdd;
import static java.util.Objects.requireNonNull;

import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.MoreObjects;

import com.linecorp.centraldogma.common.CentralDogmaException;
import com.linecorp.centraldogma.common.EntryNotFoundException;
import com.linecorp.centraldogma.common.PathPattern;
import com.linecorp.centraldogma.common.Query;
import com.linecorp.centraldogma.common.RepositoryNotFoundException;
import com.linecorp.centraldogma.common.Revision;

abstract class AbstractWatcher implements Watcher {

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

    private enum State {
        INIT,
        STARTED,
        STOPPED
    }

    private final ScheduledExecutorService watchScheduler;
    private final String projectName;
    private final String repositoryName;
    private final String pathPattern;
    private final boolean errorOnEntryNotFound;
    private final long delayOnSuccessMillis;
    private final long initialDelayMillis;
    private final long maxDelayMillis;
    private final double multiplier;
    private final double jitterRate;

    private final List, Executor>> updateListeners =
            new CopyOnWriteArrayList<>();
    private final AtomicReference state = new AtomicReference<>(State.INIT);
    private final CompletableFuture> initialValueFuture = new CompletableFuture<>();

    @Nullable
    private volatile Latest latest;
    @Nullable
    private volatile ScheduledFuture currentScheduleFuture;
    @Nullable
    private volatile CompletableFuture currentWatchFuture;

    AbstractWatcher(ScheduledExecutorService watchScheduler, String projectName, String repositoryName,
                    String pathPattern, boolean errorOnEntryNotFound, long delayOnSuccessMillis,
                    long initialDelayMillis, long maxDelayMillis, double multiplier, double jitterRate) {
        this.watchScheduler = watchScheduler;
        this.projectName = projectName;
        this.repositoryName = repositoryName;
        this.pathPattern = pathPattern;
        this.errorOnEntryNotFound = errorOnEntryNotFound;
        this.delayOnSuccessMillis = delayOnSuccessMillis;
        this.initialDelayMillis = initialDelayMillis;
        this.maxDelayMillis = maxDelayMillis;
        this.multiplier = multiplier;
        this.jitterRate = jitterRate;
    }

    @Override
    public ScheduledExecutorService watchScheduler() {
        return watchScheduler;
    }

    @Override
    public CompletableFuture> initialValueFuture() {
        return initialValueFuture;
    }

    @Override
    public Latest latest() {
        final Latest latest = this.latest;
        if (latest == null) {
            throw new IllegalStateException("value not available yet");
        }
        return latest;
    }

    /**
     * Starts to watch the file specified in the {@link Query} or the {@link PathPattern}
     * given with the constructor.
     */
    void start() {
        if (state.compareAndSet(State.INIT, State.STARTED)) {
            scheduleWatch(0);
        }
    }

    @Override
    public void close() {
        state.set(State.STOPPED);
        if (!initialValueFuture.isDone()) {
            initialValueFuture.cancel(false);
        }

        // Cancel any scheduled operations.
        final ScheduledFuture currentScheduleFuture = this.currentScheduleFuture;
        if (currentScheduleFuture != null && !currentScheduleFuture.isDone()) {
            currentScheduleFuture.cancel(false);
        }
        final CompletableFuture currentWatchFuture = this.currentWatchFuture;
        if (currentWatchFuture != null && !currentWatchFuture.isDone()) {
            currentWatchFuture.cancel(false);
        }
    }

    private boolean isStopped() {
        return state.get() == State.STOPPED;
    }

    @Override
    public void watch(BiConsumer listener) {
        watch(listener, watchScheduler);
    }

    @Override
    public void watch(BiConsumer listener, Executor executor) {
        requireNonNull(listener, "listener");
        checkState(!isStopped(), "watcher closed");
        updateListeners.add(new SimpleImmutableEntry<>(listener, executor));

        final Latest latest = this.latest;
        if (latest != null) {
            // There's a chance that listener.accept(...) is called twice for the same value
            // if this watch method is called:
            // - after " this.latest = newLatest;" is invoked.
            // - and before notifyListener() is called.
            // However, it's such a rare case and we usually call `watch` method right after creating a Watcher,
            // which means latest is probably not set yet, so we don't use a lock to guarantee
            // the atomicity.
            executor.execute(() -> listener.accept(latest.revision(), latest.value()));
        }
    }

    private void scheduleWatch(int numAttemptsSoFar) {
        if (isStopped()) {
            return;
        }

        final long delay;
        if (numAttemptsSoFar == 0) {
            delay = latest != null ? delayOnSuccessMillis : 0;
        } else {
            delay = nextDelayMillis(numAttemptsSoFar);
        }

        currentScheduleFuture = watchScheduler.schedule(() -> {
            currentScheduleFuture = null;
            doWatch(numAttemptsSoFar);
        }, delay, TimeUnit.MILLISECONDS);
    }

    private long nextDelayMillis(int numAttemptsSoFar) {
        final long nextDelayMillis;
        if (numAttemptsSoFar == 1) {
            nextDelayMillis = initialDelayMillis;
        } else {
            nextDelayMillis =
                    Math.min(saturatedMultiply(initialDelayMillis, Math.pow(multiplier, numAttemptsSoFar - 1)),
                             maxDelayMillis);
        }

        final long minJitter = (long) (nextDelayMillis * (1 - jitterRate));
        final long maxJitter = (long) (nextDelayMillis * (1 + jitterRate));
        final long bound = maxJitter - minJitter + 1;
        final long millis = random(bound);
        return Math.max(0, saturatedAdd(minJitter, millis));
    }

    private static long saturatedMultiply(long left, double right) {
        final double result = left * right;
        return result >= Long.MAX_VALUE ? Long.MAX_VALUE : (long) result;
    }

    private static long random(long bound) {
        assert bound > 0;
        final long mask = bound - 1;
        final Random random = ThreadLocalRandom.current();
        long result = random.nextLong();

        if ((bound & mask) == 0L) {
            // power of two
            result &= mask;
        } else { // reject over-represented candidates
            for (long u = result >>> 1; u + mask - (result = u % bound) < 0L; u = random.nextLong() >>> 1) {
                continue;
            }
        }

        return result;
    }

    private void doWatch(int numAttemptsSoFar) {
        if (isStopped()) {
            return;
        }

        final Latest latest = this.latest;
        final Revision lastKnownRevision = latest != null ? latest.revision() : Revision.INIT;
        final CompletableFuture> f = doWatch(lastKnownRevision);

        currentWatchFuture = f;
        f.thenAccept(newLatest -> {
             currentWatchFuture = null;
             if (newLatest != null) {
                 this.latest = newLatest;
                 logger.debug("watcher noticed updated file {}/{}{}: rev={}",
                              projectName, repositoryName, pathPattern, newLatest.revision());
                 notifyListeners(newLatest);
                 if (!initialValueFuture.isDone()) {
                     initialValueFuture.complete(newLatest);
                 }
             }

             // Watch again for the next change.
             scheduleWatch(0);
         })
         .exceptionally(thrown -> {
             currentWatchFuture = null;
             try {
                 final Throwable cause = thrown instanceof CompletionException ? thrown.getCause() : thrown;
                 boolean logged = false;
                 if (cause instanceof CentralDogmaException) {
                     if (cause instanceof EntryNotFoundException) {
                         if (!initialValueFuture.isDone() && errorOnEntryNotFound) {
                             initialValueFuture.completeExceptionally(thrown);
                             close();
                             return null;
                         }
                         logger.info("{}/{}{} does not exist yet; trying again",
                                     projectName, repositoryName, pathPattern);
                         logged = true;
                     } else if (cause instanceof RepositoryNotFoundException) {
                         logger.info("{}/{} does not exist yet; trying again",
                                     projectName, repositoryName);
                         logged = true;
                     }
                 }

                 if (cause instanceof CancellationException) {
                     // Cancelled by close()
                     return null;
                 }

                 if (!logged) {
                     logger.warn("Failed to watch a file ({}/{}{}) at Central Dogma; trying again",
                                 projectName, repositoryName, pathPattern, cause);
                 }

                 scheduleWatch(numAttemptsSoFar + 1);
             } catch (Throwable t) {
                 logger.error("Unexpected exception while watching a file at Central Dogma:", t);
             }
             return null;
         });
    }

    abstract CompletableFuture> doWatch(Revision lastKnownRevision);

    private void notifyListeners(Latest latest) {
        if (isStopped()) {
            // Do not notify after stopped.
            return;
        }

        for (Map.Entry, Executor> entry : updateListeners) {
            final BiConsumer listener = entry.getKey();
            final Executor executor = entry.getValue();
            executor.execute(() -> {
                try {
                    listener.accept(latest.revision(), latest.value());
                } catch (Exception e) {
                    logger.warn("Exception thrown for watcher ({}/{}{}): rev={}",
                                projectName, repositoryName, pathPattern, latest.revision(), e);
                }
            });
        }
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).omitNullValues()
                          .add("watchScheduler", watchScheduler)
                          .add("projectName", projectName)
                          .add("repositoryName", repositoryName)
                          .add("pathPattern", pathPattern)
                          .add("errorOnEntryNotFound", errorOnEntryNotFound)
                          .add("delayOnSuccessMillis", delayOnSuccessMillis)
                          .add("initialDelayMillis", initialDelayMillis)
                          .add("maxDelayMillis", maxDelayMillis)
                          .add("multiplier", multiplier)
                          .add("jitterRate", jitterRate)
                          .add("latest", latest)
                          .toString();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy