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

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

package com.github.phantomthief.zookeeper;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static com.google.common.util.concurrent.Uninterruptibles.awaitUninterruptibly;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import static java.lang.Thread.MIN_PRIORITY;
import static java.lang.Thread.holdsLock;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.commons.lang3.StringUtils.removeStart;
import static org.apache.curator.framework.recipes.cache.TreeCacheEvent.Type.CONNECTION_LOST;
import static org.apache.curator.framework.recipes.cache.TreeCacheEvent.Type.CONNECTION_SUSPENDED;
import static org.apache.curator.framework.recipes.cache.TreeCacheEvent.Type.INITIALIZED;
import static org.apache.curator.utils.ThreadUtils.newThreadFactory;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.Closeable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.function.BiConsumer;
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.concurrent.GuardedBy;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.TreeCache;
import org.slf4j.Logger;

import com.github.phantomthief.util.ThrowableConsumer;
import com.github.phantomthief.util.ThrowableFunction;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

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

    private static Logger logger = getLogger(ZkBasedTreeNodeResource.class);

    private final Object lock = new Object();

    private final ThrowableFunction, T, Exception> factory;
    private final Predicate cleanup;
    private final long waitStopPeriod;
    private final BiConsumer onResourceChange;
    private final Supplier curatorFrameworkFactory;
    private final String path;

    @GuardedBy("lock")
    private volatile TreeCache treeCache;

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

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

    private ZkBasedTreeNodeResource(Builder builder) {
        this.factory = builder.factory;
        this.cleanup = builder.cleanup;
        this.path = builder.path;
        this.waitStopPeriod = builder.waitStopPeriod;
        this.curatorFrameworkFactory = builder.curatorFrameworkFactory;
        this.onResourceChange = builder.onResourceChange;
    }

    public static  Builder newBuilder() {
        return new Builder<>();
    }

    private void ensureTreeCacheReady() {
        assert holdsLock(lock);
        if (treeCache == null) {
            try {
                CountDownLatch countDownLatch = new CountDownLatch(1);
                TreeCache building = TreeCache.newBuilder(curatorFrameworkFactory.get(), path) //
                        .setCacheData(true) //
                        .setExecutor(newSingleThreadExecutor(
                                newThreadFactory("TreeCache-[" + path + "]")))
                        .build();
                building.getListenable().addListener((c, event) -> {
                    if (event.getType() == INITIALIZED) {
                        countDownLatch.countDown();
                        return;
                    }
                    if (countDownLatch.getCount() > 0) {
                        logger.debug("ignore event before initialized:{}=>{}", event.getType(),
                                path);
                        return;
                    }
                    if (event.getType() == CONNECTION_SUSPENDED
                            || event.getType() == CONNECTION_LOST) {
                        logger.info("ignore event:{} for tree node:{}", event.getType(), path);
                        return;
                    }
                    T oldResource;
                    synchronized (lock) {
                        oldResource = resource;
                        resource = doFactory();
                        cleanup(resource, oldResource);
                    }
                });
                building.start();
                awaitUninterruptibly(countDownLatch);
                treeCache = building;
            } catch (Throwable e) {
                throwIfUnchecked(e);
                throw new RuntimeException(e);
            }
        }
    }

    public T get() {
        checkClosed();
        if (resource == null) {
            synchronized (lock) {
                checkClosed();
                if (resource == null) {
                    ensureTreeCacheReady();
                    try {
                        resource = doFactory();
                        if (onResourceChange != null) {
                            onResourceChange.accept(resource, null);
                        }
                    } catch (Exception e) {
                        throwIfUnchecked(e);
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        return resource;
    }

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

    private void cleanup(T currentResource, T oldResource) {
        if (oldResource != null) {
            if (currentResource == oldResource) {
                logger.warn(
                        "[BUG!!!!] should NOT occured, old resource is same as current, path:{}, {}",
                        path, oldResource);
            } else {
                new ThreadFactoryBuilder() //
                        .setNameFormat("old [" + oldResource.getClass().getSimpleName()
                                + "] cleanup thread-[%d]")
                        .setUncaughtExceptionHandler(
                                (t, e) -> logger.error("fail to cleanup resource, path:{}, {}",
                                        path, 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 (resource != null && cleanup != null) {
                cleanup.test(resource);
            }
            if (treeCache != null) {
                treeCache.close();
            }
            closed = true;
        }
    }

    public boolean isClosed() {
        return closed;
    }

    private T doFactory() throws Exception {
        Map map = new HashMap<>();
        generateFullTree(map, treeCache, path);
        return factory.apply(map);
    }

    private void generateFullTree(Map map, TreeCache cache, String rootPath) {
        Map thisMap = cache.getCurrentChildren(rootPath);
        if (thisMap != null) {
            thisMap.values().forEach(c -> map.put(removeStart(c.getPath(), path), c));
            thisMap.values().forEach(c -> generateFullTree(map, cache, c.getPath()));
        }
    }

    public static final class Builder {

        private ThrowableFunction, E, Exception> factory;
        private String path;
        private Supplier curatorFrameworkFactory;
        private Predicate cleanup;
        private long waitStopPeriod;
        private BiConsumer onResourceChange;

        @CheckReturnValue
        @Nonnull
        public Builder path(String path) {
            this.path = path;
            return this;
        }

        /**
         * use {@link #factoryEx}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public Builder factory(Function, E> factory) {
            return factoryEx(factory::apply);
        }

        @CheckReturnValue
        @Nonnull
        public Builder
                factoryEx(ThrowableFunction, E, Exception> factory) {
            this.factory = factory;
            return this;
        }

        /**
         * use {@link #childDataFactoryEx}
         */
        @CheckReturnValue
        @Deprecated
        @Nonnull
        public Builder childDataFactory(Function, E> factory) {
            return childDataFactoryEx(factory::apply);
        }

        @CheckReturnValue
        @Nonnull
        public Builder
                childDataFactoryEx(ThrowableFunction, E, Exception> factory) {
            checkNotNull(factory);
            return factoryEx(map -> factory.apply(map.values()));
        }

        /**
         * use {@link #keysFactoryEx}
         */
        @Deprecated
        @CheckReturnValue
        @Nonnull
        public Builder keysFactory(Function, E> factory) {
            return keysFactoryEx(factory::apply);
        }

        @CheckReturnValue
        @Nonnull
        public Builder
                keysFactoryEx(ThrowableFunction, E, Exception> factory) {
            checkNotNull(factory);
            return factoryEx(map -> factory.apply(map.keySet()));
        }

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

        @CheckReturnValue
        @Nonnull
        public Builder curator(Supplier curatorFactory) {
            this.curatorFrameworkFactory = curatorFactory;
            return this;
        }

        @CheckReturnValue
        @Nonnull
        public Builder curator(CuratorFramework curator) {
            this.curatorFrameworkFactory = () -> curator;
            return this;
        }

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

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

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

        @Nonnull
        public ZkBasedTreeNodeResource build() {
            ensure();
            return new ZkBasedTreeNodeResource<>(this);
        }

        private void ensure() {
            checkNotNull(factory);
            checkNotNull(curatorFrameworkFactory);

            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) {
                cleanup(t -> {
                    if (t instanceof Closeable) {
                        try {
                            ((Closeable) t).close();
                        } catch (Throwable e) {
                            throwIfUnchecked(e);
                            throw new RuntimeException(e);
                        }
                    }
                });
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy