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

com.github.phantomthief.zookeeper.ZkBasedNodeResource Maven / Gradle / Ivy

The newest version!
package com.github.phantomthief.zookeeper;

import static com.github.phantomthief.util.MoreSuppliers.lazy;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static com.google.common.util.concurrent.Futures.addCallback;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import static java.lang.Thread.MIN_PRIORITY;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;

import org.apache.commons.lang3.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;

import com.github.phantomthief.util.ThrowableBiConsumer;
import com.github.phantomthief.util.ThrowableBiFunction;
import com.github.phantomthief.util.ThrowableConsumer;
import com.github.phantomthief.util.ThrowableFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

/**
 * @author w.vela
 */
public final class ZkBasedNodeResource implements Closeable {

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

    private static final int UNKNOWN = 0;
    private static final int EXISTS = 1;
    private static final int NOT_EXISTS = 2;

    private final Object lock = new Object();

    private final ThrowableBiFunction factory;
    private final ThrowableBiFunction, Exception> refreshFactory;
    private final Predicate cleanup;
    private final long waitStopPeriod;
    private final T emptyObject;
    private final BiConsumer onResourceChange;
    private final Supplier nodeCache;
    private final Runnable nodeCacheShutdown;
    private final BiConsumer factoryFailedListener;

    @GuardedBy("lock")
    private volatile T resource;

    @GuardedBy("lock")
    private volatile boolean emptyLogged = false;

    @GuardedBy("lock")
    private volatile boolean closed = false;

    /**
     * {@link #UNKNOWN}, {@link #EXISTS} or {@link #NOT_EXISTS}
     */
    private volatile int zkNodeExists = UNKNOWN;

    private volatile boolean hasAddListener = false;
    private volatile Runnable nodeCacheRemoveListener;

    private ZkBasedNodeResource(Builder builder) {
        this.factory = builder.factory;
        this.refreshFactory = builder.refreshFactory;
        this.cleanup = builder.cleanup;
        this.waitStopPeriod = builder.waitStopPeriod;
        this.emptyObject = builder.emptyObject;
        this.onResourceChange = builder.onResourceChange;
        this.nodeCacheShutdown = builder.nodeCacheShutdown;
        this.nodeCache = lazy(builder.cacheFactory);
        this.factoryFailedListener = (d, t) -> {
            for (ThrowableBiConsumer failedListener : builder.factoryFailedListeners) {
                try {
                    failedListener.accept(d, t);
                } catch (Throwable e) {
                    logger.error("", e);
                }
            }
        };
    }

    /**
     * use {@link #newGenericBuilder()} instead
     */
    @Deprecated
    @Nonnull
    public static Builder newBuilder() {
        return new Builder<>();
    }

    @CheckReturnValue
    @Nonnull
    public static  GenericZkBasedNodeBuilder newGenericBuilder() {
        return new GenericZkBasedNodeBuilder<>(newBuilder());
    }

    private static String path(NodeCache nodeCache) {
        try {
            if (nodeCache == null) {
                return "n/a";
            }
            Field f = NodeCache.class.getDeclaredField("path");
            f.setAccessible(true);
            return (String) f.get(nodeCache);
        } catch (Throwable e) {
            logger.error("Ops.fail to get path from node:{}, exception:{}", nodeCache,
                    e.toString());
            return null;
        }
    }

    private static String zkConn(CuratorFramework zk) {
        String result = zk.getZookeeperClient().getCurrentConnectionString();
        if (StringUtils.isNotBlank(zk.getNamespace())) {
            result += "[" + zk.getNamespace() + "]";
        }
        return result;
    }

    public ZkNode zkNode() {
        T t = get();
        return new ZkNode<>(t, zkNodeExists == EXISTS);
    }

    public T get() {
        checkClosed();
        if (resource == null) {
            if (zkNodeExists == NOT_EXISTS) { // for performance, short circuit it outside sync block.
                return emptyObject;
            }
            synchronized (lock) {
                checkClosed();
                if (resource == null) {
                    NodeCache cache = nodeCache.get();
                    tryAddListener(cache);
                    ChildData currentData = cache.getCurrentData();
                    if (currentData == null || currentData.getData() == null) {
                        zkNodeExists = NOT_EXISTS;
                        if (!emptyLogged) { // 只在刚开始一次或者几次打印这个log
                            logger.info("found no zk path for:{}:{}, using empty data:{}",
                                    zkConn(cache.getClient()), path(cache), emptyObject);
                            emptyLogged = true;
                        }
                        return emptyObject;
                    }
                    zkNodeExists = EXISTS;
                    try {
                        resource = factory.apply(currentData.getData(), currentData.getStat());
                        if (onResourceChange != null) {
                            onResourceChange.accept(resource, emptyObject);
                        }
                    } catch (Exception e) {
                        factoryFailedListener.accept(currentData, e);
                        throwIfUnchecked(e);
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        return resource;
    }

    private void checkClosed() {
        if (closed) {
            throw new IllegalStateException("zkNode has been closed.");
        }
    }

    private void tryAddListener(NodeCache cache) {
        if (!hasAddListener) {
            NodeCacheListener nodeCacheListener = () -> {
                T oldResource;
                synchronized (lock) {
                    ChildData data = cache.getCurrentData();
                    oldResource = resource;
                    if (data != null && data.getData() != null) {
                        zkNodeExists = EXISTS;
                        ListenableFuture future = refreshFactory.apply(data.getData(),
                                data.getStat());
                        addCallback(future, new FutureCallback() {

                            @Override
                            public void onSuccess(@Nullable T result) {
                                resource = result;
                                cleanup(resource, oldResource, cache);
                            }

                            @Override
                            public void onFailure(Throwable t) {
                                factoryFailedListener.accept(data, t);
                                logger.error("", t);
                            }
                        }, directExecutor());
                    } else {
                        zkNodeExists = NOT_EXISTS;
                        resource = null;
                        emptyLogged = false;
                        cleanup(resource, oldResource, cache);
                    }
                }
            };
            cache.getListenable().addListener(nodeCacheListener);
            nodeCacheRemoveListener = () -> cache.getListenable().removeListener(nodeCacheListener);
            hasAddListener = true;
        }
    }

    private void cleanup(T currentResource, T oldResource, NodeCache nodeCache) {
        if (oldResource != null && oldResource != emptyObject) {
            if (currentResource == oldResource) {
                logger.warn(
                        "[BUG!!!!] should NOT occurred, old resource is same as current, path:{}, {}",
                        path(nodeCache), oldResource);
            } else {
                new ThreadFactoryBuilder() //
                        .setNameFormat("old [" + oldResource.getClass().getSimpleName()
                                + "] cleanup thread-[%d]")
                        .setUncaughtExceptionHandler(
                                (t, e) -> logger.error("fail to cleanup resource, path:{}, {}",
                                        path(nodeCache), oldResource.getClass().getSimpleName(), e)) //
                        .setPriority(MIN_PRIORITY) //
                        .setDaemon(true) //
                        .build() //
                        .newThread(() -> {
                            do {
                                if (waitStopPeriod > 0) {
                                    sleepUninterruptibly(waitStopPeriod, MILLISECONDS);
                                }
                                if (cleanup.test(oldResource)) {
                                    break;
                                }
                            } while (true);
                            if (onResourceChange != null) {
                                onResourceChange.accept(currentResource, oldResource);
                            }
                        }).start();
                return;
            }
        }
        if (onResourceChange != null) {
            onResourceChange.accept(currentResource, oldResource);
        }
    }

    @Override
    public void close() {
        synchronized (lock) {
            if (nodeCacheShutdown != null) {
                nodeCacheShutdown.run();
            }
            if (nodeCacheRemoveListener != null) {
                nodeCacheRemoveListener.run();
            }
            if (resource != null && resource != emptyObject && cleanup != null) {
                cleanup.test(resource);
            }
            closed = true;
        }
    }

    public boolean isClosed() {
        return closed;
    }

    /**
     * use {@link #newGenericBuilder()} instead
     */
    @Deprecated
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static final class Builder {

        private ThrowableBiFunction factory;
        private ThrowableBiFunction, Exception> refreshFactory;
        private Supplier cacheFactory;
        private Predicate cleanup;
        private long waitStopPeriod;
        private E emptyObject;
        private BiConsumer onResourceChange;
        private Runnable nodeCacheShutdown;
        private ListeningExecutorService refreshExecutor;
        private List> factoryFailedListeners = new ArrayList<>();

        @CheckReturnValue
        @Nonnull
        public  Builder addFactoryFailedListener(
                @Nonnull ThrowableConsumer listener) {
            checkNotNull(listener);
            return addFactoryFailedListener((d, t) -> listener.accept(t));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder addFactoryFailedListener(
                @Nonnull ThrowableBiConsumer listener) {
            factoryFailedListeners.add(checkNotNull(listener));
            return (Builder) this;
        }

        /**
         * use {@link #withFactoryEx}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public  Builder
                withFactory(@Nonnull BiFunction factory) {
            return withFactoryEx(factory::apply);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withFactoryEx(
                @Nonnull ThrowableBiFunction factory) {
            Builder thisBuilder = (Builder) this;
            thisBuilder.factory = (ThrowableBiFunction) factory;
            return thisBuilder;
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshStringFactory(
                @Nonnull ThrowableBiFunction factory) {
            return withRefreshStringFactory(null, factory);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshStringFactory(
                @Nullable ListeningExecutorService executor,
                @Nonnull ThrowableBiFunction factory) {
            return withRefreshFactory(executor,
                    (b, s) -> factory.apply(b == null ? null : new String(b), s));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshFactory(
                @Nonnull ThrowableBiFunction factory) {
            return withRefreshFactory(null, factory);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshFactory(@Nullable ListeningExecutorService executor,
                @Nonnull ThrowableBiFunction factory) {
            Builder thisBuilder = (Builder) this;
            if (executor == null) {
                thisBuilder.refreshFactory = (b, s) -> {
                    try {
                        return immediateFuture(factory.apply(b, s));
                    } catch (Throwable t) {
                        return immediateFailedFuture(t);
                    }
                };
            } else {
                thisBuilder.refreshFactory = (b, s) -> executor.submit(() -> factory.apply(b, s));
            }
            return thisBuilder;
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withAsyncRefreshStringFactory(
                @Nonnull ThrowableBiFunction, Exception> factory) {
            return withAsyncRefreshFactory(
                    (b, s) -> factory.apply(b == null ? null : new String(b), s));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withAsyncRefreshFactory(
                @Nonnull ThrowableBiFunction, Exception> factory) {
            Builder thisBuilder = (Builder) this;
            thisBuilder.refreshFactory = factory;
            return thisBuilder;
        }

        @CheckReturnValue
        @Nonnull
        public  Builder onResourceChange(BiConsumer callback) {
            Builder thisBuilder = (Builder) this;
            thisBuilder.onResourceChange = (BiConsumer) callback;
            return thisBuilder;
        }

        /**
         * use {@link #withFactoryEx}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public  Builder withFactory(@Nonnull Function factory) {
            return withFactoryEx(factory::apply);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder
                withFactoryEx(@Nonnull ThrowableFunction factory) {
            return withFactoryEx((b, s) -> factory.apply(b));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshStringFactory(
                @Nonnull ThrowableFunction factory) {
            return withRefreshStringFactory(null, factory);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshStringFactory(
                @Nullable ListeningExecutorService executor,
                @Nonnull ThrowableFunction factory) {
            return withRefreshStringFactory(executor, (b, s) -> factory.apply(b));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshFactory(
                @Nonnull ThrowableFunction factory) {
            return withRefreshFactory(null, factory);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withRefreshFactory(@Nullable ListeningExecutorService executor,
                @Nonnull ThrowableFunction factory) {
            return withRefreshFactory(executor, (b, s) -> factory.apply(b));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withAsyncRefreshStringFactory(
                ThrowableFunction, Exception> factory) {
            return withAsyncRefreshStringFactory((b, s) -> factory.apply(b));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withAsyncRefreshFactory(
                ThrowableFunction, Exception> factory) {
            return withAsyncRefreshFactory((b, s) -> factory.apply(b));
        }

        @CheckReturnValue
        @Nonnull
        public  Builder asyncRefresh(@Nonnull ListeningExecutorService executor) {
            Builder thisBuilder = (Builder) this;
            thisBuilder.refreshExecutor = checkNotNull(executor);
            return thisBuilder;
        }

        /**
         * use {@link #withStringFactoryEx}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public  Builder withStringFactory(BiFunction factory) {
            return withStringFactoryEx(factory::apply);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withStringFactoryEx(
                ThrowableBiFunction factory) {
            return withFactoryEx((b, s) -> factory.apply(b == null ? null : new String(b), s));
        }

        /**
         * use {@link #withStringFactoryEx}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public  Builder withStringFactory(Function factory) {
            return withStringFactoryEx(factory::apply);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder
                withStringFactoryEx(ThrowableFunction factory) {
            return withStringFactoryEx((b, s) -> factory.apply(b));
        }

        @CheckReturnValue
        @Nonnull
        public Builder withCacheFactory(Supplier cacheFactory) {
            this.cacheFactory = cacheFactory;
            return this;
        }

        @CheckReturnValue
        @Nonnull
        public Builder withCacheFactory(String path, CuratorFramework curator) {
            return withCacheFactory(path, () -> curator);
        }

        @CheckReturnValue
        @Nonnull
        public Builder withCacheFactory(String path, Supplier curatorFactory) {
            this.cacheFactory = () -> {
                CuratorFramework thisClient = curatorFactory.get();
                if (thisClient.getState() != CuratorFrameworkState.STARTED) {
                    thisClient.start();
                }
                NodeCache buildingCache = new NodeCache(thisClient, path);
                try {
                    buildingCache.start();
                    // not safety check but do it better. due to IE breaks in rebuild.
                    if (Thread.currentThread().isInterrupted()) {
                        Thread.interrupted();
                    }
                    buildingCache.rebuild();
                    this.nodeCacheShutdown = () -> {
                        try {
                            buildingCache.close();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    };
                    return buildingCache;
                } catch (Throwable e) {
                    throwIfUnchecked(e);
                    throw new RuntimeException(e);
                }
            };
            return this;
        }

        /**
         * using {@link #withCleanupConsumer(ThrowableConsumer)}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public  Builder withCleanup(ThrowableConsumer cleanup) {
            return withCleanupConsumer(cleanup);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder
                withCleanupConsumer(ThrowableConsumer cleanup) {
            Builder thisBuilder = (Builder) this;
            thisBuilder.cleanup = t -> {
                try {
                    cleanup.accept(t);
                    return true;
                } catch (Throwable e) {
                    logger.error("Ops. fail to close, path:{}", t, e);
                    return false;
                }
            };
            return thisBuilder;
        }

        /**
         * using {@link #withCleanupPredicate(Predicate)}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public  Builder withCleanup(Predicate cleanup) {
            return withCleanupPredicate(cleanup);
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withCleanupPredicate(Predicate cleanup) {
            Builder thisBuilder = (Builder) this;
            thisBuilder.cleanup = (Predicate) cleanup;
            return thisBuilder;
        }

        @CheckReturnValue
        @Nonnull
        public Builder withWaitStopPeriod(long waitStopPeriod) {
            this.waitStopPeriod = waitStopPeriod;
            return this;
        }

        @CheckReturnValue
        @Nonnull
        public  Builder withEmptyObject(E1 emptyObject) {
            Builder thisBuilder = (Builder) this;
            thisBuilder.emptyObject = emptyObject;
            return thisBuilder;
        }

        @Nonnull
        public  ZkBasedNodeResource build() {
            ensure();
            return new ZkBasedNodeResource(this);
        }

        private void ensure() {
            checkNotNull(factory);
            checkNotNull(cacheFactory);
            if (refreshFactory == null) {
                if (refreshExecutor != null) {
                    refreshFactory = (bs, stat) -> refreshExecutor
                            .submit(() -> factory.apply(bs, stat));
                } else {
                    refreshFactory = (bs, stat) -> {
                        try {
                            return immediateFuture(factory.apply(bs, stat));
                        } catch (Throwable t) {
                            return immediateFailedFuture(t);
                        }
                    };
                }
            }

            if (onResourceChange != null) {
                BiConsumer temp = onResourceChange;
                onResourceChange = (t, u) -> { // safe wrapper
                    try {
                        temp.accept(t, u);
                    } catch (Throwable e) {
                        logger.error("Ops.", e);
                    }
                };
            }

            if (cleanup == null) {
                withCleanup(t -> {
                    if (t instanceof Closeable) {
                        try {
                            ((Closeable) t).close();
                        } catch (Throwable e) {
                            throw propagate(e);
                        }
                    }
                });
            }
        }
    }
}