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

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

Go to download

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

The newest version!
/*
 * Copyright 2019 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.MoreObjects.toStringHelper;
import static java.util.Objects.requireNonNull;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.BiConsumer;
import java.util.function.Function;

import javax.annotation.Nullable;

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

import com.google.common.collect.Maps;

import com.linecorp.centraldogma.common.Revision;

final class MappingWatcher implements Watcher {

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

    static  MappingWatcher of(Watcher parent, Function mapper,
                                          Executor executor, boolean closeParentWhenClosing) {
        requireNonNull(parent, "parent");
        requireNonNull(mapper, "mapper");
        requireNonNull(executor, "executor");
        // TODO(minwoo): extract mapper function and combine it with the new mapper.
        return new MappingWatcher<>(parent, mapper, executor, closeParentWhenClosing);
    }

    private final Watcher parent;
    private final Function mapper;
    private final Executor mapperExecutor;
    private final boolean closeParentWhenClosing;
    private final CompletableFuture> initialValueFuture = new CompletableFuture<>();
    private final List, Executor>> updateListeners =
            new CopyOnWriteArrayList<>();

    @Nullable
    private volatile Latest mappedLatest;
    private volatile boolean closed;

    MappingWatcher(Watcher parent, Function mapper, Executor mapperExecutor,
                   boolean closeParentWhenClosing) {
        this.parent = parent;
        this.mapper = mapper;
        this.mapperExecutor = mapperExecutor;
        this.closeParentWhenClosing = closeParentWhenClosing;
        parent.initialValueFuture().exceptionally(cause -> {
            initialValueFuture.completeExceptionally(cause);
            return null;
        });
        parent.watch((revision, value) -> {
            if (closed) {
                return;
            }
            final U mappedValue;
            try {
                mappedValue = mapper.apply(value);
            } catch (Exception e) {
                logger.warn("Unexpected exception is raised from mapper.apply(). mapper: {}", mapper, e);
                if (!initialValueFuture.isDone()) {
                    initialValueFuture.completeExceptionally(e);
                }
                close();
                return;
            }
            final Latest oldLatest = mappedLatest;
            if (oldLatest != null && Objects.equals(oldLatest.value(), mappedValue)) {
                return;
            }

            // mappedValue can be nullable which is fine.
            final Latest newLatest = new Latest<>(revision, mappedValue);
            mappedLatest = newLatest;
            notifyListeners(newLatest);
            if (!initialValueFuture.isDone()) {
                initialValueFuture.complete(newLatest);
            }
        }, mapperExecutor);
    }

    private void notifyListeners(Latest latest) {
        if (closed) {
            return;
        }

        for (Map.Entry, Executor> entry : updateListeners) {
            final BiConsumer listener = entry.getKey();
            final Executor executor = entry.getValue();
            if (mapperExecutor == executor) {
                notifyListener(latest, listener);
            } else {
                executor.execute(() -> notifyListener(latest, listener));
            }
        }
    }

    private void notifyListener(Latest latest, BiConsumer listener) {
        try {
            listener.accept(latest.revision(), latest.value());
        } catch (Exception e) {
            logger.warn("Unexpected exception is raised from {}: rev={}",
                        listener, latest.revision(), e);
        }
    }

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

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

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

    @Override
    public void close() {
        closed = true;
        if (!initialValueFuture.isDone()) {
            initialValueFuture.cancel(false);
        }
        if (closeParentWhenClosing) {
            parent.close();
        }
    }

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

    @Override
    public void watch(BiConsumer listener, Executor executor) {
        requireNonNull(listener, "listener");
        requireNonNull(executor, "executor");
        updateListeners.add(Maps.immutableEntry(listener, executor));

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

    @Override
    public String toString() {
        return toStringHelper(this)
                .add("parent", parent)
                .add("mapper", mapper)
                .add("closed", closed)
                .toString();
    }
}