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

com.github.phantomthief.failover.util.SharedResourceV2 Maven / Gradle / Ivy

There is a newer version: 0.1.32
Show newest version
package com.github.phantomthief.failover.util;

import static com.github.phantomthief.tuple.Tuple.tuple;
import static com.github.phantomthief.util.MoreSuppliers.lazy;
import static org.slf4j.LoggerFactory.getLogger;

import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Supplier;

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

import org.slf4j.Logger;

import com.github.phantomthief.tuple.TwoTuple;
import com.github.phantomthief.util.ThrowableConsumer;

/**
 * @author myco
 * Created on 2020-10-22
 * @see SharedResource
 * 新版 shared resource 实现,行为具体如下:
 */
public class SharedResourceV2 {

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

    private final ConcurrentMap> resourceMap = new ConcurrentHashMap<>();
    private final Function factory;
    private final ThrowableConsumer cleanup;

    public SharedResourceV2(@Nonnull Function factory,
            @Nonnull ThrowableConsumer cleanup) {
        this.factory = factory;
        this.cleanup = cleanup;
    }

    /**
     * 不管是否成功,都只get一次,然后缓存起来
     */
    private static class OnceSupplier implements Supplier {

        private final Supplier> delegate;

        public OnceSupplier(Object key, Supplier delegate) {
            this.delegate = lazy(() -> {
                T value = null;
                Throwable throwable = null;
                try {
                    value = delegate.get();
                    logger.debug("create shared resource for key: [{}] => [{}]", key, value);
                } catch (Throwable t) {
                    throwable = t;
                }
                return tuple(value, throwable);
            });
        }

        @Override
        public T get() {
            TwoTuple tuple = delegate.get();
            Throwable throwable = tuple.getSecond();
            if (throwable != null) {
                throw new OnceBrokenException(throwable);
            } else {
                return tuple.getFirst();
            }
        }
    }

    private static class ResourceWrapper {

        private final K key;
        private final OnceSupplier resourceSupplier;

        @GuardedBy("ResourceWrapper::this")
        private volatile int counter = 0;
        @GuardedBy("ResourceWrapper::this")
        private volatile boolean expired = false;

        public ResourceWrapper(K key, @Nonnull Supplier resourceSupplier) {
            this.key = key;
            this.resourceSupplier = new OnceSupplier<>(key, resourceSupplier);
        }

        @Nonnull
        public V get() {
            return resourceSupplier.get();
        }

        public int count() {
            return counter;
        }

        public boolean incr() {
            synchronized (this) {
                if (!expired) {
                    counter++;
                    logger.debug("incr success: [{}], refCount: [{}], expired: [{}]", key, counter, expired);
                    return true;
                }
            }
            return false;
        }

        public boolean decr() {
            synchronized (this) {
                if (counter <= 0) {
                    throw new AssertionError("INVALID INTERNAL STATE:" + key);
                }
                if (!expired) {
                    counter--;
                    if (counter <= 0) {
                        expired = true;
                    }
                    logger.debug("decr success: [{}], refCount: [{}], expired: [{}]", key, counter, expired);
                    return true;
                }
            }
            return false;
        }
    }

    @Nonnull
    private ResourceWrapper ensureWrapperExist(@Nonnull K key) {
        return resourceMap.compute(key, (k, v) -> {
            if (v == null || v.expired) {
                return new ResourceWrapper<>(k,
                        () -> Objects.requireNonNull(factory.apply(k), "factory 不应返回 null, key: " + key));
            } else {
                return v;
            }
        });
    }

    private void removeExpiredWrapper(@Nonnull K key) {
        resourceMap.compute(key, (k, v) -> {
            if (v != null && v.expired) {
                return null;
            } else {
                return v;
            }
        });
    }

    /**
     * 获取之前注册且未被注销的资源。
     * 使用相同 key 并发调用 get 和 register/unregister 时,本方法的返回值是不确定的(可能是null,也可能是之前注册的值)
     * 真实使用场景下,应避免相同的 key 并发 get/register/unregister,虽然本类是线程安全的,但是并发调用这些方法会让人很难理解
     * 真正共享的资源到底是哪一个
     */
    @Nullable
    public V get(@Nonnull K key) {
        ResourceWrapper resourceWrapper = resourceMap.get(key);
        if (resourceWrapper != null) {
            return resourceWrapper.get();
        }
        return null;
    }

    /**
     * 这个方法实现的过程中,踩了挺多坑。这里把当前实现的关键思路记录一下,看后续有没有更好的实现方法:
     * 1. 如果之前成功创建过相同 key 的资源,就应当复用之前创建的资源;所以不能一上来就用 factory 创建资源
     * 2. 为了避免 unregister 已经在清理中的资源再次被增加引用计数,肯定需要加锁;为了避免全局锁,首先想到可以对 key 加锁
     * 3. 但是 key 有可能出现不同对象,但是 equals & hashcode 相同的情况,所以直接对 key 加锁会有问题
     * 4. key 不能用于加锁,那能不能对 value resourceWrapper 加锁呢?也有问题,因为为了避免资源泄漏,value 在引用计数减到 0 的时候
     * 需要删掉,这样导致有可能锁没有加在同一个对象上
     * 5. 所以目前采用这样的方式:我们在 resourceWrapper 上记录一个状态,如果某个资源的引用计数降到 0 则认为这个资源已经过期,
     * 不能继续使用/增加计数了;在 register 和 unregister 过程中发现已经过期的资源,采用清理 + 重建的方式来解决
     */
    @Nonnull
    public V register(@Nonnull K key) {
        boolean incr = false;
        ResourceWrapper resourceWrapper = null;
        while (!incr) {
            resourceWrapper = ensureWrapperExist(key);
            // 确保引用计数加成功才退出
            incr = resourceWrapper.incr();
        }
        try {
            // 尝试初始化资源
            return resourceWrapper.get();
        } catch (Throwable t) {
            // 初始化资源失败的话,把引用计数恢复一下,同时清理下 resourceMap 里引用计数降到 0 的记录
            unregister(key);
            throw t;
        }
    }

    @Nonnull
    public V unregister(@Nonnull K key) {
        boolean decr = false;
        ResourceWrapper resourceWrapper = null;
        while (!decr) {
            resourceWrapper = resourceMap.get(key);
            if (resourceWrapper == null) {
                throw new IllegalStateException("non paired unregister call for key:" + key);
            }
            decr = resourceWrapper.decr();
            removeExpiredWrapper(key);
        }
        V resource = resourceWrapper.get();
        if (resourceWrapper.expired) {
            try {
                cleanup.accept(resource);
                logger.info("cleanup resource: [{}] => [{}]", key, resource);
            } catch (Throwable e) {
                throw new UnregisterFailedException(e, resource);
            }
        }
        return resource;
    }

    public static class UnregisterFailedException extends RuntimeException {

        private final Object removed;

        private UnregisterFailedException(Throwable cause, Object removed) {
            super(cause);
            this.removed = removed;
        }

        @SuppressWarnings("unchecked")
        public  T getRemoved() {
            return (T) removed;
        }
    }

    public static class OnceBrokenException extends RuntimeException {

        private OnceBrokenException(Throwable cause) {
            super("resource broken", cause);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy