├── .travis.yml ├── .gitignore ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── phantomthief │ │ └── failover │ │ ├── impl │ │ ├── checker │ │ │ ├── DataSourceChecker.java │ │ │ └── SimplePortChecker.java │ │ ├── WeightListener.java │ │ ├── GcUtil.java │ │ ├── SimpleWeightFunction.java │ │ ├── WeightFunction.java │ │ ├── GenericRecoverableCheckFailoverBuilder.java │ │ ├── DummyFailover.java │ │ ├── AbstractWeightFunction.java │ │ ├── RecoverableCheckFailoverBuilder.java │ │ ├── GenericWeightFailoverBuilder.java │ │ ├── RatioWeightFunction.java │ │ ├── PriorityFailoverCheckTask.java │ │ ├── PartitionFailoverBuilder.java │ │ ├── ComboFailover.java │ │ ├── RecoverableCheckFailover.java │ │ ├── WeightFailoverCheckTask.java │ │ ├── PriorityFailoverManager.java │ │ └── PriorityGroupManager.java │ │ ├── exception │ │ └── NoAvailableResourceException.java │ │ ├── SimpleFailover.java │ │ ├── util │ │ ├── LcgRandomIterator.java │ │ ├── SharedCheckExecutorHolder.java │ │ ├── RandomListUtils.java │ │ ├── Weight.java │ │ ├── SharedResource.java │ │ ├── AliasMethod.java │ │ ├── FailoverUtils.java │ │ ├── ConcurrencyAware.java │ │ └── SharedResourceV2.java │ │ └── Failover.java └── test │ ├── resources │ └── logback.xml │ └── java │ └── com │ └── github │ └── phantomthief │ └── failover │ ├── WeighTestUtils.java │ ├── impl │ ├── SimpleWeightFunctionTest.java │ ├── benchmark │ │ ├── BenchmarkMain.java │ │ ├── WeightBenchmark.java │ │ ├── Group1PriorityFailover.java │ │ ├── Group1WeightFailover.java │ │ ├── Group2PartitionFailover.java │ │ └── Group2PriorityFailover.java │ ├── DummyFailoverTest.java │ ├── WeightFailoverTestLeakRecovery.java │ ├── WeightFailoverJmhTest.java │ ├── RecoverableCheckFailoverTest.java │ ├── checker │ │ └── SimplePortCheckerTest.java │ ├── RatioWeightFunctionTest.java │ ├── WeightFailoverCheckTaskTest.java │ ├── WeightFailoverTestMissingNode.java │ ├── ComboFailoverTest.java │ └── PriorityFailoverManagerTest.java │ └── util │ ├── FilteredFailoverTest.java │ ├── FailoverUtilsTest.java │ ├── RandomListUtilsTest.java │ ├── WeightTest.java │ ├── SharedResourceTest.java │ ├── RetryTest.java │ ├── ConcurrencyAwareTest.java │ └── SharedResourceV2Test.java └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk11 4 | after_success: 5 | - mvn clean test jacoco:report coveralls:report -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .idea 3 | *.iml 4 | 5 | # Mobile Tools for Java (J2ME) 6 | .mtj.tmp/ 7 | 8 | # Package Files # 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 14 | hs_err_pid* 15 | /target/ 16 | 17 | .project 18 | .settings 19 | .classpath 20 | 21 | .DS_Store 22 | 23 | .README.md.html 24 | 25 | pom.xml.releaseBackup 26 | release.properties 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/checker/DataSourceChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.checker; 2 | 3 | import java.sql.Connection; 4 | 5 | import javax.sql.DataSource; 6 | 7 | /** 8 | * @author w.vela 9 | */ 10 | public class DataSourceChecker { 11 | 12 | public static boolean test(DataSource t) { 13 | try (Connection conn = t.getConnection()) { 14 | return conn != null; 15 | } catch (Throwable e) { 16 | return false; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level \(%F:%L\) - %msg %n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/WeighTestUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover; 2 | 3 | /** 4 | * @author w.vela 5 | * Created on 2019-01-04. 6 | */ 7 | public class WeighTestUtils { 8 | 9 | private static final double OFFSET = 0.3; 10 | 11 | public static boolean checkRatio(int a, int b, int ratio) { 12 | return between((double) a / b, (double) ratio - OFFSET, (double) ratio + OFFSET); 13 | } 14 | 15 | private static boolean between(double k, double min, double max) { 16 | return min <= k && k <= max; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/exception/NoAvailableResourceException.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.exception; 2 | 3 | /** 4 | * @author w.vela 5 | */ 6 | public class NoAvailableResourceException extends RuntimeException { 7 | 8 | private static final long serialVersionUID = -8580979490752713492L; 9 | 10 | public NoAvailableResourceException() { 11 | super(); 12 | } 13 | 14 | public NoAvailableResourceException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public NoAvailableResourceException(String message) { 19 | super(message); 20 | } 21 | 22 | public NoAvailableResourceException(Throwable cause) { 23 | super(cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/SimpleWeightFunctionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * @author huangli 9 | * Created on 2020-05-06 10 | */ 11 | public class SimpleWeightFunctionTest { 12 | 13 | @Test 14 | public void testFail() { 15 | SimpleWeightFunction f = new SimpleWeightFunction<>(0.5, 0.01); 16 | assertEquals(0.5, f.fail(1, 0, 0, 1, "R1")); 17 | assertEquals(0, f.fail(1, 0, 0, 0.5, "R1")); 18 | assertEquals(0, f.fail(1, 0, 0, 0, "R1")); 19 | } 20 | 21 | @Test 22 | public void testSuccess() { 23 | SimpleWeightFunction f = new SimpleWeightFunction<>(0.5, 0.5); 24 | assertEquals(0.5, f.success(1, 0, 0, 0, "R1")); 25 | assertEquals(1, f.success(1, 0, 0, 0.5, "R1")); 26 | assertEquals(1, f.success(1, 0, 0, 1, "R1")); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/benchmark/BenchmarkMain.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.benchmark; 2 | 3 | import org.openjdk.jmh.runner.Runner; 4 | import org.openjdk.jmh.runner.RunnerException; 5 | import org.openjdk.jmh.runner.options.Options; 6 | import org.openjdk.jmh.runner.options.OptionsBuilder; 7 | 8 | /** 9 | * @author huangli 10 | * Created on 2020-01-21 11 | */ 12 | public class BenchmarkMain { 13 | public static void main(String[] args) throws RunnerException { 14 | Options options = new OptionsBuilder() 15 | .include(Group1PriorityFailover.class.getSimpleName()) 16 | //.include(Group1WeightFailover.class.getSimpleName()) 17 | .include(Group2PriorityFailover.class.getSimpleName()) 18 | //.include(Group2PartitionFailover.class.getSimpleName()) 19 | .output(System.getProperty("user.home") + "/benchmark.txt") 20 | .build(); 21 | new Runner(options).run(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/DummyFailoverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static java.util.Collections.singletonList; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | /** 11 | * @author w.vela 12 | * Created on 2017-11-22. 13 | */ 14 | class DummyFailoverTest { 15 | 16 | @Test 17 | void testSupplier() { 18 | AtomicReference atomicReference = new AtomicReference<>(); 19 | DummyFailover testSupplier = DummyFailover.ofSingleSupplier(atomicReference::get); 20 | assertEquals(singletonList(atomicReference.get()), testSupplier.getAvailable()); 21 | atomicReference.set("test"); 22 | assertEquals(singletonList(atomicReference.get()), testSupplier.getAvailable()); 23 | atomicReference.set("test1"); 24 | assertEquals(singletonList(atomicReference.get()), testSupplier.getAvailable()); 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/WeightFailoverTestLeakRecovery.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 4 | import static java.util.Collections.singletonList; 5 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | /** 12 | * @author w.vela 13 | * Created on 2017-12-07. 14 | */ 15 | class WeightFailoverTestLeakRecovery { 16 | 17 | @Test 18 | void test() { 19 | boolean[] check = { false }; 20 | WeightFailover weightFailover = WeightFailover. newGenericBuilder() 21 | .checkDuration(10, MILLISECONDS) 22 | .checker(str -> { 23 | check[0] = true; 24 | return 0.0; 25 | }).build(singletonList("test"), 10); 26 | weightFailover.down("test"); 27 | sleepUninterruptibly(100, MILLISECONDS); 28 | assertTrue(check[0]); 29 | weightFailover.close(); 30 | check[0] = false; 31 | weightFailover.down("test"); 32 | sleepUninterruptibly(100, MILLISECONDS); 33 | check[0] = false; 34 | assertFalse(check[0]); 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/WeightListener.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | /** 4 | * 使用者可以注册权重监听器,用来监听特定的变化事件,比如资源调用失败,或者权重降低到最小值。 5 | * 6 | * @author huangli 7 | * Created on 2020-01-20 8 | */ 9 | public interface WeightListener { 10 | /** 11 | * 资源访问成功时被调用,当然,failover本身并不知道资源调用是否成功,它实际上是使用者调用 12 | * {@link com.github.phantomthief.failover.Failover#success(Object)}后触发的。 13 | * 14 | * 当权重没有变化的时候可能不会被调用,比如当前权重=最大权重的情况下,再次调用success。 15 | * 16 | * @param maxWeight 最大权重 17 | * @param minWeight 最小权重 18 | * @param priority 优先级 19 | * @param currentOldWeight 调用成功前的权重 20 | * @param currentNewWeight 调用成功后的权重 21 | * @param resource 相关资源 22 | */ 23 | default void onSuccess(double maxWeight, double minWeight, int priority, double currentOldWeight, 24 | double currentNewWeight, T resource) { 25 | } 26 | 27 | /** 28 | * 资源访问失败时被调用,当然,failover本身并不知道资源调用是否失败,它实际上是使用者调用 29 | * {@link com.github.phantomthief.failover.Failover#fail(Object)}和 30 | * {@link com.github.phantomthief.failover.Failover#down(Object)}后触发的。 31 | * 32 | * 当权重没有变化的时候可能不会被调用,比如当前权重=最小权重的情况下,再次调用fail。 33 | * 34 | * @param maxWeight 最大权重 35 | * @param minWeight 最小权重 36 | * @param priority 优先级 37 | * @param currentOldWeight 调用失败前的权重 38 | * @param currentNewWeight 调用失败后的权重 39 | * @param resource 相关资源 40 | */ 41 | default void onFail(double maxWeight, double minWeight, int priority, double currentOldWeight, 42 | double currentNewWeight, T resource) { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/GcUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import java.lang.ref.PhantomReference; 4 | import java.lang.ref.Reference; 5 | import java.lang.ref.ReferenceQueue; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import com.google.common.annotations.VisibleForTesting; 12 | 13 | 14 | /** 15 | * @author huangli 16 | * Created on 2019-12-30 17 | */ 18 | final class GcUtil { 19 | 20 | private static final Logger logger = LoggerFactory.getLogger(GcUtil.class); 21 | 22 | private static final ConcurrentHashMap, Runnable> refMap = new ConcurrentHashMap<>(); 23 | private static final ReferenceQueue REF_QUEUE = new ReferenceQueue<>(); 24 | 25 | @VisibleForTesting 26 | static ConcurrentHashMap, Runnable> getRefMap() { 27 | return refMap; 28 | } 29 | 30 | public static void register(Object resource, Runnable cleaner) { 31 | if (resource != null && cleaner != null) { 32 | PhantomReference ref = new PhantomReference<>(resource, REF_QUEUE); 33 | refMap.put(ref, cleaner); 34 | } 35 | } 36 | 37 | public static void doClean() { 38 | Reference ref = REF_QUEUE.poll(); 39 | while (ref != null) { 40 | Runnable cleaner = refMap.remove(ref); 41 | try { 42 | cleaner.run(); 43 | } catch (Throwable t) { 44 | logger.warn("Failover GC doClean failed", t); 45 | } 46 | ref = REF_QUEUE.poll(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/WeightFailoverJmhTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.annotations.BenchmarkMode; 5 | import org.openjdk.jmh.annotations.Fork; 6 | import org.openjdk.jmh.annotations.Measurement; 7 | import org.openjdk.jmh.annotations.Mode; 8 | import org.openjdk.jmh.annotations.Param; 9 | import org.openjdk.jmh.annotations.Scope; 10 | import org.openjdk.jmh.annotations.Setup; 11 | import org.openjdk.jmh.annotations.State; 12 | import org.openjdk.jmh.annotations.Warmup; 13 | 14 | import com.google.common.collect.ImmutableMap; 15 | import com.google.common.collect.ImmutableMap.Builder; 16 | 17 | /** 18 | * @author lijie 19 | * Created on 2019-02-17 20 | */ 21 | @BenchmarkMode(Mode.Throughput) 22 | @Fork(1) 23 | @Warmup(iterations = 2, time = 2) 24 | @Measurement(iterations = 2, time = 2) 25 | @State(Scope.Benchmark) 26 | public class WeightFailoverJmhTest { 27 | 28 | @Param({"5", "20", "100", "200", "1000"}) 29 | public int size; 30 | 31 | private WeightFailover failover; 32 | 33 | @Setup 34 | public void init() { 35 | Builder builder = ImmutableMap.builder(); 36 | for (int i = 0; i < size; ++i) { 37 | builder.put("key" + i, i); 38 | } 39 | failover = WeightFailover. newGenericBuilder() 40 | .checker(it -> true, 1) 41 | .build(builder.build()); 42 | } 43 | 44 | @Benchmark 45 | public void getAvailableOpitmized() { 46 | failover.getAvailable(); 47 | } 48 | 49 | @Benchmark 50 | public void getAvailable() { 51 | failover.getAvailable(size); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/SimpleFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover; 2 | 3 | import java.util.Collection; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | 8 | /** 9 | * 简化的Failover接口,提供的方法更少更简单,以达到更好的性能,简单的接口也有利于实现类做组合。 10 | * 11 | *

12 | * 请先阅读README.md,可以到这里在线阅读。 13 | *

14 | * 15 | * @author huangli 16 | * Created on 2020-01-15 17 | */ 18 | public interface SimpleFailover extends AutoCloseable { 19 | 20 | /** 21 | * 资源调用成功后,由使用方调用本方法,通知failover组件资源调用成功,随后failover内部通常会增加这个资源的权重(不超过最大值)。 22 | * 23 | * 这个方法以前居然是个默认方法,为了保持兼容还是别改了。 24 | * 25 | * @param object 被调用的资源 26 | */ 27 | default void success(@Nonnull T object) { 28 | // default behavior: do nothing 29 | } 30 | 31 | /** 32 | * 资源调用失败后,由使用方调用本方法,通知failover组件资源调用失败,随后failover内部通常会扣减这个资源的权重(不低于最小值)。 33 | * 34 | * @param object 被调用的资源 35 | */ 36 | void fail(@Nonnull T object); 37 | 38 | /** 39 | * 有时候使用方可能想把资源直接的权重直接扣减到最小值,可调用本方法。 40 | * 41 | * @param object 被调用的资源 42 | */ 43 | void down(@Nonnull T object); 44 | 45 | /** 46 | * 获取一个可使用的资源,如果所有的资源都down了,可能会返回null,注意判空。 47 | * 48 | * @return 一个用于执行调用的资源 49 | */ 50 | @Nullable 51 | T getOneAvailable(); 52 | 53 | /** 54 | * 当调用失败后,使用方想要换一个资源重试,可使用本方法重新获取另一个资源,注意判空。 55 | * 56 | * @param exclusions 需要排除的资源列表,不会出现在返回的结果中,通常是之前调用失败的资源 57 | * @return 一个用于执行调用的资源 58 | */ 59 | @Nullable 60 | T getOneAvailableExclude(Collection exclusions); 61 | 62 | /** 63 | * 为了编译通过,提供默认实现 64 | * 实现内部应关闭健康检查任务。 65 | */ 66 | @Override 67 | default void close() { 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/LcgRandomIterator.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.NoSuchElementException; 8 | import java.util.RandomAccess; 9 | import java.util.concurrent.ThreadLocalRandom; 10 | 11 | import javax.annotation.Nonnull; 12 | 13 | /** 14 | * base on https://en.wikipedia.org/wiki/Linear_congruential_generator 15 | * 16 | * @author w.vela 17 | * Created on 2018-02-26. 18 | */ 19 | class LcgRandomIterator implements Iterator { 20 | 21 | private static final int c = 11; 22 | private static final long a = 25214903917L; 23 | 24 | private final List original; 25 | private final long seed; 26 | private final long m; 27 | private final long n; 28 | 29 | private long next; 30 | private boolean hasNext = true; 31 | 32 | LcgRandomIterator(@Nonnull List original) { 33 | if (!(original instanceof RandomAccess)) { 34 | throw new IllegalArgumentException(); 35 | } 36 | this.original = checkNotNull(original); 37 | this.n = original.size(); 38 | m = (long) Math.pow(2, Math.ceil(Math.log(n) / Math.log(2))); 39 | next = seed = ThreadLocalRandom.current().nextLong(Math.min(n, Integer.MAX_VALUE)); 40 | } 41 | 42 | @Override 43 | public boolean hasNext() { 44 | return hasNext; 45 | } 46 | 47 | @Override 48 | public T next() { 49 | if (!hasNext) { 50 | throw new NoSuchElementException(); 51 | } 52 | next = (a * next + c) % m; 53 | while (next >= n) { 54 | next = (a * next + c) % m; 55 | } 56 | if (next == seed) { 57 | hasNext = false; 58 | } 59 | return original.get((int) next); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/SimpleWeightFunction.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | /** 4 | * @author huangli 5 | * Created on 2020-01-20 6 | */ 7 | public class SimpleWeightFunction extends AbstractWeightFunction { 8 | 9 | private static final double DEFAULT_FAIL_DECREASE_RATE = 0.05; 10 | private static final double DEFAULT_SUCCESS_INCREASE_RATE = 0.01; 11 | 12 | private final double failDecreaseRate; 13 | private final double successIncreaseRate; 14 | 15 | public SimpleWeightFunction() { 16 | this(DEFAULT_FAIL_DECREASE_RATE, DEFAULT_SUCCESS_INCREASE_RATE); 17 | } 18 | 19 | public SimpleWeightFunction(double failDecreaseRate, double successIncreaseRate) { 20 | this(failDecreaseRate, successIncreaseRate, DEFAULT_RECOVER_THRESHOLD); 21 | } 22 | 23 | public SimpleWeightFunction(double failDecreaseRate, double successIncreaseRate, int recoverThreshold) { 24 | super(recoverThreshold); 25 | if (failDecreaseRate < 0 || failDecreaseRate > 1) { 26 | throw new IllegalArgumentException("bad failDecreaseRate:" + failDecreaseRate); 27 | } 28 | if (successIncreaseRate < 0 || successIncreaseRate > 1) { 29 | throw new IllegalArgumentException("bad successIncreaseRate:" + successIncreaseRate); 30 | } 31 | this.failDecreaseRate = failDecreaseRate; 32 | this.successIncreaseRate = successIncreaseRate; 33 | } 34 | 35 | @Override 36 | public double computeSuccess(double maxWeight, double minWeight, int priority, double currentOldWeight, T resource) { 37 | return currentOldWeight + maxWeight * successIncreaseRate; 38 | } 39 | 40 | @Override 41 | public double computeFail(double maxWeight, double minWeight, int priority, double currentOldWeight, T resource) { 42 | return currentOldWeight - maxWeight * failDecreaseRate; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/WeightFunction.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | /** 4 | * 权重控制的回调函数,如果你需要定制权重增减算法,或者定制健康检查的时机,就需要用到这个接口。 5 | * 如果没有太多要求用默认的就可以,{@link PriorityFailover}默认内置一个{@link RatioWeightFunction}。 6 | * 7 | * @author huangli 8 | * Created on 2020-01-16 9 | */ 10 | public interface WeightFunction { 11 | 12 | /** 13 | * 一个资源访问成功后,会调用本方法,计算新的权重。 14 | * Failover内部可能会优化,在当前权重已经等于最大权重的情况下, 15 | * 继续调用{@link com.github.phantomthief.failover.Failover#success(Object)}方法, 16 | * 不会导致本接口的本方法被调用。 17 | * 18 | * @param maxWeight 这个资源的最大权重 19 | * @param minWeight 这个资源的最小权重 20 | * @param priority 这个资源优先级 21 | * @param currentOldWeight 这个资源的当前权重(计算前) 22 | * @param resource 这个资源 23 | * @return 新的权重 24 | */ 25 | double success(double maxWeight, double minWeight, int priority, double currentOldWeight, T resource); 26 | 27 | /** 28 | * 一个资源访问失败后,会调用本方法,计算新的权重。 29 | * Failover内部可能会优化,在当前权重已经等于最小权重的情况下, 30 | * 继续调用{@link com.github.phantomthief.failover.Failover#fail(Object)}方法, 31 | * 不会导致本接口的本方法被调用。 32 | * 33 | * @param maxWeight 这个资源的最大权重 34 | * @param minWeight 这个资源的最小权重 35 | * @param priority 这个资源优先级 36 | * @param currentOldWeight 这个资源的当前权重(计算前) 37 | * @param resource 这个资源 38 | * @return 新的权重 39 | */ 40 | double fail(double maxWeight, double minWeight, int priority, double currentOldWeight, T resource); 41 | 42 | /** 43 | * 用来控制一个资源是否需要健康检查,健康检查程序运行时,针对每个资源,逐个调用本方法,如果本方法返回true那么就对相关资源进行 44 | * 健康检查。 45 | * 46 | * @param maxWeight 这个资源的最大权重 47 | * @param minWeight 这个资源的最小权重 48 | * @param priority 这个资源优先级 49 | * @param currentWeight 当前权重 50 | * @param resource 这个资源 51 | * @return 是否需要健康检查 52 | */ 53 | boolean needCheck(double maxWeight, double minWeight, int priority, double currentWeight, T resource); 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/RecoverableCheckFailoverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.github.phantomthief.failover.WeighTestUtils.checkRatio; 4 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 5 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import java.util.function.Predicate; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | import com.google.common.collect.HashMultiset; 14 | import com.google.common.collect.ImmutableList; 15 | import com.google.common.collect.Multiset; 16 | 17 | /** 18 | * @author w.vela 19 | * Created on 2019-01-04. 20 | */ 21 | class RecoverableCheckFailoverTest { 22 | 23 | @Test 24 | void test() { 25 | boolean[] checkerSwitcher = { false }; 26 | Predicate checker = it -> checkerSwitcher[0]; 27 | RecoverableCheckFailover failover = RecoverableCheckFailover 28 | . newGenericBuilder().setChecker(checker) // 29 | .setFailCount(10) 30 | .setFailDuration(100, MILLISECONDS) 31 | .setRecoveryCheckDuration(100, MILLISECONDS) 32 | .setReturnOriginalWhileAllFailed(false) 33 | .build(ImmutableList.of("s1", "s2")); 34 | for (int i = 0; i < 10; i++) { 35 | failover.fail("s2"); 36 | } 37 | for (int i = 0; i < 10; i++) { 38 | assertEquals("s1", failover.getOneAvailable()); 39 | } 40 | checkerSwitcher[0] = true; 41 | sleepUninterruptibly(100, MILLISECONDS); 42 | Multiset result = HashMultiset.create(); 43 | for (int i = 0; i < 10000; i++) { 44 | result.add(failover.getOneAvailable()); 45 | } 46 | assertTrue(checkRatio(result.count("s2"), result.count("s1"), 1)); 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/checker/SimplePortCheckerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.checker; 2 | 3 | import static com.github.phantomthief.failover.impl.checker.SimplePortChecker.asyncCheck; 4 | import static com.github.phantomthief.failover.impl.checker.SimplePortChecker.check; 5 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 6 | import static java.util.concurrent.TimeUnit.SECONDS; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | import java.io.IOException; 11 | import java.net.ServerSocket; 12 | import java.util.concurrent.CancellationException; 13 | import java.util.concurrent.ExecutionException; 14 | import java.util.concurrent.ThreadLocalRandom; 15 | import java.util.concurrent.TimeoutException; 16 | 17 | import org.junit.jupiter.api.AfterEach; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import com.google.common.util.concurrent.ListenableFuture; 22 | 23 | /** 24 | * @author w.vela 25 | * Created on 2018-06-25. 26 | */ 27 | class SimplePortCheckerTest { 28 | 29 | private ServerSocket serverSocket; 30 | private int port; 31 | 32 | @BeforeEach 33 | void setUp() throws IOException { 34 | port = ThreadLocalRandom.current().nextInt(1025, 10240); 35 | serverSocket = new ServerSocket(port); 36 | } 37 | 38 | @Test 39 | void testSync() { 40 | assertTrue(check("localhost", port, 100)); 41 | } 42 | 43 | @Test 44 | void testAsync() throws InterruptedException, ExecutionException, TimeoutException { 45 | ListenableFuture future = asyncCheck("localhost", port); 46 | assertThrows(TimeoutException.class, () -> future.get(1, NANOSECONDS)); 47 | assertThrows(CancellationException.class, future::get); 48 | 49 | ListenableFuture future2 = asyncCheck("localhost", port); 50 | future2.get(1, SECONDS); 51 | } 52 | 53 | @AfterEach 54 | void tearDown() throws IOException { 55 | serverSocket.close(); 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/GenericRecoverableCheckFailoverBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.TimeUnit; 5 | import java.util.function.Predicate; 6 | 7 | import javax.annotation.CheckReturnValue; 8 | import javax.annotation.Nonnull; 9 | 10 | /** 11 | * @author w.vela 12 | */ 13 | @Deprecated 14 | public class GenericRecoverableCheckFailoverBuilder { 15 | 16 | private final RecoverableCheckFailoverBuilder builder; 17 | 18 | GenericRecoverableCheckFailoverBuilder(RecoverableCheckFailoverBuilder builder) { 19 | this.builder = builder; 20 | } 21 | 22 | @CheckReturnValue 23 | @Nonnull 24 | public RecoverableCheckFailoverBuilder setFailCount(int failCount) { 25 | return builder.setFailCount(failCount); 26 | } 27 | 28 | @CheckReturnValue 29 | @Nonnull 30 | public GenericRecoverableCheckFailoverBuilder setChecker(Predicate checker) { 31 | builder.setChecker(checker); 32 | return this; 33 | } 34 | 35 | @CheckReturnValue 36 | @Nonnull 37 | public GenericRecoverableCheckFailoverBuilder 38 | setRecoveryCheckDuration(long recoveryCheckDuration, TimeUnit unit) { 39 | builder.setRecoveryCheckDuration(recoveryCheckDuration, unit); 40 | return this; 41 | } 42 | 43 | @CheckReturnValue 44 | @Nonnull 45 | public GenericRecoverableCheckFailoverBuilder setFailDuration(long failDuration, 46 | TimeUnit unit) { 47 | builder.setFailDuration(failDuration, unit); 48 | return this; 49 | } 50 | 51 | @CheckReturnValue 52 | @Nonnull 53 | public GenericRecoverableCheckFailoverBuilder 54 | setReturnOriginalWhileAllFailed(boolean returnOriginalWhileAllFailed) { 55 | builder.setReturnOriginalWhileAllFailed(returnOriginalWhileAllFailed); 56 | return this; 57 | } 58 | 59 | @Nonnull 60 | public RecoverableCheckFailover build(List original) { 61 | return builder.build(original); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/SharedCheckExecutorHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static java.lang.String.format; 4 | import static java.lang.Thread.MIN_PRIORITY; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.ScheduledExecutorService; 8 | import java.util.concurrent.ScheduledThreadPoolExecutor; 9 | import java.util.concurrent.ThreadFactory; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | /** 13 | * @author w.vela 14 | */ 15 | public class SharedCheckExecutorHolder { 16 | 17 | private static final int THREAD_COUNT = 10; 18 | 19 | public static ScheduledExecutorService getInstance() { 20 | return LazyHolder.INSTANCE; 21 | } 22 | 23 | private static class LazyHolder { 24 | 25 | private static final ScheduledExecutorService INSTANCE = new ScheduledThreadPoolExecutor( 26 | THREAD_COUNT, 27 | new ThreadFactory() { 28 | private AtomicLong count = new AtomicLong(); 29 | private static final String NAME_PATTERN = "scheduled-failover-recovery-check-%d"; 30 | @Override 31 | public Thread newThread(Runnable r) { 32 | String name = format(NAME_PATTERN, count.getAndIncrement()); 33 | Thread thread = new Thread(r, name); 34 | thread.setDaemon(true); 35 | thread.setPriority(MIN_PRIORITY); 36 | if (Thread.getDefaultUncaughtExceptionHandler() == null) { 37 | thread.setUncaughtExceptionHandler((t, e) -> { 38 | e.printStackTrace(); 39 | }); 40 | } 41 | return thread; 42 | } 43 | }) { 44 | 45 | public void shutdown() { 46 | throw new UnsupportedOperationException(); 47 | } 48 | 49 | public List shutdownNow() { 50 | throw new UnsupportedOperationException(); 51 | } 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/RandomListUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static java.lang.Math.min; 4 | import static java.util.Collections.emptyList; 5 | import static java.util.Collections.shuffle; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.RandomAccess; 11 | import java.util.concurrent.ThreadLocalRandom; 12 | 13 | import javax.annotation.Nullable; 14 | 15 | /** 16 | * @author w.vela 17 | */ 18 | public class RandomListUtils { 19 | 20 | private static final int LCG_THRESHOLD = 3; 21 | 22 | private RandomListUtils() { 23 | throw new UnsupportedOperationException(); 24 | } 25 | 26 | @Nullable 27 | public static T getRandom(List source) { 28 | if (source == null || source.isEmpty()) { 29 | return null; 30 | } 31 | return source.get(ThreadLocalRandom.current().nextInt(source.size())); 32 | } 33 | 34 | public static List getRandom(Collection source, int size) { 35 | if (source == null || source.isEmpty()) { 36 | return emptyList(); 37 | } 38 | if (source instanceof List && source instanceof RandomAccess 39 | && size < source.size() / LCG_THRESHOLD) { 40 | return getRandomUsingLcg((List) source, size); 41 | } else { 42 | return getRandomUsingShuffle(source, size); 43 | } 44 | } 45 | 46 | static List getRandomUsingShuffle(Collection source, int size) { 47 | List newList = new ArrayList<>(source); 48 | shuffle(newList, ThreadLocalRandom.current()); 49 | return newList.subList(0, min(newList.size(), size)); 50 | } 51 | 52 | static List getRandomUsingLcg(List source, int size) { 53 | int targetSize = min(source.size(), size); 54 | List newList = new ArrayList<>(targetSize); 55 | LcgRandomIterator iterator = new LcgRandomIterator<>(source); 56 | for (int i = 0; i < targetSize; i++) { 57 | newList.add(iterator.next()); 58 | } 59 | return newList; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/Weight.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.google.common.collect.Range.closedOpen; 4 | import static com.google.common.collect.TreeRangeMap.create; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.concurrent.ThreadLocalRandom; 10 | 11 | import javax.annotation.Nonnull; 12 | import javax.annotation.Nullable; 13 | 14 | import com.google.common.collect.RangeMap; 15 | 16 | /** 17 | * 带权重的树 18 | * 如果只使用 {@link #get()},可以考虑使用 {@link AliasMethod},性能更好 19 | * 20 | * @author w.vela 21 | * @param 22 | */ 23 | public class Weight { 24 | 25 | private final Map weightMap = new HashMap<>(); 26 | private final RangeMap nodes = create(); 27 | private long maxWeight = 0; 28 | 29 | public Weight add(@Nonnull T node, long weight) { 30 | if (weight > 0) { 31 | weightMap.put(node, weight); 32 | nodes.put(closedOpen(maxWeight, maxWeight + weight), node); 33 | maxWeight += weight; 34 | } 35 | return this; 36 | } 37 | 38 | @Nullable 39 | public T get() { 40 | if (isEmpty()) { 41 | return null; 42 | } 43 | long resultIndex = nextLong(0, maxWeight); 44 | return nodes.get(resultIndex); 45 | } 46 | 47 | @Nullable 48 | public T getWithout(Set exclusions) { 49 | if (weightMap.size() == exclusions.size()) { 50 | return null; 51 | } 52 | while (true) { 53 | T t = get(); 54 | if (!exclusions.contains(t)) { 55 | return t; 56 | } 57 | } 58 | } 59 | 60 | public boolean isEmpty() { 61 | return maxWeight == 0; 62 | } 63 | 64 | public Set allNodes() { 65 | return weightMap.keySet(); 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return nodes.toString(); 71 | } 72 | 73 | private static long nextLong(long startInclusive, long endExclusive) { 74 | if (startInclusive == endExclusive) { 75 | return startInclusive; 76 | } 77 | 78 | return (long) ThreadLocalRandom.current().nextDouble(startInclusive, endExclusive); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/RatioWeightFunctionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * @author huangli 9 | * Created on 2020-05-06 10 | */ 11 | public class RatioWeightFunctionTest { 12 | 13 | @Test 14 | public void testFail1() { 15 | RatioWeightFunction f = new RatioWeightFunction<>(0.5, 0.01); 16 | assertEquals(0.5, f.fail(1, 0, 0, 1, "R1")); 17 | assertEquals(0.25, f.fail(1, 0, 0, 0.5, "R1")); 18 | assertEquals(0.125, f.fail(1, 0, 0, 0.25, "R1")); 19 | assertEquals(0, f.fail(1, 0, 0, 0, "R1")); 20 | } 21 | 22 | 23 | @Test 24 | public void testFail2() { 25 | RatioWeightFunction f = new RatioWeightFunction<>(0.7, 0.01, 1, 0.4); 26 | assertEquals(0.7, f.fail(1, 0, 0, 1, "R1"), 0.001); 27 | assertEquals(0.49, f.fail(1, 0, 0, 0.7, "R1"), 0.001); 28 | assertEquals(0, f.fail(1, 0, 0, 0.49, "R1")); 29 | } 30 | 31 | @Test 32 | public void testFail3() { 33 | RatioWeightFunction f = new RatioWeightFunction<>(); 34 | f.setDownThresholdRateOfMaxWeight(0.4); 35 | assertEquals(0.5, f.fail(1, 0, 0, 1, "R1")); 36 | assertEquals(0, f.fail(1, 0, 0, 0.5, "R1")); 37 | } 38 | 39 | @Test 40 | public void testSuccess1() { 41 | RatioWeightFunction f = new RatioWeightFunction<>(0.5, 0.5); 42 | assertEquals(0.1, f.success(1, 0, 0, 0, "R1")); 43 | assertEquals(1, f.success(1, 0, 0, 0.5, "R1")); 44 | assertEquals(1, f.success(1, 0, 0, 1, "R1")); 45 | } 46 | 47 | @Test 48 | public void testSuccess2() { 49 | RatioWeightFunction f = new RatioWeightFunction<>(0.5, 0.5, 2); 50 | 51 | assertEquals(0, f.success(1, 0, 0, 0, "R1")); 52 | 53 | assertEquals(0, f.success(1, 0, 0, 0, "R2")); 54 | assertEquals(0.1, f.success(1, 0, 0, 0, "R2")); 55 | 56 | f.setRecoverRateOfMaxWeight(0.2); 57 | assertEquals(0, f.fail(1, 0, 0, 0, "R1")); 58 | assertEquals(0, f.success(1, 0, 0, 0, "R1")); 59 | assertEquals(0.2, f.success(1, 0, 0, 0, "R1")); 60 | assertEquals(0.7, f.success(1, 0, 0, 0.2, "R1")); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/DummyFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static java.util.Collections.emptySet; 5 | import static java.util.Collections.singletonList; 6 | import static java.util.Collections.unmodifiableList; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.List; 11 | import java.util.Set; 12 | import java.util.function.Supplier; 13 | 14 | import javax.annotation.Nonnull; 15 | 16 | import com.github.phantomthief.failover.Failover; 17 | 18 | /** 19 | * @author w.vela 20 | * Created on 16/6/2. 21 | */ 22 | public class DummyFailover implements Failover { 23 | 24 | private final Supplier> supplier; 25 | 26 | private DummyFailover(@Nonnull Supplier> all) { 27 | this.supplier = checkNotNull(all); 28 | } 29 | 30 | public static DummyFailover ofCollection(@Nonnull Collection all) { 31 | checkNotNull(all); 32 | return new DummyFailover<>(() -> all); 33 | } 34 | 35 | public static DummyFailover ofSingle(T single) { 36 | return new DummyFailover<>(() -> singletonList(single)); 37 | } 38 | 39 | public static DummyFailover ofCollectionSupplier(@Nonnull Supplier> supplier) { 40 | return new DummyFailover<>(supplier); 41 | } 42 | 43 | public static DummyFailover ofSingleSupplier(@Nonnull Supplier single) { 44 | checkNotNull(single); 45 | return new DummyFailover<>(() -> singletonList(single.get())); 46 | } 47 | 48 | @Override 49 | public List getAll() { 50 | Collection all = supplier.get(); 51 | if (all instanceof List) { 52 | return unmodifiableList((List) all); 53 | } else { 54 | return new ArrayList<>(all); 55 | } 56 | } 57 | 58 | @Override 59 | public void fail(T object) { 60 | // do nothing 61 | } 62 | 63 | @Override 64 | public void down(T object) { 65 | // do nothing 66 | } 67 | 68 | @Override 69 | public List getAvailable() { 70 | return getAll(); 71 | } 72 | 73 | @Override 74 | public Set getFailed() { 75 | return emptySet(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/FilteredFailoverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static java.lang.ThreadLocal.withInitial; 4 | import static java.util.stream.Collectors.toList; 5 | import static java.util.stream.IntStream.rangeClosed; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | import java.util.function.Predicate; 13 | 14 | import org.junit.jupiter.api.Test; 15 | 16 | import com.github.phantomthief.failover.impl.WeightFailover; 17 | 18 | /** 19 | * @author w.vela 20 | * Created on 2018-12-30. 21 | */ 22 | class FilteredFailoverTest { 23 | 24 | @Test 25 | void test() { 26 | Set blocked = new HashSet<>(); 27 | Predicate filter = it -> !blocked.contains(it); 28 | WeightFailover failover = WeightFailover. newGenericBuilder() 29 | .checker(it -> 1.0D) 30 | .filter(filter) 31 | .build(rangeClosed(1, 10) 32 | .mapToObj(it -> "s" + it) 33 | .collect(toList()), 10); 34 | blocked.add("s1"); 35 | for (int i = 0; i < 100; i++) { 36 | assertFalse(failover.getAvailable().contains("s1")); 37 | assertNotEquals("s1", failover.getOneAvailable()); 38 | } 39 | } 40 | 41 | @Test 42 | void testThreadRetry() { 43 | ThreadLocal> tried = withInitial(HashSet::new); 44 | WeightFailover failover = WeightFailover. newGenericBuilder() 45 | .checker(it -> 1.0D) 46 | .filter(it -> !tried.get().contains(it)) 47 | .build(rangeClosed(1, 10) 48 | .mapToObj(it -> "s" + it) 49 | .collect(toList()), 10); 50 | 51 | for (int i = 0; i < 100; i++) { 52 | try { 53 | String one = failover.getOneAvailable(); 54 | if ("s1".equals(one)) { 55 | tried.get().add(one); 56 | one = failover.getOneAvailable(); 57 | assertNotEquals("s1", one); 58 | } 59 | } finally { 60 | tried.remove(); 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/benchmark/WeightBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.benchmark; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.concurrent.ThreadLocalRandom; 6 | 7 | import org.openjdk.jmh.annotations.Benchmark; 8 | import org.openjdk.jmh.annotations.BenchmarkMode; 9 | import org.openjdk.jmh.annotations.Fork; 10 | import org.openjdk.jmh.annotations.Measurement; 11 | import org.openjdk.jmh.annotations.Mode; 12 | import org.openjdk.jmh.annotations.Param; 13 | import org.openjdk.jmh.annotations.Scope; 14 | import org.openjdk.jmh.annotations.Setup; 15 | import org.openjdk.jmh.annotations.State; 16 | import org.openjdk.jmh.annotations.Threads; 17 | import org.openjdk.jmh.annotations.Warmup; 18 | 19 | import com.github.phantomthief.failover.util.AliasMethod; 20 | import com.github.phantomthief.failover.util.Weight; 21 | 22 | /** 23 | * Benchmark (totalSize) Mode Cnt Score Error Units 24 | * WeightBenchmark.testAliasMethod 10 thrpt 3 531373408.041 ± 1310821420.530 ops/s 25 | * WeightBenchmark.testAliasMethod 100 thrpt 3 524611078.970 ± 567444175.501 ops/s 26 | * WeightBenchmark.testAliasMethod 1000 thrpt 3 442690581.938 ± 705466764.645 ops/s 27 | * WeightBenchmark.testWeight 10 thrpt 3 101635713.806 ± 28607384.102 ops/s 28 | * WeightBenchmark.testWeight 100 thrpt 3 59522839.677 ± 33694178.059 ops/s 29 | * WeightBenchmark.testWeight 1000 thrpt 3 36993978.805 ± 4766898.860 ops/s 30 | * 31 | * @author w.vela 32 | * Created on 2020-04-07. 33 | */ 34 | @BenchmarkMode(Mode.Throughput) 35 | @Fork(1) 36 | @Threads(10) 37 | @Warmup(iterations = 1, time = 1) 38 | @Measurement(iterations = 3, time = 1) 39 | @State(Scope.Benchmark) 40 | public class WeightBenchmark { 41 | 42 | @Param({"10", "100", "1000"}) 43 | private int totalSize; 44 | 45 | private Weight weight; 46 | private AliasMethod aliasMethod; 47 | 48 | @Setup 49 | public void init() { 50 | weight = new Weight<>(); 51 | Map weightMap = new HashMap<>(); 52 | for (int i = 0; i < totalSize; i++) { 53 | int weightValue = ThreadLocalRandom.current().nextInt(1, 100); 54 | String node = "key" + i; 55 | weight.add(node, weightValue); 56 | weightMap.put(node, weightValue); 57 | } 58 | aliasMethod = new AliasMethod<>(weightMap); 59 | } 60 | 61 | 62 | @Benchmark 63 | public void testWeight() { 64 | weight.get(); 65 | } 66 | 67 | @Benchmark 68 | public void testAliasMethod() { 69 | aliasMethod.get(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/FailoverUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.github.phantomthief.failover.WeighTestUtils.checkRatio; 4 | import static com.github.phantomthief.failover.util.FailoverUtils.isHostUnavailable; 5 | import static com.github.phantomthief.failover.util.FailoverUtils.runWithRetry; 6 | import static com.github.phantomthief.failover.util.FailoverUtils.supplyWithRetry; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | import java.net.InetSocketAddress; 11 | import java.net.Socket; 12 | 13 | import org.junit.jupiter.api.Test; 14 | 15 | import com.github.phantomthief.failover.impl.WeightFailover; 16 | import com.google.common.collect.HashMultiset; 17 | import com.google.common.collect.ImmutableMap; 18 | import com.google.common.collect.Multiset; 19 | 20 | /** 21 | * @author w.vela 22 | * Created on 2019-01-04. 23 | */ 24 | class FailoverUtilsTest { 25 | 26 | @Test 27 | void test() { 28 | test("localhost", 1, 100); 29 | test("111.222.222.112", 2, 100); 30 | } 31 | 32 | @Test 33 | void testRetry() { 34 | WeightFailover failover = WeightFailover. newGenericBuilder() 35 | .checker(it -> 1.0D) 36 | .failReduceRate(0.00001D) 37 | .build(ImmutableMap.of("s1", 1000, "s2", 2000)); 38 | for (int i = 0; i < 100; i++) { 39 | runWithRetry(2, 10, failover, this::run); 40 | } 41 | for (int i = 0; i < 100; i++) { 42 | assertEquals("s2", supplyWithRetry(2, 10, failover, this::supply)); 43 | } 44 | Multiset result = HashMultiset.create(); 45 | for (int i = 0; i < 10000; i++) { 46 | try { 47 | FailoverUtils.run(failover, this::run, t -> false); 48 | result.add("s2"); 49 | } catch (IllegalStateException e) { 50 | result.add("s1"); 51 | } 52 | } 53 | assertTrue(checkRatio(result.count("s2"), result.count("s1"), 2)); 54 | } 55 | 56 | private void run(String client) { 57 | if (client.equals("s1")) { 58 | throw new IllegalStateException(); 59 | } 60 | } 61 | 62 | private String supply(String client) { 63 | if (client.equals("s1")) { 64 | throw new IllegalStateException(); 65 | } else { 66 | return client; 67 | } 68 | } 69 | 70 | private void test(String host, int port, int i) { 71 | try (Socket socket = new Socket()) { 72 | socket.connect(new InetSocketAddress(host, port), i); 73 | } catch (Throwable e) { 74 | assertTrue(isHostUnavailable(e)); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/RandomListUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.github.phantomthief.failover.util.RandomListUtils.getRandomUsingLcg; 4 | import static com.github.phantomthief.failover.util.RandomListUtils.getRandomUsingShuffle; 5 | import static java.lang.System.nanoTime; 6 | import static java.util.stream.Collectors.toList; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | import java.util.List; 10 | import java.util.concurrent.ThreadLocalRandom; 11 | import java.util.stream.IntStream; 12 | 13 | import org.junit.jupiter.api.Test; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | /** 18 | * @author w.vela 19 | * Created on 2018-02-26. 20 | */ 21 | class RandomListUtilsTest { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(RandomListUtilsTest.class); 24 | 25 | @Test 26 | void getRandom() { 27 | long lcgBetter = 0, shuffleBetter = 0; 28 | for (int i = 0; i < 1000; i++) { 29 | int size = ThreadLocalRandom.current().nextInt(1, 10000); 30 | int retrieved = ThreadLocalRandom.current().nextInt(1, Math.max(2, size / 3)); 31 | List list = IntStream.range(0, size).boxed().collect(toList()); 32 | 33 | long s = nanoTime(); 34 | List result = getRandomUsingLcg(list, retrieved); 35 | long costLcg = nanoTime() - s; 36 | 37 | long s2 = nanoTime(); 38 | getRandomUsingShuffle(list, retrieved); 39 | long costShuffle = nanoTime() - s2; 40 | 41 | assertEquals(retrieved, result.stream().distinct().count()); 42 | if (costLcg > costShuffle) { 43 | shuffleBetter += costLcg - costShuffle; 44 | } else { 45 | lcgBetter += costShuffle - costLcg; 46 | } 47 | } 48 | logger.info("lcg better:{}, shuffle better:{}", lcgBetter, shuffleBetter); 49 | lcgBetter = 0; 50 | shuffleBetter = 0; 51 | for (int i = 0; i < 1000; i++) { 52 | int size = ThreadLocalRandom.current().nextInt(1, 10000); 53 | List list = IntStream.range(0, size).boxed().collect(toList()); 54 | 55 | long s = nanoTime(); 56 | List result = getRandomUsingLcg(list, size); 57 | long costLcg = nanoTime() - s; 58 | 59 | long s2 = nanoTime(); 60 | getRandomUsingShuffle(list, size); 61 | long costShuffle = nanoTime() - s2; 62 | 63 | assertEquals(size, result.stream().distinct().count()); 64 | if (costLcg > costShuffle) { 65 | shuffleBetter += costLcg - costShuffle; 66 | } else { 67 | lcgBetter += costShuffle - costLcg; 68 | } 69 | } 70 | logger.info("lcg better:{}, shuffle better:{}", lcgBetter, shuffleBetter); 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/AbstractWeightFunction.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | 5 | /** 6 | * @author huangli 7 | * Created on 2020-05-06 8 | */ 9 | public abstract class AbstractWeightFunction implements WeightFunction { 10 | 11 | public static final int DEFAULT_RECOVER_THRESHOLD = 1; 12 | 13 | private final int recoverThreshold; 14 | 15 | private final ConcurrentHashMap recoverCountMap; 16 | 17 | public AbstractWeightFunction() { 18 | this(DEFAULT_RECOVER_THRESHOLD); 19 | } 20 | 21 | public AbstractWeightFunction(int recoverThreshold) { 22 | if (recoverThreshold < 1) { 23 | throw new IllegalArgumentException("bad recoverThreshold:" + recoverThreshold); 24 | } 25 | this.recoverThreshold = recoverThreshold; 26 | if (recoverThreshold > 1) { 27 | recoverCountMap = new ConcurrentHashMap<>(); 28 | } else { 29 | recoverCountMap = null; 30 | } 31 | } 32 | 33 | @Override 34 | public double success(double maxWeight, double minWeight, int priority, double currentOldWeight, T resource) { 35 | if (recoverCountMap != null && currentOldWeight <= minWeight) { 36 | int count = recoverCountMap.compute(resource, (k, v) -> { 37 | if (v == null) { 38 | return 1; 39 | } else { 40 | return v + 1; 41 | } 42 | }); 43 | if (count < recoverThreshold) { 44 | return currentOldWeight; 45 | } 46 | } 47 | 48 | double newWeight = computeSuccess(maxWeight, minWeight, priority, currentOldWeight, resource); 49 | 50 | if (recoverCountMap != null && currentOldWeight <= minWeight && newWeight > minWeight) { 51 | recoverCountMap.remove(resource); 52 | } 53 | return Math.min(newWeight, maxWeight); 54 | } 55 | 56 | protected abstract double computeSuccess(double maxWeight, double minWeight, 57 | int priority, double currentOldWeight, T resource); 58 | 59 | @Override 60 | public double fail(double maxWeight, double minWeight, int priority, double currentOldWeight, T resource) { 61 | double newWeight = computeFail(maxWeight, minWeight, priority, currentOldWeight, resource); 62 | if (recoverCountMap != null && newWeight <= minWeight) { 63 | recoverCountMap.put(resource, 0); 64 | } 65 | return Math.max(newWeight, minWeight); 66 | } 67 | 68 | protected abstract double computeFail(double maxWeight, double minWeight, 69 | int priority, double currentOldWeight, T resource); 70 | 71 | @Override 72 | public boolean needCheck(double maxWeight, double minWeight, int priority, double currentWeight, T resource) { 73 | return currentWeight <= minWeight && maxWeight > 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/WeightTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.github.phantomthief.failover.WeighTestUtils.checkRatio; 4 | import static java.util.Collections.singleton; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | import com.google.common.collect.HashMultiset; 12 | import com.google.common.collect.ImmutableMap; 13 | import com.google.common.collect.Multiset; 14 | 15 | /** 16 | * @author w.vela 17 | * Created on 2019-01-04. 18 | */ 19 | class WeightTest { 20 | 21 | @Test 22 | void test() { 23 | Weight weight = new Weight() 24 | .add("s1", 1) 25 | .add("s2", 2) 26 | .add("s3", 3); 27 | Multiset result = HashMultiset.create(); 28 | for (int i = 0; i < 10000; i++) { 29 | result.add(weight.get()); 30 | } 31 | assertTrue(checkRatio(result.count("s2"), result.count("s1"), 2)); 32 | assertTrue(checkRatio(result.count("s3"), result.count("s1"), 3)); 33 | 34 | result.clear(); 35 | 36 | for (int i = 0; i < 10000; i++) { 37 | result.add(weight.getWithout(singleton("s3"))); 38 | } 39 | assertTrue(checkRatio(result.count("s2"), result.count("s1"), 2)); 40 | assertEquals(0, result.count("s3")); 41 | 42 | assertEquals(3, weight.allNodes().size()); 43 | } 44 | 45 | @Test 46 | void testAliasMethod() { 47 | AliasMethod weight = new AliasMethod<>(ImmutableMap. builder() 48 | .put("s1", 1) 49 | .put("s2", 2) 50 | .put("s3", 3) 51 | .build() 52 | ); 53 | Multiset result = HashMultiset.create(); 54 | for (int i = 0; i < 10000; i++) { 55 | result.add(weight.get()); 56 | } 57 | assertTrue(checkRatio(result.count("s2"), result.count("s1"), 2)); 58 | assertTrue(checkRatio(result.count("s3"), result.count("s1"), 3)); 59 | 60 | result.clear(); 61 | 62 | weight = new AliasMethod<>(ImmutableMap. builder() 63 | .put("s1", 2) 64 | .put("s2", 5) 65 | .put("s3", 10) 66 | .put("s4", 20) 67 | .put("s5", 0) 68 | .build() 69 | ); 70 | 71 | for (int i = 0; i < 100000; i++) { 72 | result.add(weight.get()); 73 | } 74 | assertTrue(checkRatio(result.count("s3"), result.count("s1"), 5)); 75 | assertTrue(checkRatio(result.count("s3"), result.count("s2"), 2)); 76 | assertTrue(checkRatio(result.count("s4"), result.count("s2"), 4)); 77 | assertTrue(checkRatio(result.count("s4"), result.count("s1"), 10)); 78 | 79 | assertThrows(IllegalArgumentException.class, () -> { 80 | new AliasMethod<>(ImmutableMap. builder() 81 | .put("s5", 0) 82 | .build() 83 | ); 84 | }); 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/WeightFailoverCheckTaskTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import java.util.Arrays; 4 | import java.util.concurrent.ScheduledFuture; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import com.github.phantomthief.util.MoreSuppliers.CloseableSupplier; 13 | import com.google.common.collect.ImmutableMap; 14 | 15 | /** 16 | * @author huangli 17 | * Created on 2019-12-17 18 | */ 19 | class WeightFailoverCheckTaskTest { 20 | @Test 21 | public void testGc() throws Exception { 22 | WeightFailover failover = WeightFailover 23 | .newGenericBuilder() 24 | .checker(o -> 1.0) 25 | .checkDuration(1, TimeUnit.SECONDS) 26 | .build(ImmutableMap.of("1", 1)); 27 | AtomicBoolean closed = failover.closed; 28 | CloseableSupplier> recoveryFuture = failover.recoveryFuture; 29 | failover.down("1"); 30 | Assertions.assertTrue(recoveryFuture.isInitialized()); 31 | failover = null; 32 | 33 | for (int i = 0; i < 5000; i++) { 34 | byte[] bs = new byte[1 * 1024 * 1024]; 35 | } 36 | System.gc(); 37 | 38 | Thread.sleep((1 + WeightFailoverCheckTask.CLEAN_DELAY_SECONDS) * 1000); 39 | 40 | Assertions.assertTrue(closed.get()); 41 | Assertions.assertTrue(recoveryFuture.get().isCancelled()); 42 | } 43 | 44 | private static class MockResource { 45 | private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0); 46 | private final String resource; 47 | private volatile WeightFailover failover; 48 | 49 | private MockResource(String resource) { 50 | this.resource = resource; 51 | INSTANCE_COUNTER.incrementAndGet(); 52 | GcUtil.register(this, INSTANCE_COUNTER::decrementAndGet); 53 | } 54 | 55 | void setFailover(WeightFailover failover) { 56 | this.failover = failover; 57 | } 58 | } 59 | 60 | @Test 61 | public void testGc2() throws Exception { 62 | MockResource mockResource = new MockResource("str"); 63 | WeightFailover failover = WeightFailover 64 | .newGenericBuilder() 65 | .checker(o -> 1.0) 66 | .checkDuration(1, TimeUnit.SECONDS) 67 | .build(Arrays.asList(mockResource)); 68 | mockResource.setFailover(failover); 69 | CloseableSupplier> recoveryFuture = failover.recoveryFuture; 70 | failover.down(mockResource); 71 | Assertions.assertTrue(recoveryFuture.isInitialized()); 72 | failover.close(); 73 | failover = null; 74 | mockResource = null; 75 | 76 | for (int i = 0; i < 5000; i++) { 77 | byte[] bs = new byte[1 * 1024 * 1024]; 78 | } 79 | System.gc(); 80 | 81 | Thread.sleep((1 + WeightFailoverCheckTask.CLEAN_DELAY_SECONDS) * 1000); 82 | GcUtil.doClean(); 83 | Assertions.assertEquals(0, MockResource.INSTANCE_COUNTER.get()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/SharedResource.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static org.slf4j.LoggerFactory.getLogger; 4 | 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentMap; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | import java.util.function.Function; 9 | 10 | import javax.annotation.Nonnull; 11 | import javax.annotation.Nullable; 12 | import javax.annotation.concurrent.GuardedBy; 13 | 14 | import org.slf4j.Logger; 15 | 16 | import com.github.phantomthief.util.ThrowableConsumer; 17 | 18 | /** 19 | * 多处使用的资源, 需要在所有使用者都注销之后, 再进行清理, 类似于引用计数. 20 | * 21 | * TODO: 其实和netty的io.netty.util.ReferenceCounted很相似 22 | * 回头可以考虑使用和netty一样的方式,使用无锁的方式…… 23 | * 24 | * TODO: 回头挪到common-util包中 25 | * 26 | * @author w.vela 27 | * Created on 16/2/19. 28 | */ 29 | public class SharedResource { 30 | 31 | private static final Logger logger = getLogger(SharedResource.class); 32 | private final ConcurrentMap resources = new ConcurrentHashMap<>(); 33 | 34 | private final Object lock = new Object(); 35 | @GuardedBy("lock") 36 | private final ConcurrentMap counters = new ConcurrentHashMap<>(); 37 | 38 | @Nonnull 39 | public V register(@Nonnull K key, @Nonnull Function factory) { 40 | synchronized (lock) { 41 | V v = resources.computeIfAbsent(key, factory); 42 | counters.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet(); 43 | return v; 44 | } 45 | } 46 | 47 | @Nullable 48 | public V get(@Nonnull K key) { 49 | return resources.get(key); 50 | } 51 | 52 | /** 53 | * @throws UnregisterFailedException if specified cleanup function failed to run. 54 | * @throws IllegalStateException if there is a illegal state found.(e.g. non paired call unregister) 55 | */ 56 | @Nonnull 57 | public V unregister(@Nonnull K key, @Nonnull ThrowableConsumer cleanup) { 58 | synchronized (lock) { 59 | AtomicInteger counter = counters.get(key); 60 | if (counter == null) { 61 | throw new IllegalStateException("non paired unregister call for key:" + key); 62 | } 63 | int count = counter.decrementAndGet(); 64 | 65 | if (count < 0) { // impossible run into here 66 | throw new AssertionError("INVALID INTERNAL STATE:" + key); 67 | } else if (count > 0) { // wait others to unregister 68 | return resources.get(key); 69 | } else { // count == 0 70 | V removed = resources.remove(key); 71 | counters.remove(key); 72 | try { 73 | cleanup.accept(removed); 74 | logger.info("cleanup resource:{}->{}", key, removed); 75 | } catch (Throwable e) { 76 | throw new UnregisterFailedException(e, removed); 77 | } 78 | return removed; 79 | } 80 | } 81 | } 82 | 83 | public static class UnregisterFailedException extends RuntimeException { 84 | 85 | private final Object removed; 86 | 87 | private UnregisterFailedException(Throwable cause, Object removed) { 88 | super(cause); 89 | this.removed = removed; 90 | } 91 | 92 | @SuppressWarnings("unchecked") 93 | public T getRemoved() { 94 | return (T) removed; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/Failover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover; 2 | 3 | import static com.github.phantomthief.failover.util.RandomListUtils.getRandom; 4 | import static java.util.stream.Collectors.toList; 5 | 6 | import java.util.Collection; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | import javax.annotation.Nullable; 11 | 12 | import com.github.phantomthief.failover.util.FailoverUtils; 13 | import com.github.phantomthief.util.ThrowableConsumer; 14 | import com.github.phantomthief.util.ThrowableFunction; 15 | 16 | /** 17 | * Failover接口,有多个实现。 18 | * 这是原来的接口,方法比较多,子类要实现这些方法负担也比较大,重构后抽取了SimpleFailover接口, 19 | * 现在建议使用SimpleFailover,更简单,也能实现绝大部分功能。 20 | * 21 | *

22 | * 请先阅读README.md,可以到这里在线阅读。 23 | *

24 | * 25 | * @author w.vela 26 | */ 27 | public interface Failover extends SimpleFailover { 28 | 29 | /** 30 | * 获取所有资源的列表,包括不可用资源。 31 | * @return 所有资源 32 | */ 33 | List getAll(); 34 | 35 | /** 36 | * 返回可用的资源列表。 37 | * @return 可用资源列表 38 | * @see #getOneAvailable() 39 | * @see #getAvailable(int) 40 | */ 41 | List getAvailable(); 42 | 43 | /** 44 | * 返回可用资源列表,但是排除掉参数指定的资源 45 | * @param exclusions 需要从结果中排除的资源 46 | * @return 满足条件的资源列表 47 | */ 48 | default List getAvailableExclude(Collection exclusions) { 49 | return getAvailable().stream().filter(e -> !exclusions.contains(e)).collect(toList()); 50 | } 51 | 52 | /** 53 | * 获取不健康(当前权重=最小权重)的资源。 54 | * @return 资源列表 55 | */ 56 | Set getFailed(); 57 | 58 | /** 59 | * 获取一个可使用的资源,如果所有的资源都down了,可能会返回null,注意判空。 60 | * 61 | * @return 一个用于执行调用的资源 62 | */ 63 | @Nullable 64 | @Override 65 | default T getOneAvailable() { 66 | return getRandom(getAvailable()); 67 | } 68 | 69 | /** 70 | * 当调用失败后,使用方想要换一个资源重试,可使用本方法重新获取另一个资源,注意判空。 71 | * 72 | * @param exclusions 需要排除的资源列表,不会出现在返回的结果中,通常是之前调用失败的资源 73 | * @return 一个用于执行调用的资源 74 | */ 75 | @Nullable 76 | @Override 77 | default T getOneAvailableExclude(Collection exclusions) { 78 | return getRandom(getAvailableExclude(exclusions)); 79 | } 80 | 81 | /** 82 | * 获取可用的资源列表,但是限制只返回n个。 83 | * @param n 返回值的限制数 84 | * @return 资源列表 85 | */ 86 | default List getAvailable(int n) { 87 | return getRandom(getAvailable(), n); 88 | } 89 | 90 | /** 91 | * 选定一个资源执行业务操作,失败了就再选另一个,直到成功或者所有的资源都试过一遍。 92 | * 通常来说,建议不要用这个方法,因为它的默认实现行为不固定,但是现在也不敢改了,使用者自己来重试更好控制一些。 93 | * @param func 业务操作回调,有返回值 94 | * @return 业务回调的返回值 95 | * @throws X 最后一次失败的业务异常,有时候还会丢出NoAvailableResourceException 96 | * @see FailoverUtils#supplyWithRetry 97 | */ 98 | default E supplyWithRetry(ThrowableFunction func) throws X { 99 | return FailoverUtils.supplyWithRetry(getAll().size(), 0, this, func); 100 | } 101 | 102 | /** 103 | * 选定一个资源执行业务操作,失败了就再选另一个,直到成功或者所有的资源都试过一遍。 104 | * 通常来说,建议不要用这个方法,因为它的默认实现行为不固定,但是现在也不敢改了,使用者自己来重试更好控制一些。 105 | * @param func 业务操作回调,没有返回值 106 | * @throws X 最后一次失败的业务异常,有时候还会丢出NoAvailableResourceException 107 | * @see FailoverUtils#runWithRetry 108 | */ 109 | default void runWithRetry(ThrowableConsumer func) throws X { 110 | FailoverUtils.runWithRetry(getAll().size(), 0, this, func); 111 | } 112 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/AliasMethod.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import java.util.ArrayDeque; 6 | import java.util.ArrayList; 7 | import java.util.Deque; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Map.Entry; 11 | import java.util.concurrent.ThreadLocalRandom; 12 | 13 | import javax.annotation.Nonnull; 14 | 15 | /** 16 | * http://www.keithschwarz.com/darts-dice-coins/ 17 | * 18 | * @author w.vela 19 | * Created on 2020-04-07. 20 | */ 21 | public class AliasMethod { 22 | 23 | private final Object[] values; 24 | private final int[] alias; 25 | private final double[] probability; 26 | 27 | public AliasMethod(@Nonnull Map weightMap) { 28 | requireNonNull(weightMap); 29 | if (weightMap.isEmpty()) { 30 | throw new IllegalArgumentException("weightMap is empty"); 31 | } 32 | List probabilities = new ArrayList<>(weightMap.size()); 33 | List valueList = new ArrayList<>(weightMap.size()); 34 | double sum = 0; 35 | for (Entry entry : weightMap.entrySet()) { 36 | double weight = entry.getValue().doubleValue(); 37 | if (weight > 0) { 38 | sum += weight; 39 | valueList.add(entry.getKey()); 40 | } 41 | } 42 | for (Entry entry : weightMap.entrySet()) { 43 | double weight = entry.getValue().doubleValue(); 44 | if (weight > 0) { 45 | probabilities.add(weight / sum); 46 | } 47 | } 48 | if (sum <= 0) { 49 | throw new IllegalArgumentException("invalid weight map:" + weightMap); 50 | } 51 | values = valueList.toArray(new Object[0]); 52 | 53 | int size = probabilities.size(); 54 | probability = new double[size]; 55 | alias = new int[size]; 56 | 57 | double average = 1.0 / size; 58 | 59 | Deque small = new ArrayDeque<>(); 60 | Deque large = new ArrayDeque<>(); 61 | 62 | for (int i = 0; i < size; ++i) { 63 | if (probabilities.get(i) >= average) { 64 | large.add(i); 65 | } else { 66 | small.add(i); 67 | } 68 | } 69 | 70 | while (!small.isEmpty() && !large.isEmpty()) { 71 | int less = small.removeLast(); 72 | int more = large.removeLast(); 73 | 74 | probability[less] = probabilities.get(less) * size; 75 | alias[less] = more; 76 | 77 | probabilities.set(more, probabilities.get(more) + probabilities.get(less) - average); 78 | 79 | if (probabilities.get(more) >= average) { 80 | large.add(more); 81 | } else { 82 | small.add(more); 83 | } 84 | } 85 | 86 | while (!small.isEmpty()) { 87 | probability[small.removeLast()] = 1.0; 88 | } 89 | while (!large.isEmpty()) { 90 | probability[large.removeLast()] = 1.0; 91 | } 92 | } 93 | 94 | @SuppressWarnings("unchecked") 95 | public T get() { 96 | ThreadLocalRandom r = ThreadLocalRandom.current(); 97 | int column = r.nextInt(probability.length); 98 | boolean coinToss = r.nextDouble() < probability[column]; 99 | int index = coinToss ? column : alias[column]; 100 | return (T) values[index]; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/WeightFailoverTestMissingNode.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 4 | import static java.util.Collections.emptyList; 5 | import static java.util.concurrent.TimeUnit.SECONDS; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | /** 12 | * @author w.vela 13 | * Created on 2017-12-18. 14 | */ 15 | class WeightFailoverTestMissingNode { 16 | 17 | @Test 18 | void testMissingEnable() { 19 | WeightFailover failover = WeightFailover. newGenericBuilder() 20 | .checker(s -> { 21 | System.err.println("check:" + s); 22 | return s.equals("test1"); 23 | }, 1) 24 | .checkDuration(1, SECONDS) 25 | .autoAddOnMissing(10) 26 | .build(emptyList()); 27 | assertTrue(failover.getAll().isEmpty()); 28 | assertTrue(failover.getAvailable().isEmpty()); 29 | assertTrue(failover.getFailed().isEmpty()); 30 | 31 | failover.success("test"); 32 | 33 | assertTrue(failover.getAll().contains("test")); 34 | assertTrue(failover.getAvailable().contains("test")); 35 | assertFalse(failover.getFailed().contains("test")); 36 | 37 | failover.fail("test2"); 38 | assertTrue(failover.getAll().contains("test2")); 39 | assertTrue(failover.getAvailable().contains("test2")); 40 | assertFalse(failover.getFailed().contains("test2")); 41 | 42 | failover.down("test1"); 43 | assertTrue(failover.getAll().contains("test1")); 44 | assertFalse(failover.getAvailable().contains("test1")); 45 | assertTrue(failover.getFailed().contains("test1")); 46 | 47 | sleepUninterruptibly(2, SECONDS); 48 | 49 | assertTrue(failover.getAll().contains("test1")); 50 | assertTrue(failover.getAvailable().contains("test1")); 51 | assertFalse(failover.getFailed().contains("test1")); 52 | } 53 | 54 | @Test 55 | void testNormal() { 56 | WeightFailover failover = WeightFailover. newGenericBuilder() 57 | .checker(s -> { 58 | System.err.println("check:" + s); 59 | return s.equals("test1"); 60 | }, 1) 61 | .checkDuration(1, SECONDS) 62 | .build(emptyList()); 63 | assertTrue(failover.getAll().isEmpty()); 64 | assertTrue(failover.getAvailable().isEmpty()); 65 | assertTrue(failover.getFailed().isEmpty()); 66 | 67 | failover.success("test"); 68 | 69 | assertFalse(failover.getAll().contains("test")); 70 | assertFalse(failover.getAvailable().contains("test")); 71 | assertFalse(failover.getFailed().contains("test")); 72 | 73 | failover.fail("test2"); 74 | assertFalse(failover.getAll().contains("test2")); 75 | assertFalse(failover.getAvailable().contains("test2")); 76 | assertFalse(failover.getFailed().contains("test2")); 77 | 78 | failover.down("test1"); 79 | assertFalse(failover.getAll().contains("test1")); 80 | assertFalse(failover.getAvailable().contains("test1")); 81 | assertFalse(failover.getFailed().contains("test1")); 82 | 83 | sleepUninterruptibly(2, SECONDS); 84 | 85 | assertFalse(failover.getAll().contains("test1")); 86 | assertFalse(failover.getAvailable().contains("test1")); 87 | assertFalse(failover.getFailed().contains("test1")); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/SharedResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertNull; 6 | import static org.junit.jupiter.api.Assertions.assertSame; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | import static org.junit.jupiter.api.Assertions.fail; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | import com.github.phantomthief.failover.util.SharedResource.UnregisterFailedException; 14 | 15 | /** 16 | * @author w.vela 17 | * Created on 16/2/19. 18 | */ 19 | class SharedResourceTest { 20 | 21 | private static SharedResource resources = new SharedResource<>(); 22 | 23 | @Test 24 | void test() { 25 | MockResource mock = resources.register("1", MockResource::new); 26 | assertSame(mock, resources.get("1")); 27 | assertSame(mock, resources.register("1", MockResource::new)); 28 | resources.register("2", MockResource::new); 29 | 30 | MockResource mockResource1 = resources.get("1"); 31 | MockResource mockResource2 = resources.get("1"); 32 | assertNotNull(mockResource1); 33 | assertSame(mockResource1, mockResource2); 34 | assertFalse(mockResource1.isShutdown()); 35 | 36 | resources.unregister("1", MockResource::close); 37 | mockResource1 = resources.get("1"); 38 | assertNotNull(mockResource1); 39 | assertFalse(mockResource1.isShutdown()); 40 | 41 | resources.unregister("1", MockResource::close); 42 | assertTrue(mockResource1.isShutdown()); 43 | mockResource1 = resources.get("1"); 44 | assertNull(mockResource1); 45 | } 46 | 47 | @Test 48 | void testUnpairUnregister() { 49 | resources.register("3", MockResource::new); 50 | MockResource mock = resources.get("3"); 51 | assertSame(mock, resources.unregister("3", MockResource::close)); 52 | assertThrows(IllegalStateException.class, 53 | () -> resources.unregister("3", MockResource::close)); 54 | } 55 | 56 | @Test 57 | void testCleanupFailed() { 58 | resources.register("4", MockResource::new); 59 | MockResource mock = resources.get("4"); 60 | UnregisterFailedException e = assertThrows(UnregisterFailedException.class, 61 | () -> resources.unregister("4", it -> { 62 | throw new IllegalArgumentException(); 63 | })); 64 | assertSame(mock, e.getRemoved()); 65 | assertSame(IllegalArgumentException.class, e.getCause().getClass()); 66 | } 67 | 68 | @Test 69 | void testRegFail() { 70 | assertThrows(RuntimeException.class, () -> resources.register("5", (s) -> { 71 | throw new RuntimeException(); 72 | })); 73 | MockResource mockResource = resources.get("5"); 74 | assertNull(mockResource); 75 | assertThrows(IllegalStateException.class, 76 | () -> resources.unregister("5", MockResource::close)); 77 | } 78 | 79 | private static class MockResource { 80 | 81 | private String name; 82 | private boolean shutdown = false; 83 | 84 | MockResource(String name) { 85 | this.name = name; 86 | } 87 | 88 | boolean isShutdown() { 89 | return shutdown; 90 | } 91 | 92 | void close() { 93 | if (shutdown) { 94 | fail("failed"); 95 | } 96 | shutdown = true; 97 | System.out.println("shutdown:" + name); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/benchmark/Group1PriorityFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.benchmark; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.annotations.BenchmarkMode; 5 | import org.openjdk.jmh.annotations.Fork; 6 | import org.openjdk.jmh.annotations.Measurement; 7 | import org.openjdk.jmh.annotations.Mode; 8 | import org.openjdk.jmh.annotations.Param; 9 | import org.openjdk.jmh.annotations.Scope; 10 | import org.openjdk.jmh.annotations.Setup; 11 | import org.openjdk.jmh.annotations.State; 12 | import org.openjdk.jmh.annotations.Threads; 13 | import org.openjdk.jmh.annotations.Warmup; 14 | import org.openjdk.jmh.runner.Runner; 15 | import org.openjdk.jmh.runner.RunnerException; 16 | import org.openjdk.jmh.runner.options.Options; 17 | import org.openjdk.jmh.runner.options.OptionsBuilder; 18 | 19 | import com.github.phantomthief.failover.impl.PriorityFailover; 20 | import com.github.phantomthief.failover.impl.PriorityFailoverBuilder; 21 | import com.github.phantomthief.failover.impl.SimpleWeightFunction; 22 | 23 | /** 24 | * @author huangli 25 | * Created on 2020-01-21 26 | */ 27 | @BenchmarkMode(Mode.Throughput) 28 | @Fork(1) 29 | @Threads(200) 30 | @Warmup(iterations = 1, time = 1) 31 | @Measurement(iterations = 2, time = 2) 32 | @State(Scope.Benchmark) 33 | public class Group1PriorityFailover { 34 | 35 | @Param({"5", "20", "100", "200", "1000"}) 36 | private int totalSize; 37 | 38 | private static final int FAIL_RATE = 999; 39 | 40 | private PriorityFailover priorityFailover; 41 | 42 | private long count; 43 | 44 | @Setup 45 | public void init() { 46 | PriorityFailoverBuilder builder = PriorityFailover. newBuilder(); 47 | for (int i = 0; i < totalSize; i++) { 48 | builder.addResource("key" + i, 100); 49 | } 50 | builder.weightFunction(new SimpleWeightFunction<>(0.01, 1.0)); 51 | priorityFailover = builder.build(); 52 | } 53 | 54 | 55 | @Benchmark 56 | public void getOneSuccess() { 57 | for (int i = 0; i < 1000; i++) { 58 | String one = priorityFailover.getOneAvailable(); 59 | priorityFailover.success(one); 60 | } 61 | } 62 | 63 | @Benchmark 64 | public void getOneFail() { 65 | for (int i = 0; i < 1000; i++) { 66 | String one = priorityFailover.getOneAvailable(); 67 | if (count++ % FAIL_RATE == 0) { 68 | priorityFailover.fail(one); 69 | } else { 70 | priorityFailover.success(one); 71 | } 72 | } 73 | } 74 | 75 | public static void main(String[] args) throws RunnerException { 76 | boolean useJmh = true; 77 | if (useJmh) { 78 | Options options = new OptionsBuilder() 79 | .include(Group1PriorityFailover.class.getSimpleName()) 80 | .output(System.getProperty("user.home") + "/" + Group1PriorityFailover.class.getSimpleName() 81 | + ".txt") 82 | .build(); 83 | new Runner(options).run(); 84 | } else { 85 | Group1PriorityFailover obj = new Group1PriorityFailover(); 86 | obj.totalSize = 5; 87 | obj.init(); 88 | int loopCount = 1_0000_0000; 89 | for (int i = 0; i < 100000; i++) { 90 | obj.getOneFail(); 91 | } 92 | long start = System.nanoTime(); 93 | for (int i = 0; i < loopCount; i++) { 94 | obj.getOneFail(); 95 | } 96 | long end = System.nanoTime(); 97 | double seconds = (end - start) / 1000.0 / 1000.0 / 1000.0; 98 | System.out.printf("tps:%.2f", 1.0 * loopCount / seconds); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/benchmark/Group1WeightFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.benchmark; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.annotations.BenchmarkMode; 5 | import org.openjdk.jmh.annotations.Fork; 6 | import org.openjdk.jmh.annotations.Measurement; 7 | import org.openjdk.jmh.annotations.Mode; 8 | import org.openjdk.jmh.annotations.Param; 9 | import org.openjdk.jmh.annotations.Scope; 10 | import org.openjdk.jmh.annotations.Setup; 11 | import org.openjdk.jmh.annotations.State; 12 | import org.openjdk.jmh.annotations.Threads; 13 | import org.openjdk.jmh.annotations.Warmup; 14 | import org.openjdk.jmh.runner.Runner; 15 | import org.openjdk.jmh.runner.RunnerException; 16 | import org.openjdk.jmh.runner.options.Options; 17 | import org.openjdk.jmh.runner.options.OptionsBuilder; 18 | 19 | import com.github.phantomthief.failover.impl.WeightFailover; 20 | import com.google.common.collect.ImmutableMap; 21 | import com.google.common.collect.ImmutableMap.Builder; 22 | 23 | /** 24 | * @author huangli 25 | * Created on 2020-01-21 26 | */ 27 | @BenchmarkMode(Mode.Throughput) 28 | @Fork(1) 29 | @Threads(200) 30 | @Warmup(iterations = 1, time = 1) 31 | @Measurement(iterations = 2, time = 2) 32 | @State(Scope.Benchmark) 33 | public class Group1WeightFailover { 34 | 35 | @Param({"5", "20", "100", "200", "1000"}) 36 | private int totalSize; 37 | 38 | private static final int FAIL_RATE = 999; 39 | 40 | private WeightFailover weightFailover; 41 | 42 | private long count; 43 | 44 | @Setup 45 | public void init() { 46 | Builder builder = ImmutableMap.builder(); 47 | for (int i = 0; i < totalSize; ++i) { 48 | builder.put("key" + i, 100); 49 | } 50 | weightFailover = WeightFailover. newGenericBuilder() 51 | .checker(it -> true, 1) 52 | .failReduceRate(0.01) 53 | .successIncreaseRate(1.0) 54 | .build(builder.build()); 55 | } 56 | 57 | @Benchmark 58 | public void getOneSuccess() { 59 | for (int i = 0; i < 1000; i++) { 60 | String one = weightFailover.getOneAvailable(); 61 | weightFailover.success(one); 62 | } 63 | } 64 | 65 | @Benchmark 66 | public void getOneFail() { 67 | for (int i = 0; i < 1000; i++) { 68 | String one = weightFailover.getOneAvailable(); 69 | if (count++ % FAIL_RATE == 0) { 70 | weightFailover.fail(one); 71 | } else { 72 | weightFailover.success(one); 73 | } 74 | } 75 | } 76 | 77 | public static void main(String[] args) throws RunnerException { 78 | boolean useJmh = true; 79 | if (useJmh) { 80 | Options options = new OptionsBuilder() 81 | .include(Group1WeightFailover.class.getSimpleName()) 82 | .output(System.getProperty("user.home") + "/" + Group1WeightFailover.class.getSimpleName() 83 | + ".txt") 84 | .build(); 85 | new Runner(options).run(); 86 | } else { 87 | Group1WeightFailover obj = new Group1WeightFailover(); 88 | obj.totalSize = 5; 89 | obj.init(); 90 | int loopCount = 1_0000_0000; 91 | for (int i = 0; i < 100000; i++) { 92 | obj.getOneSuccess(); 93 | } 94 | long start = System.nanoTime(); 95 | for (int i = 0; i < loopCount; i++) { 96 | obj.getOneSuccess(); 97 | } 98 | long end = System.nanoTime(); 99 | double seconds = (end - start) / 1000.0 / 1000.0 / 1000.0; 100 | System.out.printf("tps:%.2f", 1.0 * loopCount / seconds); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/RecoverableCheckFailoverBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | import static java.util.concurrent.TimeUnit.MINUTES; 5 | import static java.util.concurrent.TimeUnit.SECONDS; 6 | import static org.slf4j.LoggerFactory.getLogger; 7 | 8 | import java.util.List; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.function.Predicate; 11 | 12 | import javax.annotation.CheckReturnValue; 13 | import javax.annotation.Nonnull; 14 | 15 | import org.slf4j.Logger; 16 | 17 | @Deprecated 18 | public final class RecoverableCheckFailoverBuilder { 19 | 20 | private static final int DEFAULT_FAIL_COUNT = 10; 21 | private static final long DEFAULT_FAIL_DURATION = MINUTES.toMillis(1); 22 | private static final long DEFAULT_RECOVERY_CHECK_DURATION = SECONDS.toMillis(5); 23 | private static final Logger logger = getLogger(RecoverableCheckFailover.class); 24 | private int failCount; 25 | private long failDuration; 26 | private long recoveryCheckDuration; 27 | private boolean returnOriginalWhileAllFailed; 28 | private Predicate checker; 29 | 30 | @CheckReturnValue 31 | @Nonnull 32 | public RecoverableCheckFailoverBuilder setFailCount(int failCount) { 33 | this.failCount = failCount; 34 | return this; 35 | } 36 | 37 | @SuppressWarnings("unchecked") 38 | @CheckReturnValue 39 | @Nonnull 40 | public RecoverableCheckFailoverBuilder setChecker(Predicate checker) { 41 | RecoverableCheckFailoverBuilder thisBuilder = (RecoverableCheckFailoverBuilder) this; 42 | thisBuilder.checker = thisBuilder.catching((Predicate) checker); 43 | return thisBuilder; 44 | } 45 | 46 | @CheckReturnValue 47 | @Nonnull 48 | public RecoverableCheckFailoverBuilder setRecoveryCheckDuration(long recoveryCheckDuration, 49 | TimeUnit unit) { 50 | this.recoveryCheckDuration = unit.toMillis(recoveryCheckDuration); 51 | return this; 52 | } 53 | 54 | @CheckReturnValue 55 | @Nonnull 56 | public RecoverableCheckFailoverBuilder setFailDuration(long failDuration, TimeUnit unit) { 57 | this.failDuration = unit.toMillis(failDuration); 58 | return this; 59 | } 60 | 61 | @CheckReturnValue 62 | @Nonnull 63 | public RecoverableCheckFailoverBuilder 64 | setReturnOriginalWhileAllFailed(boolean returnOriginalWhileAllFailed) { 65 | this.returnOriginalWhileAllFailed = returnOriginalWhileAllFailed; 66 | return this; 67 | } 68 | 69 | @SuppressWarnings("unchecked") 70 | @Nonnull 71 | public RecoverableCheckFailover build(List original) { 72 | RecoverableCheckFailoverBuilder thisBuilder = (RecoverableCheckFailoverBuilder) this; 73 | thisBuilder.ensure(); 74 | return new RecoverableCheckFailover<>((List) original, thisBuilder.checker, failCount, 75 | failDuration, recoveryCheckDuration, returnOriginalWhileAllFailed); 76 | } 77 | 78 | private void ensure() { 79 | checkNotNull(checker); 80 | 81 | if (failCount <= 0) { 82 | failCount = DEFAULT_FAIL_COUNT; 83 | } 84 | if (failDuration <= 0) { 85 | failDuration = DEFAULT_FAIL_DURATION; 86 | } 87 | if (recoveryCheckDuration <= 0) { 88 | recoveryCheckDuration = DEFAULT_RECOVERY_CHECK_DURATION; 89 | } 90 | } 91 | 92 | private Predicate catching(Predicate predicate) { 93 | return t -> { 94 | try { 95 | return predicate.test(t); 96 | } catch (Throwable e) { 97 | logger.error("Ops. fail to test:{}", t, e); 98 | return false; 99 | } 100 | }; 101 | } 102 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/RetryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static java.util.function.Function.identity; 4 | import static java.util.stream.Collectors.toMap; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import java.util.Map; 10 | import java.util.concurrent.TimeoutException; 11 | import java.util.stream.IntStream; 12 | 13 | import org.junit.jupiter.api.Test; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import com.github.phantomthief.failover.impl.WeightFailover; 18 | import com.github.phantomthief.util.ThrowableFunction; 19 | 20 | /** 21 | * @author w.vela 22 | * Created on 2017-06-09. 23 | */ 24 | class RetryTest { 25 | 26 | private static final Logger logger = LoggerFactory.getLogger(RetryTest.class); 27 | 28 | @Test 29 | void test() { 30 | WeightFailover failover = buildNewFailover(); 31 | for (int i = 0; i < 50; i++) { 32 | String x = failover.supplyWithRetry(this::doSomething); 33 | logger.debug("test:{}", x); 34 | assertEquals(x, "r:1"); 35 | } 36 | failover = buildNewFailover(); 37 | System.out.println("test fail"); 38 | WeightFailover thisFailover = failover; 39 | for (int i = 0; i < 50; i++) { 40 | assertThrows(Throwable.class, () -> { 41 | String x = thisFailover.supplyWithRetry(this::alwaysFail); 42 | logger.debug("test:{}", x); 43 | }); 44 | } 45 | failover = buildNewFailover(); 46 | logger.info("test some failed."); 47 | for (int i = 0; i < 50; i++) { 48 | try { 49 | String x = failover.supplyWithRetry(this::doSomething); 50 | logger.debug("test:{}", x); 51 | assertEquals(x, "r:1"); 52 | } catch (Throwable e) { 53 | assertTrue(true); 54 | logger.info("fail, pass."); 55 | } 56 | } 57 | } 58 | 59 | @Test 60 | void testRetry(){ 61 | WeightFailover failover = buildNewFailover(); 62 | assertThrows(TimeoutException.class, () -> failover.supplyWithRetry(i -> { 63 | throw new TimeoutException(); 64 | })); 65 | WeightFailover failover2 = buildNewFailover(); 66 | assertThrows(ArithmeticException.class, () -> { 67 | ThrowableFunction func = i -> { 68 | int j = 0; 69 | System.out.println(j / 0); 70 | throw new TimeoutException(); 71 | }; 72 | failover2.supplyWithRetry(func); 73 | }); 74 | } 75 | 76 | @Test 77 | void testRunRetry() { 78 | WeightFailover failover = buildNewFailover(); 79 | for (int i = 0; i < 10; i++) { 80 | failover.runWithRetry(it -> { 81 | if (it == 1) { 82 | throw new IllegalStateException(); 83 | } 84 | }); 85 | } 86 | } 87 | 88 | private WeightFailover buildNewFailover() { 89 | Map weightMap = IntStream.range(1, 4).boxed() 90 | .collect(toMap(identity(), i -> 5)); 91 | return WeightFailover. newGenericBuilder() 92 | .checker(it -> false, 1) 93 | .build(weightMap); 94 | } 95 | 96 | private String doSomething(Integer i) { 97 | if (i > 1) { 98 | throw new RuntimeException("failed"); 99 | } 100 | return "r:" + i; 101 | } 102 | 103 | private String alwaysFail(Integer i) { 104 | if (i > 0) { 105 | throw new RuntimeException("failed"); 106 | } 107 | return "r:" + i; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/benchmark/Group2PartitionFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.benchmark; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.annotations.BenchmarkMode; 5 | import org.openjdk.jmh.annotations.Fork; 6 | import org.openjdk.jmh.annotations.Measurement; 7 | import org.openjdk.jmh.annotations.Mode; 8 | import org.openjdk.jmh.annotations.Param; 9 | import org.openjdk.jmh.annotations.Scope; 10 | import org.openjdk.jmh.annotations.Setup; 11 | import org.openjdk.jmh.annotations.State; 12 | import org.openjdk.jmh.annotations.Threads; 13 | import org.openjdk.jmh.annotations.Warmup; 14 | import org.openjdk.jmh.runner.Runner; 15 | import org.openjdk.jmh.runner.RunnerException; 16 | import org.openjdk.jmh.runner.options.Options; 17 | import org.openjdk.jmh.runner.options.OptionsBuilder; 18 | 19 | import com.github.phantomthief.failover.impl.PartitionFailover; 20 | import com.google.common.collect.ImmutableMap; 21 | import com.google.common.collect.ImmutableMap.Builder; 22 | 23 | /** 24 | * @author huangli 25 | * Created on 2020-01-21 26 | */ 27 | @BenchmarkMode(Mode.Throughput) 28 | @Fork(1) 29 | @Threads(200) 30 | @Warmup(iterations = 1, time = 1) 31 | @Measurement(iterations = 2, time = 2) 32 | @State(Scope.Benchmark) 33 | public class Group2PartitionFailover { 34 | 35 | @Param({"1000"}) 36 | private int totalSize; 37 | 38 | @Param({"5", "20"}) 39 | private int coreSize; 40 | 41 | private static final int FAIL_RATE = 999; 42 | 43 | private PartitionFailover partitionFailover; 44 | 45 | private long count; 46 | 47 | @Setup 48 | public void init() { 49 | Builder builder = ImmutableMap.builder(); 50 | for (int i = 0; i < totalSize; ++i) { 51 | builder.put("key" + i, 100); 52 | } 53 | partitionFailover = PartitionFailover. newBuilder() 54 | .checker(it -> true, 1) 55 | .corePartitionSize(coreSize) 56 | .failReduceRate(0.01) 57 | .successIncreaseRate(1.0) 58 | .build(builder.build()); 59 | } 60 | 61 | @Benchmark 62 | public void getOneSuccess() { 63 | for (int i = 0; i < 1000; i++) { 64 | String one = partitionFailover.getOneAvailable(); 65 | partitionFailover.success(one); 66 | } 67 | } 68 | 69 | 70 | @Benchmark 71 | public void getOneFail() { 72 | for (int i = 0; i < 1000; i++) { 73 | String one = partitionFailover.getOneAvailable(); 74 | if (count++ % FAIL_RATE == 0) { 75 | partitionFailover.fail(one); 76 | } else { 77 | partitionFailover.success(one); 78 | } 79 | } 80 | } 81 | 82 | public static void main(String[] args) throws RunnerException { 83 | boolean useJmh = true; 84 | if (useJmh) { 85 | Options options = new OptionsBuilder() 86 | .include(Group2PartitionFailover.class.getSimpleName()) 87 | .output(System.getProperty("user.home") + "/" + Group2PartitionFailover.class.getSimpleName() 88 | + ".txt") 89 | .build(); 90 | new Runner(options).run(); 91 | } else { 92 | Group2PartitionFailover obj = new Group2PartitionFailover(); 93 | obj.totalSize = 1000; 94 | obj.coreSize = 5; 95 | obj.init(); 96 | int loopCount = 5000_0000; 97 | for (int i = 0; i < 100000; i++) { 98 | obj.getOneSuccess(); 99 | } 100 | long start = System.nanoTime(); 101 | for (int i = 0; i < loopCount; i++) { 102 | obj.getOneSuccess(); 103 | } 104 | long end = System.nanoTime(); 105 | double seconds = (end - start) / 1000.0 / 1000.0 / 1000.0; 106 | System.out.printf("tps:%.2f", 1.0 * loopCount / seconds); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/GenericWeightFailoverBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.function.Consumer; 7 | import java.util.function.Predicate; 8 | 9 | import javax.annotation.CheckReturnValue; 10 | import javax.annotation.Nonnegative; 11 | import javax.annotation.Nonnull; 12 | 13 | import com.github.phantomthief.util.ThrowableFunction; 14 | import com.github.phantomthief.util.ThrowablePredicate; 15 | 16 | /** 17 | * @author w.vela 18 | */ 19 | public class GenericWeightFailoverBuilder { 20 | 21 | private final WeightFailoverBuilder builder; 22 | 23 | public GenericWeightFailoverBuilder(WeightFailoverBuilder builder) { 24 | this.builder = builder; 25 | } 26 | 27 | @CheckReturnValue 28 | @Nonnull 29 | public GenericWeightFailoverBuilder name(String value) { 30 | builder.name(value); 31 | return this; 32 | } 33 | 34 | @CheckReturnValue 35 | @Nonnull 36 | public GenericWeightFailoverBuilder autoAddOnMissing(int weight) { 37 | builder.autoAddOnMissing(weight); 38 | return this; 39 | } 40 | 41 | @CheckReturnValue 42 | @Nonnull 43 | public GenericWeightFailoverBuilder failReduceRate(double rate) { 44 | builder.failReduceRate(rate); 45 | return this; 46 | } 47 | 48 | @CheckReturnValue 49 | @Nonnull 50 | public GenericWeightFailoverBuilder failReduce(int weight) { 51 | builder.failReduce(weight); 52 | return this; 53 | } 54 | 55 | @CheckReturnValue 56 | @Nonnull 57 | public GenericWeightFailoverBuilder onMinWeight(Consumer listener) { 58 | builder.onMinWeight(listener); 59 | return this; 60 | } 61 | 62 | @CheckReturnValue 63 | @Nonnull 64 | public GenericWeightFailoverBuilder onRecovered(Consumer listener) { 65 | builder.onRecovered(listener); 66 | return this; 67 | } 68 | 69 | @CheckReturnValue 70 | @Nonnull 71 | public GenericWeightFailoverBuilder successIncreaseRate(double rate) { 72 | builder.successIncreaseRate(rate); 73 | return this; 74 | } 75 | 76 | @CheckReturnValue 77 | @Nonnull 78 | public GenericWeightFailoverBuilder successIncrease(int weight) { 79 | builder.successIncrease(weight); 80 | return this; 81 | } 82 | 83 | @CheckReturnValue 84 | @Nonnull 85 | public GenericWeightFailoverBuilder minWeight(int weight) { 86 | builder.minWeight(weight); 87 | return this; 88 | } 89 | 90 | @CheckReturnValue 91 | @Nonnull 92 | public GenericWeightFailoverBuilder checkDuration(long time, TimeUnit unit) { 93 | builder.checkDuration(time, unit); 94 | return this; 95 | } 96 | 97 | @CheckReturnValue 98 | @Nonnull 99 | public GenericWeightFailoverBuilder 100 | checker(@Nonnull ThrowableFunction failChecker) { 101 | builder.checker(failChecker); 102 | return this; 103 | } 104 | 105 | @CheckReturnValue 106 | @Nonnull 107 | public GenericWeightFailoverBuilder checker( 108 | @Nonnull ThrowablePredicate failChecker, 109 | @Nonnegative double recoveredInitRate) { 110 | builder.checker(failChecker, recoveredInitRate); 111 | return this; 112 | } 113 | 114 | @CheckReturnValue 115 | @Nonnull 116 | public GenericWeightFailoverBuilder filter(Predicate filter) { 117 | builder.filter(filter); 118 | return this; 119 | } 120 | 121 | @Nonnull 122 | public WeightFailover build(Collection original) { 123 | return builder.build(original); 124 | } 125 | 126 | @Nonnull 127 | public WeightFailover build(Collection original, int initWeight) { 128 | return builder.build(original, initWeight); 129 | } 130 | 131 | @Nonnull 132 | public WeightFailover build(Map original) { 133 | return builder.build(original); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/benchmark/Group2PriorityFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.benchmark; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.annotations.BenchmarkMode; 5 | import org.openjdk.jmh.annotations.Fork; 6 | import org.openjdk.jmh.annotations.Measurement; 7 | import org.openjdk.jmh.annotations.Mode; 8 | import org.openjdk.jmh.annotations.Param; 9 | import org.openjdk.jmh.annotations.Scope; 10 | import org.openjdk.jmh.annotations.Setup; 11 | import org.openjdk.jmh.annotations.State; 12 | import org.openjdk.jmh.annotations.Threads; 13 | import org.openjdk.jmh.annotations.Warmup; 14 | import org.openjdk.jmh.runner.Runner; 15 | import org.openjdk.jmh.runner.RunnerException; 16 | import org.openjdk.jmh.runner.options.Options; 17 | import org.openjdk.jmh.runner.options.OptionsBuilder; 18 | 19 | import com.github.phantomthief.failover.impl.PriorityFailover; 20 | import com.github.phantomthief.failover.impl.PriorityFailoverBuilder; 21 | import com.github.phantomthief.failover.impl.SimpleWeightFunction; 22 | 23 | /** 24 | * @author huangli 25 | * Created on 2020-01-21 26 | */ 27 | @BenchmarkMode(Mode.Throughput) 28 | @Fork(1) 29 | @Threads(200) 30 | @Warmup(iterations = 1, time = 1) 31 | @Measurement(iterations = 2, time = 2) 32 | @State(Scope.Benchmark) 33 | public class Group2PriorityFailover { 34 | 35 | @Param({"1000"}) 36 | private int totalSize; 37 | 38 | @Param({"5", "20"}) 39 | private int coreSize; 40 | 41 | @Param({"true", "false"}) 42 | private boolean concurrencyCtrl; 43 | 44 | private static final int FAIL_RATE = 999; 45 | 46 | private PriorityFailover priorityFailover; 47 | 48 | private long count; 49 | 50 | @Setup 51 | public void init() { 52 | PriorityFailoverBuilder builder = PriorityFailover. newBuilder(); 53 | for (int i = 0; i < totalSize; i++) { 54 | if (i < coreSize) { 55 | builder.addResource("key" + i, 100, 0, 0, 100); 56 | } else { 57 | builder.addResource("key" + i, 100, 0, 1, 100); 58 | } 59 | } 60 | builder.concurrencyControl(concurrencyCtrl); 61 | builder.weightFunction(new SimpleWeightFunction<>(0.01, 1.0)); 62 | priorityFailover = builder.build(); 63 | } 64 | 65 | 66 | @Benchmark 67 | public void getOneSuccess() { 68 | for (int i = 0; i < 1000; i++) { 69 | String one = priorityFailover.getOneAvailable(); 70 | priorityFailover.success(one); 71 | } 72 | } 73 | 74 | @Benchmark 75 | public void getOneFail() { 76 | for (int i = 0; i < 1000; i++) { 77 | String one = priorityFailover.getOneAvailable(); 78 | if (count++ % FAIL_RATE == 0) { 79 | priorityFailover.fail(one); 80 | } else { 81 | priorityFailover.success(one); 82 | } 83 | } 84 | } 85 | 86 | public static void main(String[] args) throws RunnerException { 87 | boolean useJmh = true; 88 | if (useJmh) { 89 | Options options = new OptionsBuilder() 90 | .include(Group2PriorityFailover.class.getSimpleName()) 91 | .output(System.getProperty("user.home") + "/" + Group2PriorityFailover.class.getSimpleName() 92 | + ".txt") 93 | .build(); 94 | new Runner(options).run(); 95 | } else { 96 | Group2PriorityFailover obj = new Group2PriorityFailover(); 97 | obj.totalSize = 1000; 98 | obj.coreSize = 5; 99 | obj.concurrencyCtrl = true; 100 | obj.init(); 101 | int loopCount = 5000_0000; 102 | for (int i = 0; i < 100000; i++) { 103 | obj.getOneSuccess(); 104 | } 105 | long start = System.nanoTime(); 106 | for (int i = 0; i < loopCount; i++) { 107 | obj.getOneSuccess(); 108 | } 109 | long end = System.nanoTime(); 110 | double seconds = (end - start) / 1000.0 / 1000.0 / 1000.0; 111 | System.out.printf("tps:%.2f", 1.0 * loopCount / seconds); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/ComboFailoverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.google.common.base.Throwables.throwIfUnchecked; 4 | import static com.google.common.collect.ImmutableList.of; 5 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 6 | import static java.util.Collections.singleton; 7 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertFalse; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | import org.junit.jupiter.api.Test; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import com.github.phantomthief.util.ThrowableFunction; 17 | import com.google.common.collect.ImmutableList; 18 | 19 | /** 20 | * @author w.vela 21 | * Created on 2017-12-28. 22 | */ 23 | class ComboFailoverTest { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(ComboFailoverTest.class); 26 | 27 | @Test 28 | void test() { 29 | boolean[] checkerSwitch = { true }; 30 | ThrowableFunction checker = value -> { 31 | if (checkerSwitch[0]) { 32 | return check(value); 33 | } else { 34 | return 0.0D; 35 | } 36 | }; 37 | ComboFailover combo = ComboFailover. builder() 38 | .add(WeightFailover. newGenericBuilder() 39 | .checker(checker) 40 | .checkDuration(10, MILLISECONDS) 41 | .failReduceRate(0.1) 42 | .build(of("test1", "test2"))) 43 | .add(WeightFailover. newGenericBuilder() 44 | .checker(checker) 45 | .checkDuration(10, MILLISECONDS) 46 | .failReduceRate(0.1) 47 | .build(of("test3", "test4"))) 48 | .build(); 49 | checkerSwitch[0] = false; 50 | ImmutableList all = of("test1", "test2", "test3", "test4"); 51 | assertTrue(all.containsAll(combo.getAll())); 52 | assertTrue(all.containsAll(combo.getAvailable())); 53 | assertTrue(all.contains(combo.getOneAvailable())); 54 | for (int i = 0; i < 10; i++) { 55 | assertTrue(of("test1", "test2") 56 | .contains(combo.getOneAvailableExclude(of("test3", "test4")))); 57 | assertFalse(of("test3", "test4") 58 | .contains(combo.getOneAvailableExclude(of("test3", "test4")))); 59 | } 60 | 61 | for (int i = 0; i < 10; i++) { 62 | combo.fail("test1"); 63 | } 64 | assertEquals(singleton("test1"), combo.getFailed()); 65 | for (int i = 0; i < 10; i++) { 66 | assertFalse(of("test3").contains(combo.getOneAvailable())); 67 | } 68 | checkerSwitch[0] = true; 69 | sleepUninterruptibly(100, MILLISECONDS); 70 | checkerSwitch[0] = false; 71 | assertTrue(combo.getFailed().isEmpty()); 72 | for (int i = 0; i < 10; i++) { 73 | assertTrue(of("test3") 74 | .contains(combo.getOneAvailableExclude(of("test1", "test2", "test4")))); 75 | } 76 | 77 | for (int i = 0; i < 10; i++) { 78 | assertTrue(of("test1", "test2").contains(combo.getOneAvailable())); 79 | assertFalse(of("test3", "test4").contains(combo.getOneAvailable())); 80 | } 81 | combo.down("test1"); 82 | combo.down("test2"); 83 | for (int i = 0; i < 10; i++) { 84 | assertFalse(of("test1", "test2").contains(combo.getOneAvailable())); 85 | assertTrue(of("test3", "test4").contains(combo.getOneAvailable())); 86 | } 87 | checkerSwitch[0] = true; 88 | sleepUninterruptibly(100, MILLISECONDS); 89 | checkerSwitch[0] = false; 90 | 91 | combo.forEach(failover -> { 92 | if (failover instanceof AutoCloseable) { 93 | try { 94 | ((AutoCloseable) failover).close(); 95 | } catch (Exception e) { 96 | throwIfUnchecked(e); 97 | throw new RuntimeException(e); 98 | } 99 | } 100 | }); 101 | } 102 | 103 | private double check(String value) { 104 | logger.info("check:{}", value); 105 | return 0.5D; 106 | } 107 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/RatioWeightFunction.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | /** 4 | * 这个函数等比递减权重,在失败的时候可以更加迅速的扣减权重。 5 | * 比如默认情况下,假设初始权重为1,失败一次后剩0.5,再失败一次剩0.25,连续失败7次会down 6 | * (依据downThresholdRateOfMaxWeight,默认值0.01)。 7 | * 8 | * @author huangli 9 | * Created on 2020-05-06 10 | */ 11 | public class RatioWeightFunction extends AbstractWeightFunction { 12 | 13 | private static final double DEFAULT_FAIL_DECREASE_RATE = 0.5; 14 | private static final double DEFAULT_SUCCESS_INCREASE_RATE = 0.01; 15 | private static final double DEFAULT_RECOVER_RATE = 0.1; 16 | private static final double DEFAULT_DOWN_THRESHOLD_RATE = 0.01; 17 | 18 | private final double failKeepRateOfCurrentWeight; 19 | private final double successIncreaseRateOfMaxWeight; 20 | private final double downThreshold; 21 | 22 | private double downThresholdRateOfMaxWeight = DEFAULT_DOWN_THRESHOLD_RATE; 23 | private double recoverRateOfMaxWeight = DEFAULT_RECOVER_RATE; 24 | 25 | /** 26 | * 使用默认failKeepRateOfCurrentWeight为0.5,successIncreaseRateOfMaxWeight为0.01, 27 | * recoverThreshold为1,downThreshold为0。 28 | */ 29 | public RatioWeightFunction() { 30 | this(DEFAULT_FAIL_DECREASE_RATE, DEFAULT_SUCCESS_INCREASE_RATE); 31 | } 32 | 33 | /** 34 | * 使用默认recoverThreshold为1,downThreshold为0。 35 | * @param failKeepRateOfCurrentWeight 36 | * @param successIncreaseRateOfMaxWeight 37 | */ 38 | public RatioWeightFunction(double failKeepRateOfCurrentWeight, double successIncreaseRateOfMaxWeight) { 39 | this(failKeepRateOfCurrentWeight, successIncreaseRateOfMaxWeight, DEFAULT_RECOVER_THRESHOLD); 40 | } 41 | 42 | /** 43 | * 使用默认downThreshold为0。 44 | * @param failKeepRateOfCurrentWeight 失败以后保留的权重比例(相对于当前权重) 45 | * @param successIncreaseRateOfMaxWeight 成功以后增加权重的比例(相对于最大权重) 46 | * @param recoverThreshold 探活的时候,几次探活成功开始增加权重 47 | */ 48 | public RatioWeightFunction(double failKeepRateOfCurrentWeight, double successIncreaseRateOfMaxWeight, 49 | int recoverThreshold) { 50 | this(failKeepRateOfCurrentWeight, successIncreaseRateOfMaxWeight, recoverThreshold, 0); 51 | } 52 | 53 | /** 54 | * 构造一个实例,指定4个参数。 55 | * @param failKeepRateOfCurrentWeight 失败以后扣减权重的比例(相对于当前权重) 56 | * @param successIncreaseRateOfMaxWeight 成功以后增加权重的比例(相对于最大权重) 57 | * @param recoverThreshold 探活的时候,几次探活成功开始增加权重 58 | * @param downThreshold 权重低于多少直接down(设置为最小权重),这是为了防止等比递减总是减不到0 59 | */ 60 | public RatioWeightFunction(double failKeepRateOfCurrentWeight, double successIncreaseRateOfMaxWeight, 61 | int recoverThreshold, double downThreshold) { 62 | super(recoverThreshold); 63 | if (failKeepRateOfCurrentWeight < 0 || failKeepRateOfCurrentWeight > 1) { 64 | throw new IllegalArgumentException( 65 | "bad failDecreaseRateOfCurrentWeight:" + failKeepRateOfCurrentWeight); 66 | } 67 | if (successIncreaseRateOfMaxWeight < 0 || successIncreaseRateOfMaxWeight > 1) { 68 | throw new IllegalArgumentException("bad successIncreaseRateOfMaxWeight:" + successIncreaseRateOfMaxWeight); 69 | } 70 | if (downThreshold < 0) { 71 | throw new IllegalArgumentException("bad downThreshold:" + downThreshold); 72 | } 73 | this.failKeepRateOfCurrentWeight = failKeepRateOfCurrentWeight; 74 | this.successIncreaseRateOfMaxWeight = successIncreaseRateOfMaxWeight; 75 | this.downThreshold = downThreshold; 76 | } 77 | 78 | @Override 79 | protected double computeSuccess(double maxWeight, double minWeight, int priority, double currentOldWeight, 80 | T resource) { 81 | if (currentOldWeight > minWeight) { 82 | return currentOldWeight + maxWeight * successIncreaseRateOfMaxWeight; 83 | } else { 84 | return currentOldWeight + maxWeight * recoverRateOfMaxWeight; 85 | } 86 | } 87 | 88 | @Override 89 | protected double computeFail(double maxWeight, double minWeight, int priority, double currentOldWeight, 90 | T resource) { 91 | double x = currentOldWeight * failKeepRateOfCurrentWeight; 92 | if (downThreshold == 0) { 93 | return x < maxWeight * downThresholdRateOfMaxWeight ? minWeight : x; 94 | } else { 95 | return x < downThreshold ? minWeight : x; 96 | } 97 | } 98 | 99 | public void setDownThresholdRateOfMaxWeight(double downThresholdRateOfMaxWeight) { 100 | this.downThresholdRateOfMaxWeight = downThresholdRateOfMaxWeight; 101 | } 102 | 103 | public void setRecoverRateOfMaxWeight(double recoverRateOfMaxWeight) { 104 | this.recoverRateOfMaxWeight = recoverRateOfMaxWeight; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/checker/SimplePortChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl.checker; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | import java.net.Socket; 6 | import java.nio.channels.AsynchronousSocketChannel; 7 | import java.nio.channels.CompletionHandler; 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.TimeoutException; 11 | import java.util.function.Supplier; 12 | 13 | import javax.annotation.Nonnegative; 14 | import javax.annotation.Nonnull; 15 | import javax.annotation.Nullable; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import com.google.common.net.HostAndPort; 21 | import com.google.common.util.concurrent.AbstractFuture; 22 | import com.google.common.util.concurrent.ListenableFuture; 23 | 24 | /** 25 | * @author w.vela 26 | */ 27 | public class SimplePortChecker { 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(SimplePortChecker.class); 30 | private static final int DEFAULT_CONNECTION_TIMEOUT = 5000; 31 | 32 | public static boolean check(String host, int port) { 33 | return check(host, port, DEFAULT_CONNECTION_TIMEOUT); 34 | } 35 | 36 | public static boolean check(HostAndPort hostAndPort) { 37 | return check(hostAndPort.getHost(), hostAndPort.getPort()); 38 | } 39 | 40 | public static boolean check(String host, int port, int connectionTimeoutInMs) { 41 | try (Socket socket = new Socket()) { 42 | socket.connect(new InetSocketAddress(host, port), connectionTimeoutInMs); 43 | return true; 44 | } catch (Throwable e) { 45 | return false; 46 | } 47 | } 48 | 49 | @Nonnull 50 | public static ListenableFuture asyncCheck(@Nonnull HostAndPort hostAndPort) { 51 | return asyncCheck(hostAndPort.getHost(), hostAndPort.getPort()); 52 | } 53 | 54 | @Nonnull 55 | public static ListenableFuture asyncCheck(@Nonnull String host, @Nonnegative int port) { 56 | AsynchronousSocketChannel[] channels = { null }; 57 | CheckListenableFuture future = new CheckListenableFuture(() -> channels[0]); 58 | try { 59 | InetSocketAddress hostAddress = new InetSocketAddress(host, port); 60 | AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); 61 | channels[0] = client; 62 | client.connect(hostAddress, null, new CompletionHandler() { 63 | 64 | @Override 65 | public void completed(Void result, Object attachment) { 66 | future.set(result); 67 | try { 68 | client.close(); 69 | } catch (IOException e) { 70 | logger.error("", e); 71 | } 72 | } 73 | 74 | @Override 75 | public void failed(Throwable exc, Object attachment) { 76 | future.setException(exc); 77 | try { 78 | client.close(); 79 | } catch (IOException e) { 80 | logger.error("", e); 81 | } 82 | } 83 | }); 84 | return future; 85 | } catch (Throwable e) { 86 | future.setException(e); 87 | return future; 88 | } 89 | } 90 | 91 | private static class CheckListenableFuture extends AbstractFuture { 92 | 93 | private final Supplier client; 94 | 95 | private CheckListenableFuture(Supplier client) { 96 | this.client = client; 97 | } 98 | 99 | @Override 100 | protected boolean set(@Nullable Void value) { 101 | return super.set(value); 102 | } 103 | 104 | @Override 105 | protected boolean setException(Throwable throwable) { 106 | return super.setException(throwable); 107 | } 108 | 109 | @Override 110 | public Void get(long timeout, TimeUnit unit) 111 | throws InterruptedException, TimeoutException, ExecutionException { 112 | try { 113 | return super.get(timeout, unit); 114 | } catch (TimeoutException e) { 115 | cancel(true); 116 | AsynchronousSocketChannel channel = client.get(); 117 | if (channel != null) { 118 | try { 119 | channel.close(); 120 | } catch (IOException e1) { 121 | logger.error("", e1); 122 | } 123 | } 124 | throw e; 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/PriorityFailoverCheckTask.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import java.util.HashMap; 4 | import java.util.concurrent.ScheduledFuture; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | import java.util.concurrent.atomic.AtomicReference; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import com.github.phantomthief.failover.impl.PriorityFailover.GroupInfo; 13 | import com.github.phantomthief.failover.impl.PriorityFailover.ResInfo; 14 | import com.github.phantomthief.failover.impl.PriorityFailoverBuilder.PriorityFailoverConfig; 15 | 16 | /** 17 | * @author huangli 18 | * Created on 2020-01-20 19 | */ 20 | class PriorityFailoverCheckTask implements Runnable { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(PriorityFailoverCheckTask.class); 23 | 24 | private final PriorityFailoverConfig config; 25 | 26 | private final AtomicReference> futureRef = new AtomicReference<>(); 27 | 28 | private final HashMap> resourcesMap; 29 | private final GroupInfo[] groups; 30 | 31 | private final AtomicBoolean closed = new AtomicBoolean(false); 32 | 33 | /** 34 | * 有时在 ResInfo 持有的资源对象中,会持有 failover 实例,以便调用 failover 的 success/fail 等方法 35 | * 一旦在注册到 GcUtil 的 CloseRunnable 中持有了 PriorityFailoverCheckTask 的引用,将导致 failover 36 | * 始终存在引用,无法被关闭和清理。所以这里单独构造一个 static class,避免隐式持有 this.resourcesMap。 37 | * 导致即使主动关闭了 failover,failover 也无法被 gc 掉 38 | */ 39 | private static class CloseRunnable implements Runnable { 40 | private final AtomicBoolean closed; 41 | private final AtomicReference> futureRef; 42 | 43 | CloseRunnable(AtomicBoolean closed, 44 | AtomicReference> futureRef) { 45 | this.closed = closed; 46 | this.futureRef = futureRef; 47 | } 48 | 49 | @Override 50 | public void run() { 51 | // 这里代码和 close 一样,由于此时 failover 已经被 gc 掉了,所以不需要再加锁了 52 | closed.set(true); 53 | ScheduledFuture scheduledFuture = futureRef.get(); 54 | if (scheduledFuture != null && !scheduledFuture.isCancelled()) { 55 | scheduledFuture.cancel(true); 56 | futureRef.set(null); 57 | } 58 | } 59 | } 60 | 61 | PriorityFailoverCheckTask(PriorityFailoverConfig config, PriorityFailover failover) { 62 | this.config = config; 63 | this.resourcesMap = failover.getResourcesMap(); 64 | this.groups = failover.getGroups(); 65 | if (config.getChecker() != null) { 66 | if (config.isStartCheckTaskImmediately()) { 67 | ensureStart(); 68 | } 69 | GcUtil.register(failover, new CloseRunnable(closed, futureRef)); 70 | GcUtil.doClean(); 71 | } 72 | } 73 | 74 | public void ensureStart() { 75 | if (futureRef.get() == null) { 76 | synchronized (this) { 77 | if (futureRef.get() == null && config.getChecker() != null) { 78 | futureRef.set(config.getCheckExecutor().scheduleWithFixedDelay( 79 | this, config.getCheckDuration().toMillis(), 80 | config.getCheckDuration().toMillis(), TimeUnit.MILLISECONDS)); 81 | } 82 | } 83 | } 84 | } 85 | 86 | @Override 87 | public void run() { 88 | // TODO run in multi threads mode? 89 | 90 | Thread currentThread = Thread.currentThread(); 91 | String origName = currentThread.getName(); 92 | if (config.getName() != null) { 93 | currentThread.setName(origName + "-[" + config.getName() + "]"); 94 | } 95 | try { 96 | for (ResInfo r : resourcesMap.values()) { 97 | try { 98 | if (closed.get()) { 99 | return; 100 | } 101 | if (config.getWeightFunction().needCheck(r.maxWeight, 102 | r.minWeight, r.priority, r.currentWeight, r.resource)) { 103 | boolean ok = config.getChecker().test(r.resource); 104 | if (closed.get()) { 105 | return; 106 | } 107 | PriorityFailover.updateWeight(ok, r, config, groups); 108 | } 109 | } catch (Throwable e) { 110 | // the test may fail, the user's onSuccess/onFail callback may fail 111 | if (config.getName() == null) { 112 | logger.error("failover check/updateWeight fail: {}", e.toString()); 113 | } else { 114 | logger.error("failover({}) check/updateWeight fail: {}", config.getName(), e.toString()); 115 | } 116 | } 117 | } 118 | } finally { 119 | currentThread.setName(origName); 120 | } 121 | } 122 | 123 | public synchronized void close() { 124 | closed.set(true); 125 | ScheduledFuture scheduledFuture = futureRef.get(); 126 | if (scheduledFuture != null && !scheduledFuture.isCancelled()) { 127 | scheduledFuture.cancel(true); 128 | futureRef.set(null); 129 | } 130 | } 131 | 132 | boolean isClosed() { 133 | return closed.get(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/ConcurrencyAwareTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static java.util.Map.Entry; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.Comparator; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.NoSuchElementException; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.concurrent.CountDownLatch; 17 | import java.util.concurrent.atomic.AtomicReference; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import com.google.common.collect.ImmutableList; 22 | 23 | /** 24 | * @author w.vela 25 | * Created on 2018-01-22. 26 | */ 27 | class ConcurrencyAwareTest { 28 | 29 | @Test 30 | void test() throws InterruptedException { 31 | List all = ImmutableList.of("t1", "t2", "t3"); 32 | ConcurrencyAware aware = ConcurrencyAware.create(); 33 | for (int i = 0; i < 1000; i++) { 34 | assertTrue(all.contains(aware.supply(all, it -> it))); 35 | } 36 | for (int i = 0; i < 1000; i++) { 37 | checkIdlest(all, aware); 38 | } 39 | 40 | Map concurrency = new ConcurrentHashMap<>(); 41 | for (int i = 0; i < 1000; i++) { 42 | List countDownLatches1 = new ArrayList<>(); 43 | List countDownLatches2 = new ArrayList<>(); 44 | List threads = new ArrayList<>(); 45 | for (int j = 0; j < 4; j++) { 46 | CountDownLatch c1 = new CountDownLatch(1); 47 | countDownLatches1.add(c1); 48 | CountDownLatch c2 = new CountDownLatch(1); 49 | countDownLatches2.add(c2); 50 | Thread thread = new Thread(() -> { 51 | try { 52 | aware.run(all, it -> { 53 | concurrency.merge(it, 1, Integer::sum); 54 | c1.countDown(); 55 | c2.await(); 56 | concurrency.merge(it, -1, Integer::sum); 57 | }); 58 | } catch (InterruptedException e) { 59 | Thread.currentThread().interrupt(); 60 | } 61 | }); 62 | threads.add(thread); 63 | thread.start(); 64 | } 65 | for (CountDownLatch c1 : countDownLatches1) { 66 | c1.await(); 67 | } 68 | assertNotEquals(concurrency.entrySet().stream() 69 | .sorted(Comparator.> comparingInt(Entry::getValue).reversed()) 70 | .map(Entry::getKey) 71 | .findAny() 72 | .orElse(null), aware.supply(all, it -> it)); 73 | for (CountDownLatch c2 : countDownLatches2) { 74 | c2.countDown(); 75 | } 76 | for (Thread thread : threads) { 77 | thread.join(); 78 | } 79 | concurrency.values().forEach(it -> assertEquals(0, it.intValue())); 80 | } 81 | 82 | assertThrows(NoSuchElementException.class, 83 | () -> aware.supply(Collections.emptyList(), it -> it)); 84 | } 85 | 86 | @Test 87 | void testIllegalStateHandler() { 88 | List all = ImmutableList.of("t1"); 89 | ConcurrencyAware aware = ConcurrencyAware.create(); 90 | int[] called = { 0 }; 91 | aware.addIllegalStateHandler(t -> { 92 | assertEquals(t, "t1"); 93 | called[0]++; 94 | }); 95 | String begin = aware.begin(all); 96 | aware.end(begin); 97 | aware.end(begin); 98 | assertEquals(1, called[0]); 99 | } 100 | 101 | private void checkIdlest(List all, ConcurrencyAware aware) 102 | throws InterruptedException { 103 | AtomicReference current = new AtomicReference<>(); 104 | CountDownLatch c1 = new CountDownLatch(1); 105 | CountDownLatch c2 = new CountDownLatch(1); 106 | new Thread(() -> { 107 | try { 108 | aware.run(all, it -> { 109 | current.set(it); 110 | c1.countDown(); 111 | c2.await(); 112 | }); 113 | } catch (InterruptedException e) { 114 | Thread.currentThread().interrupt(); 115 | } 116 | }).start(); 117 | c1.await(); 118 | assertNotEquals(current.get(), aware.supply(all, it -> it)); 119 | c2.countDown(); 120 | } 121 | 122 | @Test 123 | void testGetConcurrencyCount() { 124 | List all = ImmutableList.of("t1", "t2"); 125 | ConcurrencyAware aware = ConcurrencyAware.create(); 126 | String t1 = aware.beginWithoutRecordConcurrency(all); 127 | assertEquals(1, aware.recordBeginConcurrencyAndGet(t1)); 128 | String t2 = aware.beginWithoutRecordConcurrency(all); 129 | assertEquals(1, aware.recordBeginConcurrencyAndGet(t2)); 130 | String t3 = aware.beginWithoutRecordConcurrency(all); 131 | assertEquals(2, aware.recordBeginConcurrencyAndGet(t3)); 132 | assertEquals(1, aware.endAndGet(t3)); 133 | assertEquals(0, aware.endAndGet(t2)); 134 | assertEquals(0, aware.endAndGet(t1)); 135 | } 136 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/PartitionFailoverBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | import static com.google.common.base.Preconditions.checkNotNull; 5 | 6 | import java.util.Collection; 7 | import java.util.Map; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.function.Consumer; 10 | import java.util.function.Predicate; 11 | 12 | import javax.annotation.CheckReturnValue; 13 | import javax.annotation.Nonnegative; 14 | import javax.annotation.Nonnull; 15 | 16 | import com.github.phantomthief.util.ThrowableFunction; 17 | import com.github.phantomthief.util.ThrowablePredicate; 18 | 19 | /** 20 | * 这是重构过程中途的一个实现,现在建议使用{@link PriorityFailover}。 21 | */ 22 | @SuppressWarnings({"checkstyle:VisibilityModifier", "checkstyle:HiddenField"}) 23 | public class PartitionFailoverBuilder { 24 | 25 | private WeightFailoverBuilder weightFailoverBuilder = new WeightFailoverBuilder<>(); 26 | 27 | int corePartitionSize; 28 | 29 | long maxExternalPoolIdleMillis; 30 | 31 | public static PartitionFailoverBuilder newBuilder() { 32 | return new PartitionFailoverBuilder<>(); 33 | } 34 | 35 | @Nonnull 36 | public PartitionFailover build(Collection original) { 37 | checkNotNull(original); 38 | ensure(original.size()); 39 | WeightFailover weightFailover = weightFailoverBuilder.build(original); 40 | return new PartitionFailover<>(this, weightFailover); 41 | } 42 | 43 | @Nonnull 44 | public PartitionFailover build(Collection original, int initWeight) { 45 | checkNotNull(original); 46 | ensure(original.size()); 47 | WeightFailover weightFailover = weightFailoverBuilder.build(original, initWeight); 48 | return new PartitionFailover<>(this, weightFailover); 49 | } 50 | 51 | @Nonnull 52 | public PartitionFailover build(Map original) { 53 | checkNotNull(original); 54 | ensure(original.size()); 55 | WeightFailover weightFailover = weightFailoverBuilder.build(original); 56 | return new PartitionFailover<>(this, weightFailover); 57 | } 58 | 59 | private void ensure(int allResourceCount) { 60 | checkArgument(corePartitionSize >= 0, "corePartitionSize should not be negative"); 61 | checkArgument(corePartitionSize <= allResourceCount, "corePartitionSize should less or equal than size of original"); 62 | } 63 | 64 | @CheckReturnValue 65 | @Nonnull 66 | public PartitionFailoverBuilder corePartitionSize(int corePartitionSize) { 67 | this.corePartitionSize = corePartitionSize; 68 | return this; 69 | } 70 | 71 | @CheckReturnValue 72 | @Nonnull 73 | public PartitionFailoverBuilder reuseRecentResource(long maxExternalPoolIdleMillis) { 74 | this.maxExternalPoolIdleMillis = maxExternalPoolIdleMillis; 75 | return this; 76 | } 77 | 78 | //-------------------------methods delegate to weightFailoverBuilder below--------------------- 79 | 80 | @CheckReturnValue 81 | @Nonnull 82 | public PartitionFailoverBuilder name(String value) { 83 | weightFailoverBuilder.name(value); 84 | return this; 85 | } 86 | 87 | @CheckReturnValue 88 | @Nonnull 89 | public PartitionFailoverBuilder autoAddOnMissing(int weight) { 90 | weightFailoverBuilder.autoAddOnMissing(weight); 91 | return this; 92 | } 93 | 94 | @CheckReturnValue 95 | @Nonnull 96 | public PartitionFailoverBuilder onMinWeight(Consumer listener) { 97 | weightFailoverBuilder.onMinWeight(listener); 98 | return this; 99 | } 100 | 101 | @CheckReturnValue 102 | @Nonnull 103 | public PartitionFailoverBuilder onRecovered(Consumer listener) { 104 | weightFailoverBuilder.onRecovered(listener); 105 | return this; 106 | } 107 | 108 | @CheckReturnValue 109 | @Nonnull 110 | public PartitionFailoverBuilder minWeight(int value) { 111 | weightFailoverBuilder.minWeight(value); 112 | return this; 113 | } 114 | 115 | @CheckReturnValue 116 | @Nonnull 117 | public PartitionFailoverBuilder failReduceRate(double rate) { 118 | weightFailoverBuilder.failReduceRate(rate); 119 | return this; 120 | } 121 | 122 | @CheckReturnValue 123 | @Nonnull 124 | public PartitionFailoverBuilder failReduce(int weight) { 125 | weightFailoverBuilder.failReduce(weight); 126 | return this; 127 | } 128 | 129 | @CheckReturnValue 130 | @Nonnull 131 | public PartitionFailoverBuilder successIncreaseRate(double rate) { 132 | weightFailoverBuilder.successIncreaseRate(rate); 133 | return this; 134 | } 135 | 136 | @CheckReturnValue 137 | @Nonnull 138 | public PartitionFailoverBuilder successIncrease(int weight) { 139 | weightFailoverBuilder.successIncrease(weight); 140 | return this; 141 | } 142 | 143 | @CheckReturnValue 144 | @Nonnull 145 | public PartitionFailoverBuilder checkDuration(long time, TimeUnit unit) { 146 | weightFailoverBuilder.checkDuration(time, unit); 147 | return this; 148 | } 149 | 150 | @CheckReturnValue 151 | @Nonnull 152 | public PartitionFailoverBuilder filter(@Nonnull Predicate filter) { 153 | weightFailoverBuilder.filter(filter); 154 | return this; 155 | } 156 | 157 | @CheckReturnValue 158 | @Nonnull 159 | public PartitionFailoverBuilder 160 | checker(@Nonnull ThrowableFunction failChecker) { 161 | weightFailoverBuilder.checker(failChecker); 162 | return this; 163 | } 164 | 165 | @CheckReturnValue 166 | @Nonnull 167 | public PartitionFailoverBuilder checker( 168 | @Nonnull ThrowablePredicate failChecker, 169 | @Nonnegative double recoveredInitRate) { 170 | weightFailoverBuilder.checker(failChecker, recoveredInitRate); 171 | return this; 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/util/SharedResourceV2Test.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertNull; 6 | import static org.junit.jupiter.api.Assertions.assertSame; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | import static org.junit.jupiter.api.Assertions.fail; 10 | 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | import java.util.concurrent.ThreadLocalRandom; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.atomic.AtomicBoolean; 16 | import java.util.concurrent.atomic.AtomicLong; 17 | 18 | import org.junit.jupiter.api.Assertions; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import com.github.phantomthief.failover.util.SharedResourceV2.UnregisterFailedException; 22 | import com.google.common.util.concurrent.Uninterruptibles; 23 | 24 | /** 25 | * @author w.vela 26 | * Created on 16/2/19. 27 | */ 28 | class SharedResourceV2Test { 29 | 30 | private static SharedResourceV2 resources = 31 | new SharedResourceV2<>(MockResource::new, MockResource::close); 32 | 33 | @Test 34 | void testParallel() throws Throwable { 35 | ExecutorService executorService = Executors.newFixedThreadPool(200); 36 | AtomicLong failCounter = new AtomicLong(0); 37 | try { 38 | AtomicBoolean stop = new AtomicBoolean(false); 39 | executorService.submit(() -> { 40 | while (!stop.get()) { 41 | try { 42 | boolean needUnregister = false; 43 | String name = "" + ThreadLocalRandom.current().nextInt(0, 100); 44 | MockResource mockResource = resources.get(name); 45 | if (mockResource == null) { 46 | needUnregister = true; 47 | mockResource = resources.register(name); 48 | } 49 | if (mockResource.isShutdown()) { 50 | failCounter.incrementAndGet(); 51 | } 52 | Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(0, 10), 53 | TimeUnit.MICROSECONDS); 54 | if (needUnregister) { 55 | resources.unregister(name); 56 | } 57 | } catch (Throwable t) { 58 | failCounter.incrementAndGet(); 59 | } 60 | } 61 | }); 62 | Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); 63 | stop.set(true); 64 | Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); 65 | executorService.shutdown(); 66 | Assertions.assertTrue(executorService.awaitTermination(1, TimeUnit.SECONDS)); 67 | Assertions.assertEquals(0L, failCounter.get()); 68 | System.out.println("done"); 69 | } finally { 70 | executorService.shutdown(); 71 | } 72 | } 73 | 74 | @Test 75 | void test() { 76 | MockResource mock = resources.register("1"); 77 | assertSame(mock, resources.get("1")); 78 | assertSame(mock, resources.register("1")); 79 | resources.register("2"); 80 | 81 | MockResource mockResource1 = resources.get("1"); 82 | MockResource mockResource2 = resources.get("1"); 83 | assertNotNull(mockResource1); 84 | assertSame(mockResource1, mockResource2); 85 | assertFalse(mockResource1.isShutdown()); 86 | 87 | resources.unregister("1"); 88 | mockResource1 = resources.get("1"); 89 | assertNotNull(mockResource1); 90 | assertFalse(mockResource1.isShutdown()); 91 | 92 | resources.unregister("1"); 93 | assertTrue(mockResource1.isShutdown()); 94 | mockResource1 = resources.get("1"); 95 | assertNull(mockResource1); 96 | } 97 | 98 | @Test 99 | void testUnpairUnregister() { 100 | resources.register("3"); 101 | MockResource mock = resources.get("3"); 102 | assertSame(mock, resources.unregister("3")); 103 | assertThrows(IllegalStateException.class, 104 | () -> resources.unregister("3")); 105 | } 106 | 107 | @Test 108 | void testCleanupFailed() { 109 | SharedResourceV2 resources = 110 | new SharedResourceV2<>(MockResource::new, it -> { 111 | throw new IllegalArgumentException(); 112 | }); 113 | resources.register("4"); 114 | MockResource mock = resources.get("4"); 115 | UnregisterFailedException e = assertThrows(UnregisterFailedException.class, 116 | () -> resources.unregister("4")); 117 | assertSame(mock, e.getRemoved()); 118 | assertSame(IllegalArgumentException.class, e.getCause().getClass()); 119 | } 120 | 121 | @Test 122 | void testRegFail() { 123 | SharedResourceV2 resources = 124 | new SharedResourceV2<>(s -> { 125 | throw new RuntimeException(); 126 | }, MockResource::close); 127 | assertThrows(RuntimeException.class, () -> resources.register("5")); 128 | MockResource mockResource = resources.get("5"); 129 | assertNull(mockResource); 130 | assertThrows(IllegalStateException.class, () -> resources.unregister("5")); 131 | } 132 | 133 | private static class MockResource { 134 | 135 | private String name; 136 | private boolean shutdown = false; 137 | 138 | MockResource(String name) { 139 | this.name = name; 140 | } 141 | 142 | boolean isShutdown() { 143 | return shutdown; 144 | } 145 | 146 | void close() { 147 | if (shutdown) { 148 | fail("failed"); 149 | } 150 | shutdown = true; 151 | System.out.println("shutdown:" + name); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/ComboFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.github.phantomthief.tuple.Tuple.tuple; 4 | import static com.github.phantomthief.util.MoreFunctions.runCatching; 5 | import static com.google.common.base.Preconditions.checkNotNull; 6 | import static com.google.common.collect.Multimaps.toMultimap; 7 | import static java.util.stream.Collectors.toList; 8 | import static java.util.stream.Collectors.toSet; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.Objects; 15 | import java.util.Set; 16 | 17 | import javax.annotation.CheckReturnValue; 18 | import javax.annotation.Nonnull; 19 | import javax.annotation.Nullable; 20 | import javax.annotation.concurrent.NotThreadSafe; 21 | 22 | import com.github.phantomthief.failover.Failover; 23 | import com.github.phantomthief.tuple.TwoTuple; 24 | import com.google.common.collect.HashMultimap; 25 | import com.google.common.collect.Multimap; 26 | 27 | /** 28 | * @author w.vela 29 | * Created on 2017-12-28. 30 | */ 31 | public class ComboFailover implements Failover, Iterable> { 32 | 33 | private final List> failoverList; 34 | private final boolean recheckOnMiss; 35 | 36 | private volatile Multimap> mapByObject; 37 | 38 | private ComboFailover(ComboFailoverBuilder builder) { 39 | this.failoverList = builder.list; 40 | this.recheckOnMiss = builder.recheckOnMiss; 41 | mapByObject = groupByObjects(); 42 | } 43 | 44 | public static ComboFailoverBuilder builder() { 45 | return new ComboFailoverBuilder<>(); 46 | } 47 | 48 | private HashMultimap> groupByObjects() { 49 | return failoverList.stream() 50 | .flatMap(failover -> failover.getAll().stream() 51 | .map(it -> tuple(it, failover))) 52 | .collect(toMultimap(TwoTuple::getFirst, TwoTuple::getSecond, HashMultimap::create)); 53 | } 54 | 55 | @Override 56 | public List getAll() { 57 | return failoverList.stream() 58 | .map(Failover::getAll) 59 | .flatMap(List::stream) 60 | .collect(toList()); 61 | } 62 | 63 | @Nullable 64 | @Override 65 | public T getOneAvailableExclude(Collection exclusions) { 66 | return failoverList.stream() 67 | .map(failover -> failover.getOneAvailableExclude(exclusions)) 68 | .filter(Objects::nonNull) 69 | .findAny() 70 | .orElse(null); 71 | } 72 | 73 | @Override 74 | public List getAvailableExclude(Collection exclusions) { 75 | return failoverList.stream() 76 | .map(failover -> failover.getAvailableExclude(exclusions)) 77 | .flatMap(List::stream) 78 | .collect(toList()); 79 | } 80 | 81 | @Nullable 82 | @Override 83 | public T getOneAvailable() { 84 | return failoverList.stream() 85 | .map(Failover::getOneAvailable) 86 | .filter(Objects::nonNull) 87 | .findAny() 88 | .orElse(null); 89 | } 90 | 91 | @Override 92 | public List getAvailable(int n) { 93 | return failoverList.stream() 94 | .map(failover -> failover.getAvailable(n)) 95 | .flatMap(List::stream) 96 | .limit(n) 97 | .collect(toList()); 98 | } 99 | 100 | @Override 101 | public void fail(@Nonnull T object) { 102 | getByObject(object).forEach(failover -> failover.fail(object)); 103 | } 104 | 105 | @Override 106 | public void down(@Nonnull T object) { 107 | getByObject(object).forEach(failover -> failover.down(object)); 108 | } 109 | 110 | @Override 111 | public List getAvailable() { 112 | return failoverList.stream() 113 | .map(Failover::getAvailable) 114 | .flatMap(List::stream)// 115 | .collect(toList()); 116 | } 117 | 118 | @Override 119 | public Set getFailed() { 120 | return failoverList.stream() 121 | .map(Failover::getFailed) 122 | .flatMap(Set::stream) 123 | .collect(toSet()); 124 | } 125 | 126 | @Override 127 | public void success(@Nonnull T object) { 128 | getByObject(object).forEach(failover -> failover.success(object)); 129 | } 130 | 131 | private Collection> getByObject(T object) { 132 | Collection> list = mapByObject.get(object); 133 | if (recheckOnMiss && list.isEmpty()) { // surely it's wrong. build it again. 134 | mapByObject = groupByObjects(); 135 | list = mapByObject.get(object); 136 | } 137 | return list; 138 | } 139 | 140 | @Override 141 | public Iterator> iterator() { 142 | return failoverList.iterator(); 143 | } 144 | 145 | @Override 146 | public void close() { 147 | failoverList.forEach(failover -> runCatching(failover::close)); 148 | } 149 | 150 | @NotThreadSafe 151 | public static class ComboFailoverBuilder { 152 | 153 | private final List> list = new ArrayList<>(); 154 | private boolean recheckOnMiss; 155 | 156 | private ComboFailoverBuilder() { 157 | } 158 | 159 | @CheckReturnValue 160 | @Nonnull 161 | public ComboFailoverBuilder add(@Nonnull Failover failover) { 162 | list.add(checkNotNull(failover)); 163 | return this; 164 | } 165 | 166 | @CheckReturnValue 167 | @Nonnull 168 | public ComboFailoverBuilder 169 | addAll(@Nonnull Collection> failoverList) { 170 | list.addAll(checkNotNull(failoverList)); 171 | return this; 172 | } 173 | 174 | @CheckReturnValue 175 | @Nonnull 176 | public ComboFailoverBuilder recheckOnMiss(boolean value) { 177 | recheckOnMiss = value; 178 | return this; 179 | } 180 | 181 | @Nonnull 182 | public ComboFailover build() { 183 | return new ComboFailover<>(this); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/FailoverUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | import static com.google.common.base.Predicates.alwaysTrue; 5 | import static com.google.common.base.Throwables.getRootCause; 6 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 7 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 8 | 9 | import java.net.ConnectException; 10 | import java.net.MalformedURLException; 11 | import java.net.NoRouteToHostException; 12 | import java.net.SocketTimeoutException; 13 | import java.net.UnknownHostException; 14 | import java.util.HashSet; 15 | import java.util.Set; 16 | import java.util.function.Predicate; 17 | 18 | import javax.annotation.Nonnegative; 19 | import javax.annotation.Nonnull; 20 | 21 | import com.github.phantomthief.failover.Failover; 22 | import com.github.phantomthief.failover.exception.NoAvailableResourceException; 23 | import com.github.phantomthief.util.ThrowableConsumer; 24 | import com.github.phantomthief.util.ThrowableFunction; 25 | 26 | /** 27 | * @author w.vela 28 | */ 29 | public class FailoverUtils { 30 | 31 | private FailoverUtils() { 32 | throw new UnsupportedOperationException(); 33 | } 34 | 35 | public static R supplyWithRetry(int maxRetryTimes, 36 | long sleepBetweenRetryMs, Failover failover, ThrowableFunction func) 37 | throws X { 38 | return supplyWithRetry(maxRetryTimes, sleepBetweenRetryMs, failover, func, alwaysTrue()); 39 | } 40 | 41 | /** 42 | * @param failChecker {@code true} if need retry, {@code false} means no need retry and mark success 43 | */ 44 | public static R supplyWithRetry(@Nonnegative int maxRetryTimes, 45 | long sleepBetweenRetryMs, Failover failover, ThrowableFunction func, 46 | @Nonnull Predicate failChecker) throws X { 47 | checkArgument(maxRetryTimes > 0); 48 | Set failed = new HashSet<>(); 49 | Throwable lastError = null; 50 | for (int i = 0; i < maxRetryTimes; i++) { 51 | T oneAvailable = failover.getOneAvailableExclude(failed); 52 | if (oneAvailable != null) { 53 | try { 54 | R result = func.apply(oneAvailable); 55 | failover.success(oneAvailable); 56 | return result; 57 | } catch (Throwable e) { 58 | if (failChecker.test(e)) { 59 | failover.fail(oneAvailable); 60 | failed.add(oneAvailable); 61 | if (sleepBetweenRetryMs > 0) { 62 | sleepUninterruptibly(sleepBetweenRetryMs, MILLISECONDS); 63 | } 64 | lastError = e; 65 | continue; 66 | } else { 67 | failover.success(oneAvailable); 68 | throw e; 69 | } 70 | } 71 | } else { 72 | throw new NoAvailableResourceException(); 73 | } 74 | } 75 | //noinspection unchecked 76 | throw (X) lastError; 77 | } 78 | 79 | public static R supply(Failover failover, 80 | ThrowableFunction func, Predicate failChecker) throws X { 81 | T oneAvailable = failover.getOneAvailable(); 82 | if (oneAvailable != null) { 83 | try { 84 | R result = func.apply(oneAvailable); 85 | failover.success(oneAvailable); 86 | return result; 87 | } catch (Throwable e) { 88 | if (failChecker == null || failChecker.test(e)) { 89 | failover.fail(oneAvailable); 90 | } else { 91 | failover.success(oneAvailable); 92 | } 93 | throw e; 94 | } 95 | } else { 96 | throw new NoAvailableResourceException(); 97 | } 98 | } 99 | 100 | public static void runWithRetry(int maxRetryTimes, 101 | long sleepBetweenRetryMs, Failover failover, ThrowableConsumer func) throws X { 102 | supplyWithRetry(maxRetryTimes, sleepBetweenRetryMs, failover, t -> { 103 | func.accept(t); 104 | return null; 105 | }, alwaysTrue()); 106 | } 107 | 108 | /** 109 | * @param failChecker {@code true} if need retry, {@code false} means no need retry and mark success 110 | */ 111 | public static void runWithRetry(int maxRetryTimes, 112 | long sleepBetweenRetryMs, Failover failover, ThrowableConsumer func, 113 | @Nonnull Predicate failChecker) throws X { 114 | supplyWithRetry(maxRetryTimes, sleepBetweenRetryMs, failover, t -> { 115 | func.accept(t); 116 | return null; 117 | }, failChecker); 118 | } 119 | 120 | public static void run(Failover failover, 121 | ThrowableConsumer func, Predicate failChecker) throws X { 122 | supply(failover, t -> { 123 | func.accept(t); 124 | return null; 125 | }, failChecker); 126 | } 127 | 128 | public static boolean isHostUnavailable(Throwable t) { 129 | Throwable rootCause = getRootCause(t); 130 | if (rootCause instanceof NoRouteToHostException) { 131 | return true; 132 | } 133 | if (rootCause instanceof UnknownHostException) { 134 | return false; 135 | } 136 | if (rootCause instanceof MalformedURLException) { 137 | return false; 138 | } 139 | if (rootCause instanceof ConnectException) { 140 | if (rootCause.getMessage() != null 141 | && rootCause.getMessage().toLowerCase().contains("connection refused")) { 142 | return true; 143 | } 144 | } 145 | if (rootCause instanceof SocketTimeoutException) { 146 | if (rootCause.getMessage() != null 147 | && rootCause.getMessage().toLowerCase().contains("connect timed out")) { 148 | return true; 149 | } 150 | } 151 | return false; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/RecoverableCheckFailover.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.github.phantomthief.failover.util.SharedCheckExecutorHolder.getInstance; 4 | import static com.github.phantomthief.util.MoreSuppliers.lazy; 5 | import static com.google.common.collect.EvictingQueue.create; 6 | import static java.lang.System.currentTimeMillis; 7 | import static java.util.Collections.emptySet; 8 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 9 | import static java.util.stream.Collectors.toList; 10 | import static org.slf4j.LoggerFactory.getLogger; 11 | 12 | import java.io.Closeable; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.Set; 16 | import java.util.concurrent.CopyOnWriteArraySet; 17 | import java.util.concurrent.ScheduledFuture; 18 | import java.util.function.Predicate; 19 | 20 | import org.slf4j.Logger; 21 | 22 | import com.github.phantomthief.failover.Failover; 23 | import com.github.phantomthief.util.MoreSuppliers.CloseableSupplier; 24 | import com.google.common.cache.CacheBuilder; 25 | import com.google.common.cache.CacheLoader; 26 | import com.google.common.cache.LoadingCache; 27 | import com.google.common.collect.EvictingQueue; 28 | 29 | /** 30 | * 一个简易的failover/failback策略类 31 | * failover条件是一段时间内出错次数超过一个阈值 32 | * failback策略是定期检查可用 33 | * 34 | * @author w.vela 35 | */ 36 | @Deprecated 37 | public class RecoverableCheckFailover implements Failover, Closeable { 38 | 39 | private static final Logger logger = getLogger(RecoverableCheckFailover.class); 40 | private final List original; 41 | private final long failDuration; 42 | private final Set failedList = new CopyOnWriteArraySet<>(); 43 | private final LoadingCache> failCountMap; 44 | private final boolean returnOriginalWhileAllFailed; 45 | private final CloseableSupplier> recoveryFuture; 46 | 47 | private volatile boolean closed; 48 | 49 | RecoverableCheckFailover(List original, Predicate checker, int failCount, 50 | long failDuration, long recoveryCheckDuration, boolean returnOriginalWhileAllFailed) { 51 | this.returnOriginalWhileAllFailed = returnOriginalWhileAllFailed; 52 | this.original = original; 53 | this.failDuration = failDuration; 54 | this.failCountMap = CacheBuilder.newBuilder().weakKeys() 55 | .build(new CacheLoader>() { 56 | 57 | @Override 58 | public EvictingQueue load(T key) { 59 | return create(failCount); 60 | } 61 | }); 62 | recoveryFuture = lazy(() -> getInstance().scheduleWithFixedDelay(() -> { 63 | if (closed) { 64 | tryCloseScheduler(); 65 | return; 66 | } 67 | if (failedList.isEmpty()) { 68 | return; 69 | } 70 | try { 71 | // 考虑到COWArraySet不支持iterator.remove,所以这里使用搜集->统一清理的策略 72 | List covered = failedList.stream() 73 | .filter(checker) 74 | .peek(obj -> logger.info("obj:{} is recovered during test.", obj)) 75 | .collect(toList()); 76 | failedList.removeAll(covered); 77 | } catch (Throwable e) { 78 | logger.error("Ops.", e); 79 | } 80 | }, recoveryCheckDuration, recoveryCheckDuration, MILLISECONDS)); 81 | } 82 | 83 | public static RecoverableCheckFailoverBuilder newBuilder() { 84 | return new RecoverableCheckFailoverBuilder<>(); 85 | } 86 | 87 | public static GenericRecoverableCheckFailoverBuilder newGenericBuilder() { 88 | return new GenericRecoverableCheckFailoverBuilder<>(newBuilder()); 89 | } 90 | 91 | @Override 92 | public void fail(T object) { 93 | if (!getAll().contains(object)) { 94 | logger.warn("invalid fail obj:{}, it's not in original list.", object); 95 | return; 96 | } 97 | logger.warn("server {} failed.", object); 98 | boolean addToFail = false; 99 | EvictingQueue evictingQueue = failCountMap.getUnchecked(object); 100 | synchronized (evictingQueue) { 101 | evictingQueue.add(currentTimeMillis()); 102 | if (evictingQueue.remainingCapacity() == 0 103 | && evictingQueue.element() >= currentTimeMillis() - failDuration) { 104 | addToFail = true; 105 | } 106 | } 107 | if (addToFail) { 108 | failedList.add(object); 109 | } 110 | recoveryFuture.get(); 111 | } 112 | 113 | @Override 114 | public void down(T object) { 115 | if (!getAll().contains(object)) { 116 | logger.warn("invalid fail obj:{}, it's not in original list.", object); 117 | return; 118 | } 119 | logger.warn("server {} down.", object); 120 | failedList.add(object); 121 | recoveryFuture.get(); 122 | } 123 | 124 | @Override 125 | public List getAvailable() { 126 | return getAvailableExclude(emptySet()); 127 | } 128 | 129 | @Override 130 | public List getAvailableExclude(Collection exclusions) { 131 | List availables = original.stream() 132 | .filter(obj -> !getFailed().contains(obj)) 133 | .filter(obj -> !exclusions.contains(obj)) 134 | .collect(toList()); 135 | if (availables.isEmpty() && returnOriginalWhileAllFailed) { 136 | return original; 137 | } else { 138 | return availables; 139 | } 140 | } 141 | 142 | @Override 143 | public Set getFailed() { 144 | return failedList; 145 | } 146 | 147 | @Override 148 | public List getAll() { 149 | return original; 150 | } 151 | 152 | public synchronized void close() { 153 | closed = true; 154 | tryCloseScheduler(); 155 | } 156 | 157 | private void tryCloseScheduler() { 158 | recoveryFuture.ifPresent(future -> { 159 | if (!future.isCancelled()) { 160 | if (!future.cancel(true)) { 161 | logger.warn("fail to close failover:{}", this); 162 | } 163 | } 164 | }); 165 | } 166 | 167 | @Override 168 | public String toString() { 169 | return "RecoverableCheckFailover [" + original + "]"; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/WeightFailoverCheckTask.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.github.phantomthief.util.MoreSuppliers.lazy; 4 | import static com.google.common.primitives.Ints.constrainToRange; 5 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 6 | import static java.util.concurrent.TimeUnit.SECONDS; 7 | 8 | import java.lang.ref.PhantomReference; 9 | import java.lang.ref.ReferenceQueue; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.concurrent.ConcurrentMap; 13 | import java.util.concurrent.ScheduledFuture; 14 | import java.util.concurrent.atomic.AtomicBoolean; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | import java.util.function.Supplier; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import com.github.phantomthief.failover.util.SharedCheckExecutorHolder; 22 | import com.github.phantomthief.util.MoreSuppliers.CloseableSupplier; 23 | 24 | /** 25 | * 26 | * @author w.vela 27 | * @author huangli 28 | */ 29 | 30 | class WeightFailoverCheckTask { 31 | 32 | private static final Logger logger = LoggerFactory.getLogger(WeightFailoverCheckTask.class); 33 | 34 | static final int CLEAN_INIT_DELAY_SECONDS = 5; 35 | static final int CLEAN_DELAY_SECONDS = 10; 36 | 37 | static { 38 | SharedCheckExecutorHolder.getInstance().scheduleWithFixedDelay(WeightFailoverCheckTask::doClean, 39 | CLEAN_INIT_DELAY_SECONDS, CLEAN_DELAY_SECONDS, SECONDS); 40 | } 41 | 42 | private final WeightFailoverBuilder builder; 43 | private final AtomicBoolean closed; 44 | private final ConcurrentMap initWeightMap; 45 | private final ConcurrentMap currentWeightMap; 46 | private final AtomicInteger allAvailableVersion; 47 | 48 | private final CloseableSupplier> recoveryFuture; 49 | 50 | // we need keep reference of this object 51 | private final MyPhantomReference phantomReference; 52 | 53 | private static final ReferenceQueue> REF_QUEUE = new ReferenceQueue<>(); 54 | 55 | private static class MyPhantomReference extends PhantomReference> { 56 | private CloseableSupplier> recoveryFuture; 57 | private AtomicBoolean closed; 58 | private String failoverName; 59 | public MyPhantomReference(WeightFailover referent, ReferenceQueue> q, 60 | CloseableSupplier> recoveryFuture, AtomicBoolean closed) { 61 | super(referent, q); 62 | this.recoveryFuture = recoveryFuture; 63 | this.closed = closed; 64 | this.failoverName = referent.toString(); 65 | } 66 | 67 | private void close() { 68 | if (!closed.get()) { 69 | logger.warn("failover not released manually: {}", failoverName); 70 | closed.set(true); 71 | WeightFailover.tryCloseRecoveryScheduler(recoveryFuture, () -> failoverName); 72 | } 73 | } 74 | } 75 | 76 | // lambda会隐式持有this,所以弄一个静态类 77 | static class RecoveryFutureSupplier implements Supplier> { 78 | private Runnable runnable; 79 | private final long initDelay; 80 | private final long delay; 81 | 82 | public RecoveryFutureSupplier(Runnable runnable, long initDelay, long delay) { 83 | this.runnable = runnable; 84 | this.initDelay = initDelay; 85 | this.delay = delay; 86 | } 87 | 88 | @Override 89 | public ScheduledFuture get() { 90 | ScheduledFuture f = SharedCheckExecutorHolder.getInstance().scheduleWithFixedDelay( 91 | runnable, initDelay, delay, MILLISECONDS); 92 | runnable = null; 93 | return f; 94 | } 95 | } 96 | 97 | WeightFailoverCheckTask(WeightFailover failover, WeightFailoverBuilder builder, AtomicBoolean closed, 98 | ConcurrentMap initWeightMap, ConcurrentMap currentWeightMap, 99 | AtomicInteger allAvailableVersion) { 100 | this.builder = builder; 101 | this.closed = closed; 102 | this.initWeightMap = initWeightMap; 103 | this.currentWeightMap = currentWeightMap; 104 | this.allAvailableVersion = allAvailableVersion; 105 | this.recoveryFuture = lazy(new RecoveryFutureSupplier(this::run, builder.checkDuration, builder.checkDuration)); 106 | 107 | phantomReference = new MyPhantomReference<>(failover, REF_QUEUE, recoveryFuture, closed); 108 | } 109 | 110 | private static void doClean() { 111 | MyPhantomReference ref = (MyPhantomReference) REF_QUEUE.poll(); 112 | while (ref != null) { 113 | ref.close(); 114 | ref = (MyPhantomReference) REF_QUEUE.poll(); 115 | } 116 | } 117 | 118 | public CloseableSupplier> lazyFuture() { 119 | return recoveryFuture; 120 | } 121 | 122 | private void run() { 123 | if (closed.get()) { 124 | return; 125 | } 126 | Thread currentThread = Thread.currentThread(); 127 | String origName = currentThread.getName(); 128 | if (builder.name != null) { 129 | currentThread.setName(origName + "-[" + builder.name + "]"); 130 | } 131 | try { 132 | Map recoveredObjects = new HashMap<>(); 133 | this.currentWeightMap.forEach((obj, weight) -> { 134 | if (weight == 0) { 135 | double recoverRate = builder.checker.applyAsDouble(obj); 136 | if (recoverRate > 0) { 137 | recoveredObjects.put(obj, recoverRate); 138 | } 139 | } 140 | }); 141 | if (!recoveredObjects.isEmpty()) { 142 | logger.info("found recovered objects:{}", recoveredObjects); 143 | } 144 | recoveredObjects.forEach((recovered, rate) -> { 145 | Integer initWeight = initWeightMap.get(recovered); 146 | if (initWeight == null) { 147 | throw new IllegalStateException("obj:" + recovered); 148 | } 149 | int recoveredWeight = constrainToRange((int) (initWeight * rate), 1, 150 | initWeight); 151 | currentWeightMap.put(recovered, recoveredWeight); 152 | allAvailableVersion.incrementAndGet(); 153 | if (builder.onRecovered != null) { 154 | builder.onRecovered.accept(recovered); 155 | } 156 | }); 157 | } catch (Throwable e) { 158 | logger.error("", e); 159 | } finally { 160 | currentThread.setName(origName); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/ConcurrencyAware.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.github.phantomthief.failover.util.RandomListUtils.getRandom; 4 | import static com.google.common.base.Preconditions.checkNotNull; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.NoSuchElementException; 10 | import java.util.RandomAccess; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | 13 | import javax.annotation.Nonnull; 14 | import javax.annotation.Nullable; 15 | 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import com.github.phantomthief.util.ThrowableConsumer; 20 | import com.github.phantomthief.util.ThrowableFunction; 21 | 22 | /** 23 | * @author w.vela 24 | * Created on 2018-01-22. 25 | */ 26 | public class ConcurrencyAware { 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(ConcurrencyAware.class); 29 | 30 | private static final int OPTIMIZE_RANDOM_TRIES = 2; 31 | 32 | private final Map concurrency = new ConcurrentHashMap<>(); 33 | private final List> illegalStateHandlers = new ArrayList<>(); 34 | 35 | private ConcurrencyAware() { 36 | } 37 | 38 | public static ConcurrencyAware create() { 39 | return new ConcurrencyAware<>(); 40 | } 41 | 42 | @Nullable 43 | private T selectIdlest(@Nonnull Iterable candidates) { 44 | checkNotNull(candidates); 45 | if (candidates instanceof List && candidates instanceof RandomAccess) { 46 | List candidatesCol = (List) candidates; 47 | T t = selectIdlestFast(candidatesCol); 48 | if (t != null) { 49 | return t; 50 | } 51 | } 52 | 53 | // find objects with minimum concurrency 54 | List idlest = new ArrayList<>(); 55 | int minValue = Integer.MAX_VALUE; 56 | for (T obj : candidates) { 57 | int c = concurrency.getOrDefault(obj, 0); 58 | if (c < minValue) { 59 | minValue = c; 60 | idlest.clear(); 61 | idlest.add(obj); 62 | } else if (c == minValue) { 63 | idlest.add(obj); 64 | } 65 | } 66 | T result = getRandom(idlest); 67 | assert result != null; 68 | return result; 69 | } 70 | 71 | /** 72 | * Try to find a node with 0 concurrency by pure random. 73 | * It is assuming that concurrency is very low for most cases. 74 | * This method will optimize performance significantly on large candidates collection, 75 | * because it is no need to build a large ListMultimap of TreeMap. 76 | */ 77 | @Nullable 78 | private T selectIdlestFast(List candidates) { 79 | if (candidates.isEmpty()) { 80 | throw new NoSuchElementException("candidates list is empty"); 81 | } else if (candidates.size() == 1) { 82 | return candidates.get(0); 83 | } 84 | for (int i = 0; i < OPTIMIZE_RANDOM_TRIES; ++i) { 85 | T result = getRandom(candidates); 86 | int objConcurrency = concurrency.getOrDefault(result, 0); 87 | if (objConcurrency <= 0) { 88 | return result; 89 | } 90 | } 91 | return null; 92 | } 93 | 94 | /** 95 | * @throws X, or {@link NoSuchElementException} if candidates is empty 96 | */ 97 | public void run(@Nonnull Iterable candidates, 98 | @Nonnull ThrowableConsumer func) throws X { 99 | checkNotNull(func); 100 | supply(candidates, it -> { 101 | func.accept(it); 102 | return null; 103 | }); 104 | } 105 | 106 | /** 107 | * @throws X, or {@link NoSuchElementException} if candidates is empty 108 | */ 109 | public E supply(@Nonnull Iterable candidates, 110 | @Nonnull ThrowableFunction func) throws X { 111 | checkNotNull(func); 112 | T obj = begin(candidates); 113 | try { 114 | return func.apply(obj); 115 | } finally { 116 | end(obj); 117 | } 118 | } 119 | 120 | /** 121 | * better use {@link #supply} or {@link #run} unless need to control begin and end in special situations. 122 | * @throws NoSuchElementException if candidates is empty 123 | */ 124 | @Nonnull 125 | public T begin(@Nonnull Iterable candidates) { 126 | T obj = beginWithoutRecordConcurrency(candidates); 127 | recordBeginConcurrency(obj); 128 | return obj; 129 | } 130 | 131 | /** 132 | * this is a low level api, for special purpose or mock. 133 | */ 134 | @Nonnull 135 | public T beginWithoutRecordConcurrency(@Nonnull Iterable candidates) { 136 | T obj = selectIdlest(candidates); 137 | if (obj == null) { 138 | throw new NoSuchElementException(); 139 | } 140 | return obj; 141 | } 142 | 143 | /** 144 | * this is a low level api, for special purpose or mock. 145 | * 使用 {@link ConcurrencyAware#recordBeginConcurrencyAndGet(Object)} 146 | */ 147 | @Deprecated 148 | public void recordBeginConcurrency(@Nonnull T obj) { 149 | recordBeginConcurrencyAndGet(obj); 150 | } 151 | 152 | /** 153 | * 增加并发计数,并返回当前的并发数 154 | */ 155 | public int recordBeginConcurrencyAndGet(@Nonnull T obj) { 156 | return concurrency.merge(obj, 1, Integer::sum); 157 | } 158 | 159 | /** 160 | * @param obj from {@link #begin}'s return 161 | * @see #begin 162 | * 使用 {@link ConcurrencyAware#endAndGet(Object)} 163 | */ 164 | @Deprecated 165 | public void end(@Nonnull T obj) { 166 | endAndGet(obj); 167 | } 168 | 169 | public int endAndGet(@Nonnull T obj) { 170 | Integer concurrentNum = concurrency.compute(obj, (thisKey, oldValue) -> { 171 | if (oldValue == null) { 172 | logger.warn("illegal state found, obj:{}", thisKey); 173 | for (ThrowableConsumer handler : illegalStateHandlers) { 174 | try { 175 | handler.accept(thisKey); 176 | } catch (Throwable e) { 177 | logger.error("", e); 178 | } 179 | } 180 | return null; 181 | } 182 | int result = oldValue - 1; 183 | if (result == 0) { 184 | return null; 185 | } else { 186 | return result; 187 | } 188 | }); 189 | return concurrentNum == null ? 0 : concurrentNum; 190 | } 191 | 192 | public ConcurrencyAware 193 | addIllegalStateHandler(@Nonnull ThrowableConsumer handler) { 194 | illegalStateHandlers.add(checkNotNull(handler)); 195 | return this; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/PriorityFailoverManager.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import java.util.HashMap; 4 | import java.util.Iterator; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | import java.util.Set; 8 | 9 | import javax.annotation.Nonnull; 10 | import javax.annotation.Nullable; 11 | import javax.annotation.concurrent.NotThreadSafe; 12 | 13 | import com.github.phantomthief.failover.impl.PriorityFailoverBuilder.PriorityFailoverConfig; 14 | import com.github.phantomthief.failover.impl.PriorityFailoverBuilder.ResConfig; 15 | 16 | /** 17 | * 为了提升性能,PriorityFailover是个"不可变"对象,构造以后,资源列表、每个资源的配置(比如最大权重)就不能变了, 18 | * 如果需要构建后持续变更(比如资源上下线),并且保留以前资源的当前权重等信息,就需要用到这个类。 19 | * 20 | * 这个类的update/updateAll方法不是线程安全的,使用的时候,请自行加锁。 21 | * 22 | * @author huangli 23 | * Created on 2020-01-23 24 | */ 25 | @NotThreadSafe 26 | public class PriorityFailoverManager { 27 | private volatile PriorityFailover failover; 28 | 29 | @Nullable 30 | private final PriorityGroupManager groupManager; 31 | 32 | /** 33 | * 手工构造一个PriorityFailoverManager,用户通常不需要调用这个方法来构造,而是使用{@link PriorityFailoverBuilder#buildManager()}。 34 | * @param failover failover 35 | * @param groupManager 自动分组管理器,如果不需要自动分组管理,可以为null 36 | * @see PriorityFailoverBuilder#buildManager() 37 | */ 38 | public PriorityFailoverManager(PriorityFailover failover, @Nullable PriorityGroupManager groupManager) { 39 | setFailover(failover); 40 | this.groupManager = groupManager; 41 | } 42 | 43 | 44 | /** 45 | * 获取本类管理的failover,每次返回的对象可能是不同的(比如调用update方法更新了)。 46 | * 虽然本类不是线程安全的,但是本方法多线程调用时不必加锁。 47 | * @return 受管的failover 48 | */ 49 | public PriorityFailover getFailover() { 50 | return failover; 51 | } 52 | 53 | protected void setFailover(PriorityFailover failover) { 54 | this.failover = failover; 55 | } 56 | 57 | /** 58 | * 增量add/update/remove资源。 59 | * 如果一个被更新的资源的最大权重被改变,当前权重会按百分比保留(在不溢出的情况下),举例来说,如果一个资源原来的最大权重是100,当前权重90, 60 | * 更新后最大权重如果设定为50,那么当前权重自动设置为45。 61 | * @param resNeedToAddOrUpdate 需要添加或者更新的资源,可以为null,如果为null就代表不需要添加和更新资源 62 | * @param resNeedToRemove 需要删除的资源,可以为null,如果为null就代表不需要删除资源 63 | */ 64 | public UpdateResult update(@Nullable Map resNeedToAddOrUpdate, @Nullable Set resNeedToRemove) { 65 | UpdateResult result = new UpdateResult<>(); 66 | PriorityFailoverConfig oldConfigCopy = failover.getConfig().clone(); 67 | if (groupManager != null) { 68 | groupManager.update(resNeedToAddOrUpdate == null ? null : resNeedToAddOrUpdate.keySet(), 69 | resNeedToRemove); 70 | } 71 | if (resNeedToAddOrUpdate != null) { 72 | processAddAndUpdate(resNeedToAddOrUpdate, oldConfigCopy, result); 73 | } 74 | if (resNeedToRemove != null) { 75 | resNeedToRemove.forEach(res -> { 76 | ResConfig config = oldConfigCopy.getResources().remove(res); 77 | result.getRemovedResources().put(res, config.clone()); 78 | }); 79 | } 80 | failover.close(); 81 | setFailover(new PriorityFailover<>(oldConfigCopy)); 82 | return result; 83 | } 84 | 85 | private void processAddAndUpdate(@Nonnull Map resNeedToAddOrUpdate, 86 | PriorityFailoverConfig oldConfigCopy, UpdateResult result) { 87 | HashMap> currentDataMap = failover.getResourcesMap(); 88 | Map initResConfigCopy = oldConfigCopy.getResources(); 89 | resNeedToAddOrUpdate.forEach((res, newConfig) -> { 90 | PriorityFailover.ResInfo resInfo = currentDataMap.get(res); 91 | double initWeight; 92 | if (resInfo != null) { 93 | double current = resInfo.currentWeight; 94 | if (current != resInfo.maxWeight) { 95 | initWeight = current / resInfo.maxWeight * newConfig.getMaxWeight(); 96 | } else { 97 | initWeight = newConfig.getMaxWeight(); 98 | } 99 | initWeight = Math.min(initWeight, newConfig.getMaxWeight()); 100 | initWeight = Math.max(initWeight, newConfig.getMinWeight()); 101 | } else { 102 | initWeight = newConfig.getInitWeight(); 103 | } 104 | int pri = groupManager == null ? newConfig.getPriority() : groupManager.getPriority(res); 105 | newConfig = new ResConfig(newConfig.getMaxWeight(), 106 | newConfig.getMinWeight(), pri, initWeight); 107 | PriorityFailoverBuilder.checkResConfig(newConfig); 108 | if (initResConfigCopy.containsKey(res)) { 109 | result.getUpdatedResources().put(res, newConfig.clone()); 110 | } else { 111 | result.getAddedResources().put(res, newConfig.clone()); 112 | } 113 | initResConfigCopy.put(res, newConfig); 114 | }); 115 | } 116 | 117 | /** 118 | * 全量更新资源,已有的资源,如果不在新的列表中的资源会被删除,如果在新的列表中的资源会被更新。 119 | * 如果一个被更新的资源的最大权重被改变,当前权重会按百分比保留(在不溢出的情况下),举例来说,如果一个资源原来的最大权重是100,当前权重90, 120 | * 更新后最大权重如果设定为50,那么当前权重自动设置为45。 121 | * @param newResourceConfigs 新的资源列表 122 | */ 123 | public UpdateResult updateAll(@Nonnull Map newResourceConfigs) { 124 | UpdateResult result = new UpdateResult<>(); 125 | PriorityFailoverConfig oldConfigCopy = failover.getConfig().clone(); 126 | if (groupManager != null) { 127 | groupManager.updateAll(newResourceConfigs.keySet()); 128 | } 129 | 130 | processAddAndUpdate(newResourceConfigs, oldConfigCopy, result); 131 | 132 | Iterator> iterator = oldConfigCopy.getResources().entrySet().iterator(); 133 | while (iterator.hasNext()) { 134 | Entry en = iterator.next(); 135 | if (!newResourceConfigs.containsKey(en.getKey())) { 136 | iterator.remove(); 137 | result.getRemovedResources().put(en.getKey(), en.getValue()); 138 | } 139 | } 140 | 141 | failover.close(); 142 | setFailover(new PriorityFailover<>(oldConfigCopy)); 143 | return result; 144 | } 145 | 146 | @Nullable 147 | PriorityGroupManager getGroupManager() { 148 | return groupManager; 149 | } 150 | 151 | /** 152 | * 更新的结果 153 | * @param 类型参数 154 | */ 155 | public static class UpdateResult { 156 | private Map addedResources = new HashMap<>(); 157 | private Map removedResources = new HashMap<>(); 158 | private Map updatedResources = new HashMap<>(); 159 | 160 | /** 161 | * 本次添加的资源,Map的value是新的配置。 162 | * @return 更新的资源 163 | */ 164 | public Map getAddedResources() { 165 | return addedResources; 166 | } 167 | 168 | /** 169 | * 删除的资源,Map的value是旧的配置。 170 | * @return 更新的资源 171 | */ 172 | public Map getRemovedResources() { 173 | return removedResources; 174 | } 175 | 176 | /** 177 | * 本次更新的资源,Map的value是新的配置。 178 | * @return 更新的资源 179 | */ 180 | public Map getUpdatedResources() { 181 | return updatedResources; 182 | } 183 | 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/impl/PriorityGroupManager.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static java.util.stream.Collectors.toMap; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Map.Entry; 12 | import java.util.Objects; 13 | import java.util.Random; 14 | import java.util.Set; 15 | import java.util.stream.Collectors; 16 | 17 | import javax.annotation.Nonnull; 18 | import javax.annotation.Nullable; 19 | import javax.annotation.concurrent.NotThreadSafe; 20 | 21 | /** 22 | * 自动分组管理器,自动将资源分成若干组,第一组的priority是0,第二组是1,依次类推。 23 | * 同时,提供全量和增量变更管理的功能。 24 | * 25 | *

26 | * 举个例子,假设update后新的资源列表共100个,分两个组,分别有5个和95个资源。 27 | *

  • 每个加入的资源有均等的机会进入第一组,几率为5%,这样可以避免新增的资源(服务器)没有流量,并且新增的资源和已有的资源流量应该是对等的; 28 | *
  • 对于保留的资源来说,如果一个资源A之前进入了前一组,而B没有,那么A仍然优先,不会出现B进了第一组而A没有进的情况 29 | * (B有可能因为旧资源删除而晋升),这样可以尽量保持主调方的粘性,有利于连接复用和被调用方的缓存等。 30 | *

    31 | * 32 | * @author huangli 33 | * Created on 2020-02-03 34 | */ 35 | @NotThreadSafe 36 | public class PriorityGroupManager { 37 | 38 | private final int sumOfCoreGroupSize; 39 | private final int[] coreGroupSizes; 40 | 41 | // contains resources from group 0 to group N-2, ordered 42 | private volatile ArrayList coreResources; 43 | 44 | // contains resources of group N-1 (the last group), has no order 45 | private volatile ArrayList restResources; 46 | 47 | // res -> [int priority, int logicIndex] 48 | private volatile HashMap resMap; 49 | 50 | /** 51 | * 构造自动分组管理器。 52 | * @param initResources 初始资源列表 53 | * @param coreGroupSizes 你对每组资源数量的要求,只填N-1个组,比如想分3组,第一组5个,第二组20个,剩下是第三组,那么这个参数传入[5, 10] 54 | */ 55 | public PriorityGroupManager(@Nonnull Set initResources, int... coreGroupSizes) { 56 | Objects.requireNonNull(initResources); 57 | Objects.requireNonNull(coreGroupSizes); 58 | this.coreGroupSizes = coreGroupSizes.clone(); 59 | int sum = 0; 60 | for (int size : coreGroupSizes) { 61 | if (size < 0) { 62 | throw new IllegalArgumentException("illegal coreGroupSizes:" + size); 63 | } 64 | sum += size; 65 | } 66 | this.sumOfCoreGroupSize = sum; 67 | ArrayList totalResources = new ArrayList<>(initResources); 68 | Collections.shuffle(totalResources); 69 | 70 | this.coreResources = new ArrayList<>(totalResources.subList(0, 71 | Math.min(sumOfCoreGroupSize, totalResources.size()))); 72 | if (totalResources.size() > sumOfCoreGroupSize) { 73 | this.restResources = new ArrayList<>(totalResources.subList(sumOfCoreGroupSize, totalResources.size())); 74 | } else { 75 | this.restResources = new ArrayList<>(); 76 | } 77 | resMap = buildResMap(coreResources, restResources, coreGroupSizes); 78 | } 79 | 80 | static HashMap buildResMap(List coreResources, List restResources, 81 | int[] coreGroupSizes) { 82 | final int totalGroupCount = coreGroupSizes.length + 1; 83 | HashMap map = new HashMap<>(); 84 | int logicIndex = 0; 85 | int priority = 0; 86 | int groupIndex = 0; 87 | for (T res : coreResources) { 88 | while (groupIndex >= coreGroupSizes[priority]) { 89 | priority++; 90 | groupIndex = 0; 91 | } 92 | map.put(res, new int[] {priority, logicIndex}); 93 | groupIndex++; 94 | logicIndex++; 95 | } 96 | for (T res : restResources) { 97 | map.put(res, new int[] {totalGroupCount - 1, logicIndex}); 98 | logicIndex++; 99 | } 100 | return map; 101 | } 102 | 103 | /** 104 | * 获取资源优先级map。 105 | * @return map里面的key是资源,value是优先级(0 based) 106 | */ 107 | public Map getPriorityMap() { 108 | return resMap.entrySet().stream() 109 | .collect(toMap(Entry::getKey, e -> e.getValue()[0])); 110 | } 111 | 112 | /** 113 | * 获取某个资源的优先级 114 | * @param res 资源 115 | * @return 优先级(0 based) 116 | */ 117 | public int getPriority(T res) { 118 | return resMap.get(res)[0]; 119 | } 120 | 121 | /** 122 | * 全量更新,会自动计算出需要增加的、删除的、保留的资源,以前的分组数据会按几率分布保留。 123 | * 124 | *

    125 | * 举个例子,假设update后新的资源列表共100个,分两个组,分别有5个和95个资源。 126 | *

  • 每个加入的资源有均等的机会进入第一组,几率为5%,这样可以避免新增的资源(服务器)没有流量,并且新增的资源和已有的资源流量应该是对等的; 127 | *
  • 对于保留的资源来说,如果一个资源A之前进入了前一组,而B没有,那么A仍然优先,不会出现B进了第一组而A没有进的情况 128 | * (B有可能因为旧资源删除而晋升),这样可以尽量保持主调方的粘性,有利于连接复用和被调用方的缓存等。 129 | *

    130 | * @param resources 新的资源列表 131 | */ 132 | public void updateAll(@Nonnull Set resources) { 133 | Objects.requireNonNull(resources); 134 | Set resNeedToAdd = resources.stream() 135 | .filter(res -> !resMap.containsKey(res)) 136 | .collect(Collectors.toSet()); 137 | Set resNeedToRemove = resMap.keySet().stream() 138 | .filter(res -> !resources.contains(res)) 139 | .collect(Collectors.toSet()); 140 | update0(resNeedToAdd, resNeedToRemove); 141 | } 142 | 143 | /** 144 | * 增量更新,会自动计算出需要增加的、删除的、保留的资源,以前的分组数据会按几率分布保留。 145 | * 146 | *

    147 | * 举个例子,假设update后新的资源列表共100个,分两个组,分别有5个和95个资源。 148 | *

  • 每个加入的资源有均等的机会进入第一组,几率为5%,这样可以避免新增的资源(服务器)没有流量,并且新增的资源和已有的资源流量应该是对等的; 149 | *
  • 对于保留的资源来说,如果一个资源A之前进入了前一组,而B没有,那么A仍然优先,不会出现B进了第一组而A没有进的情况 150 | * (B有可能因为旧资源删除而晋升),这样可以尽量保持主调方的粘性,有利于连接复用和被调用方的缓存等。 151 | *

    152 | * @param resNeedToAdd 需要添加的 153 | * @param resNeedToRemove 需要删除的 154 | */ 155 | public void update(@Nullable Set resNeedToAdd, @Nullable Set resNeedToRemove) { 156 | if (resNeedToAdd != null) { 157 | resNeedToAdd = new HashSet<>(resNeedToAdd); 158 | resNeedToAdd.removeAll(resMap.keySet()); 159 | if (resNeedToRemove != null) { 160 | // 以免两个set之间有重叠 161 | resNeedToAdd.removeAll(resNeedToRemove); 162 | } 163 | } 164 | update0(resNeedToAdd, resNeedToRemove); 165 | } 166 | 167 | @SuppressWarnings("checkstyle:HiddenField") 168 | private void update0(@Nullable Set resNeedToAdd, @Nullable Set resNeedToRemove) { 169 | ArrayList coreResources = new ArrayList<>(this.coreResources); 170 | ArrayList restResources = new ArrayList<>(this.restResources); 171 | if (resNeedToRemove != null) { 172 | coreResources.removeAll(resNeedToRemove); 173 | restResources.removeAll(resNeedToRemove); 174 | } 175 | Random r = new Random(); 176 | while (coreResources.size() < sumOfCoreGroupSize && restResources.size() > 0) { 177 | int index = r.nextInt(restResources.size()); 178 | coreResources.add(restResources.remove(index)); 179 | } 180 | if (resNeedToAdd != null) { 181 | for (T res : resNeedToAdd) { 182 | int currentCount = coreResources.size() + restResources.size(); 183 | int index = r.nextInt(currentCount + 1); 184 | if (index < sumOfCoreGroupSize) { 185 | coreResources.add(index, res); 186 | } else { 187 | restResources.add(res); 188 | } 189 | if (coreResources.size() > sumOfCoreGroupSize) { 190 | // move last one 191 | restResources.add(coreResources.remove(coreResources.size() - 1)); 192 | } 193 | } 194 | } 195 | resMap = buildResMap(coreResources, restResources, coreGroupSizes); 196 | this.coreResources = coreResources; 197 | this.restResources = restResources; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/failover/util/SharedResourceV2.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.util; 2 | 3 | import static com.github.phantomthief.tuple.Tuple.tuple; 4 | import static com.github.phantomthief.util.MoreSuppliers.lazy; 5 | import static org.slf4j.LoggerFactory.getLogger; 6 | 7 | import java.util.Objects; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentMap; 10 | import java.util.function.Function; 11 | import java.util.function.Supplier; 12 | 13 | import javax.annotation.Nonnull; 14 | import javax.annotation.Nullable; 15 | import javax.annotation.concurrent.GuardedBy; 16 | 17 | import org.slf4j.Logger; 18 | 19 | import com.github.phantomthief.tuple.TwoTuple; 20 | import com.github.phantomthief.util.ThrowableConsumer; 21 | 22 | /** 23 | * @author myco 24 | * Created on 2020-10-22 25 | * @see SharedResource 26 | * 新版 shared resource 实现,行为具体如下: 27 | */ 28 | public class SharedResourceV2 { 29 | 30 | private static final Logger logger = getLogger(SharedResourceV2.class); 31 | 32 | private final ConcurrentMap> resourceMap = new ConcurrentHashMap<>(); 33 | private final Function factory; 34 | private final ThrowableConsumer cleanup; 35 | 36 | public SharedResourceV2(@Nonnull Function factory, 37 | @Nonnull ThrowableConsumer cleanup) { 38 | this.factory = factory; 39 | this.cleanup = cleanup; 40 | } 41 | 42 | /** 43 | * 不管是否成功,都只get一次,然后缓存起来 44 | */ 45 | private static class OnceSupplier implements Supplier { 46 | 47 | private final Supplier> delegate; 48 | 49 | public OnceSupplier(Object key, Supplier delegate) { 50 | this.delegate = lazy(() -> { 51 | T value = null; 52 | Throwable throwable = null; 53 | try { 54 | value = delegate.get(); 55 | logger.debug("create shared resource for key: [{}] => [{}]", key, value); 56 | } catch (Throwable t) { 57 | throwable = t; 58 | } 59 | return tuple(value, throwable); 60 | }); 61 | } 62 | 63 | @Override 64 | public T get() { 65 | TwoTuple tuple = delegate.get(); 66 | Throwable throwable = tuple.getSecond(); 67 | if (throwable != null) { 68 | throw new OnceBrokenException(throwable); 69 | } else { 70 | return tuple.getFirst(); 71 | } 72 | } 73 | } 74 | 75 | private static class ResourceWrapper { 76 | 77 | private final K key; 78 | private final OnceSupplier resourceSupplier; 79 | 80 | @GuardedBy("ResourceWrapper::this") 81 | private volatile int counter = 0; 82 | @GuardedBy("ResourceWrapper::this") 83 | private volatile boolean expired = false; 84 | 85 | public ResourceWrapper(K key, @Nonnull Supplier resourceSupplier) { 86 | this.key = key; 87 | this.resourceSupplier = new OnceSupplier<>(key, resourceSupplier); 88 | } 89 | 90 | @Nonnull 91 | public V get() { 92 | return resourceSupplier.get(); 93 | } 94 | 95 | public int count() { 96 | return counter; 97 | } 98 | 99 | public boolean incr() { 100 | synchronized (this) { 101 | if (!expired) { 102 | counter++; 103 | logger.debug("incr success: [{}], refCount: [{}], expired: [{}]", key, counter, expired); 104 | return true; 105 | } 106 | } 107 | return false; 108 | } 109 | 110 | public boolean decr() { 111 | synchronized (this) { 112 | if (counter <= 0) { 113 | throw new AssertionError("INVALID INTERNAL STATE:" + key); 114 | } 115 | if (!expired) { 116 | counter--; 117 | if (counter <= 0) { 118 | expired = true; 119 | } 120 | logger.debug("decr success: [{}], refCount: [{}], expired: [{}]", key, counter, expired); 121 | return true; 122 | } 123 | } 124 | return false; 125 | } 126 | } 127 | 128 | @Nonnull 129 | private ResourceWrapper ensureWrapperExist(@Nonnull K key) { 130 | return resourceMap.compute(key, (k, v) -> { 131 | if (v == null || v.expired) { 132 | return new ResourceWrapper<>(k, 133 | () -> Objects.requireNonNull(factory.apply(k), "factory 不应返回 null, key: " + key)); 134 | } else { 135 | return v; 136 | } 137 | }); 138 | } 139 | 140 | private void removeExpiredWrapper(@Nonnull K key) { 141 | resourceMap.compute(key, (k, v) -> { 142 | if (v != null && v.expired) { 143 | return null; 144 | } else { 145 | return v; 146 | } 147 | }); 148 | } 149 | 150 | /** 151 | * 获取之前注册且未被注销的资源。 152 | * 使用相同 key 并发调用 get 和 register/unregister 时,本方法的返回值是不确定的(可能是null,也可能是之前注册的值) 153 | * 真实使用场景下,应避免相同的 key 并发 get/register/unregister,虽然本类是线程安全的,但是并发调用这些方法会让人很难理解 154 | * 真正共享的资源到底是哪一个 155 | */ 156 | @Nullable 157 | public V get(@Nonnull K key) { 158 | ResourceWrapper resourceWrapper = resourceMap.get(key); 159 | if (resourceWrapper != null) { 160 | return resourceWrapper.get(); 161 | } 162 | return null; 163 | } 164 | 165 | /** 166 | * 这个方法实现的过程中,踩了挺多坑。这里把当前实现的关键思路记录一下,看后续有没有更好的实现方法: 167 | * 1. 如果之前成功创建过相同 key 的资源,就应当复用之前创建的资源;所以不能一上来就用 factory 创建资源 168 | * 2. 为了避免 unregister 已经在清理中的资源再次被增加引用计数,肯定需要加锁;为了避免全局锁,首先想到可以对 key 加锁 169 | * 3. 但是 key 有可能出现不同对象,但是 equals & hashcode 相同的情况,所以直接对 key 加锁会有问题 170 | * 4. key 不能用于加锁,那能不能对 value resourceWrapper 加锁呢?也有问题,因为为了避免资源泄漏,value 在引用计数减到 0 的时候 171 | * 需要删掉,这样导致有可能锁没有加在同一个对象上 172 | * 5. 所以目前采用这样的方式:我们在 resourceWrapper 上记录一个状态,如果某个资源的引用计数降到 0 则认为这个资源已经过期, 173 | * 不能继续使用/增加计数了;在 register 和 unregister 过程中发现已经过期的资源,采用清理 + 重建的方式来解决 174 | */ 175 | @Nonnull 176 | public V register(@Nonnull K key) { 177 | boolean incr = false; 178 | ResourceWrapper resourceWrapper = null; 179 | while (!incr) { 180 | resourceWrapper = ensureWrapperExist(key); 181 | // 确保引用计数加成功才退出 182 | incr = resourceWrapper.incr(); 183 | } 184 | try { 185 | // 尝试初始化资源 186 | return resourceWrapper.get(); 187 | } catch (Throwable t) { 188 | // 初始化资源失败的话,把引用计数恢复一下,同时清理下 resourceMap 里引用计数降到 0 的记录 189 | unregister(key); 190 | throw t; 191 | } 192 | } 193 | 194 | @Nonnull 195 | public V unregister(@Nonnull K key) { 196 | boolean decr = false; 197 | ResourceWrapper resourceWrapper = null; 198 | while (!decr) { 199 | resourceWrapper = resourceMap.get(key); 200 | if (resourceWrapper == null) { 201 | throw new IllegalStateException("non paired unregister call for key:" + key); 202 | } 203 | decr = resourceWrapper.decr(); 204 | removeExpiredWrapper(key); 205 | } 206 | V resource = resourceWrapper.get(); 207 | if (resourceWrapper.expired) { 208 | try { 209 | cleanup.accept(resource); 210 | logger.info("cleanup resource: [{}] => [{}]", key, resource); 211 | } catch (Throwable e) { 212 | throw new UnregisterFailedException(e, resource); 213 | } 214 | } 215 | return resource; 216 | } 217 | 218 | public static class UnregisterFailedException extends RuntimeException { 219 | 220 | private final Object removed; 221 | 222 | private UnregisterFailedException(Throwable cause, Object removed) { 223 | super(cause); 224 | this.removed = removed; 225 | } 226 | 227 | @SuppressWarnings("unchecked") 228 | public T getRemoved() { 229 | return (T) removed; 230 | } 231 | } 232 | 233 | public static class OnceBrokenException extends RuntimeException { 234 | 235 | private OnceBrokenException(Throwable cause) { 236 | super("resource broken", cause); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/failover/impl/PriorityFailoverManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.failover.impl; 2 | 3 | import static com.github.phantomthief.failover.impl.PriorityGroupManagerTest.countOfEachGroup; 4 | import static java.util.Collections.singleton; 5 | import static java.util.Collections.singletonMap; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import com.github.phantomthief.failover.impl.PriorityFailoverBuilder.ResConfig; 15 | import com.github.phantomthief.failover.impl.PriorityFailoverManager.UpdateResult; 16 | 17 | /** 18 | * @author huangli 19 | * Created on 2020-02-02 20 | */ 21 | class PriorityFailoverManagerTest { 22 | 23 | private Object o0 = "o0"; 24 | private Object o1 = "o1"; 25 | private Object o2 = "o2"; 26 | 27 | @Test 28 | public void testUpdate() { 29 | PriorityFailover failover = PriorityFailover.newBuilder() 30 | .addResource(o0, 100, 0, 0, 100) 31 | .addResource(o1, 100, 0, 0, 100) 32 | .build(); 33 | PriorityFailoverManager manager = new PriorityFailoverManager<>(failover, null); 34 | 35 | Map addOrUpdate = new HashMap<>(); 36 | addOrUpdate.put(o1, new ResConfig( 37 | 10, 5, 1, -1/*illegal but will be ignore*/)); 38 | addOrUpdate.put(o2, new ResConfig(10, 5, 1, 7)); 39 | 40 | UpdateResult result = manager.update(addOrUpdate, singleton(o0)); 41 | 42 | assertEquals(1, result.getAddedResources().size()); 43 | assertEquals(1, result.getUpdatedResources().size()); 44 | assertEquals(1, result.getRemovedResources().size()); 45 | assertEquals(2, manager.getFailover().getResourcesMap().size()); 46 | 47 | assertEquals(10, result.getAddedResources().get(o2).getMaxWeight()); 48 | assertEquals(5, result.getAddedResources().get(o2).getMinWeight()); 49 | assertEquals(1, result.getAddedResources().get(o2).getPriority()); 50 | assertEquals(7, result.getAddedResources().get(o2).getInitWeight()); 51 | assertEquals(10, result.getUpdatedResources().get(o1).getMaxWeight()); 52 | assertEquals(5, result.getUpdatedResources().get(o1).getMinWeight()); 53 | assertEquals(1, result.getUpdatedResources().get(o1).getPriority()); 54 | assertEquals(10, result.getUpdatedResources().get(o1).getInitWeight()); 55 | assertEquals(100, result.getRemovedResources().get(o0).getMaxWeight()); 56 | assertEquals(0, result.getRemovedResources().get(o0).getMinWeight()); 57 | assertEquals(0, result.getRemovedResources().get(o0).getPriority()); 58 | assertEquals(100, result.getRemovedResources().get(o0).getInitWeight()); 59 | 60 | assertEquals(10, manager.getFailover().getResourcesMap().get(o1).currentWeight); 61 | assertEquals(10, manager.getFailover().getResourcesMap().get(o1).maxWeight); 62 | assertEquals(5, manager.getFailover().getResourcesMap().get(o1).minWeight); 63 | assertEquals(1, manager.getFailover().getResourcesMap().get(o1).priority); 64 | 65 | assertEquals(7, manager.getFailover().getResourcesMap().get(o2).currentWeight); 66 | assertEquals(10, manager.getFailover().getResourcesMap().get(o2).maxWeight); 67 | assertEquals(5, manager.getFailover().getResourcesMap().get(o2).minWeight); 68 | assertEquals(1, manager.getFailover().getResourcesMap().get(o2).priority); 69 | } 70 | 71 | @Test 72 | public void testUpdateAll() { 73 | PriorityFailover failover = PriorityFailover.newBuilder() 74 | .addResource(o0, 100, 0, 0, 100) 75 | .addResource(o1, 100, 0, 0, 100) 76 | .build(); 77 | PriorityFailoverManager manager = new PriorityFailoverManager<>(failover, null); 78 | Map addOrUpdate = new HashMap<>(); 79 | addOrUpdate.put(o1, new ResConfig( 80 | 10, 5, 1, -1/*illegal but will be ignore*/)); 81 | addOrUpdate.put(o2, new ResConfig(10, 5, 1, 7)); 82 | 83 | UpdateResult result = manager.updateAll(addOrUpdate); 84 | 85 | assertEquals(1, result.getAddedResources().size()); 86 | assertEquals(1, result.getUpdatedResources().size()); 87 | assertEquals(1, result.getRemovedResources().size()); 88 | assertEquals(2, manager.getFailover().getResourcesMap().size()); 89 | 90 | assertEquals(10, result.getAddedResources().get(o2).getMaxWeight()); 91 | assertEquals(5, result.getAddedResources().get(o2).getMinWeight()); 92 | assertEquals(1, result.getAddedResources().get(o2).getPriority()); 93 | assertEquals(7, result.getAddedResources().get(o2).getInitWeight()); 94 | assertEquals(10, result.getUpdatedResources().get(o1).getMaxWeight()); 95 | assertEquals(5, result.getUpdatedResources().get(o1).getMinWeight()); 96 | assertEquals(1, result.getUpdatedResources().get(o1).getPriority()); 97 | assertEquals(10, result.getUpdatedResources().get(o1).getInitWeight()); 98 | assertEquals(100, result.getRemovedResources().get(o0).getMaxWeight()); 99 | assertEquals(0, result.getRemovedResources().get(o0).getMinWeight()); 100 | assertEquals(0, result.getRemovedResources().get(o0).getPriority()); 101 | assertEquals(100, result.getRemovedResources().get(o0).getInitWeight()); 102 | 103 | assertEquals(10, manager.getFailover().getResourcesMap().get(o1).currentWeight); 104 | assertEquals(10, manager.getFailover().getResourcesMap().get(o1).maxWeight); 105 | assertEquals(5, manager.getFailover().getResourcesMap().get(o1).minWeight); 106 | assertEquals(1, manager.getFailover().getResourcesMap().get(o1).priority); 107 | 108 | 109 | assertEquals(7, manager.getFailover().getResourcesMap().get(o2).currentWeight); 110 | assertEquals(10, manager.getFailover().getResourcesMap().get(o2).maxWeight); 111 | assertEquals(5, manager.getFailover().getResourcesMap().get(o2).minWeight); 112 | assertEquals(1, manager.getFailover().getResourcesMap().get(o2).priority); 113 | } 114 | 115 | @Test 116 | public void testCurrentWeight() { 117 | PriorityFailover failover = PriorityFailover.newBuilder() 118 | .addResource(o0, 100, 0, 0, 100) 119 | .weightFunction(new SimpleWeightFunction<>(0.5, 1)) 120 | .build(); 121 | PriorityFailoverManager manager = new PriorityFailoverManager<>(failover, null); 122 | manager.getFailover().fail(manager.getFailover().getOneAvailable()); 123 | 124 | manager.updateAll(singletonMap(o0, new ResConfig(100, 0, 0, 100))); 125 | assertEquals(50, manager.getFailover().getResourcesMap().get(o0).currentWeight); 126 | 127 | 128 | manager.updateAll(singletonMap(o0, new ResConfig(100, 60, 0, 100))); 129 | assertEquals(60, manager.getFailover().getResourcesMap().get(o0).currentWeight); 130 | 131 | manager.updateAll(singletonMap(o0, new ResConfig(10, 0, 0, 100))); 132 | assertEquals(6, manager.getFailover().getResourcesMap().get(o0).currentWeight); 133 | 134 | manager.getFailover().success(manager.getFailover().getOneAvailable()); 135 | 136 | manager.updateAll(singletonMap(o0, new ResConfig(100, 0, 0, 100))); 137 | assertEquals(100, manager.getFailover().getResourcesMap().get(o0).currentWeight); 138 | 139 | manager.updateAll(singletonMap(o0, new ResConfig(10, 0, 0, 100))); 140 | assertEquals(10, manager.getFailover().getResourcesMap().get(o0).currentWeight); 141 | } 142 | 143 | @Test 144 | public void testAutoPriority() { 145 | PriorityFailoverManager manager = PriorityFailover.newBuilder() 146 | .enableAutoPriority(1) 147 | .addResource(o0) 148 | .addResource(o1) 149 | .buildManager(); 150 | int[] resCountOfGroup = countOfEachGroup(2, manager.getGroupManager()); 151 | Assertions.assertEquals(1, resCountOfGroup[0]); 152 | Assertions.assertEquals(1, resCountOfGroup[1]); 153 | 154 | 155 | manager.update(singletonMap(o2, new ResConfig()), singleton(o0)); 156 | 157 | resCountOfGroup = countOfEachGroup(2, manager.getGroupManager()); 158 | Assertions.assertEquals(1, resCountOfGroup[0]); 159 | Assertions.assertEquals(1, resCountOfGroup[1]); 160 | 161 | Map map = new HashMap<>(); 162 | map.put(o2, new ResConfig()); 163 | map.put(new Object(), new ResConfig()); 164 | map.put(new Object(), new ResConfig()); 165 | manager.updateAll(map); 166 | 167 | resCountOfGroup = countOfEachGroup(2, manager.getGroupManager()); 168 | Assertions.assertEquals(1, resCountOfGroup[0]); 169 | Assertions.assertEquals(2, resCountOfGroup[1]); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2015 w.vela 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | 203 | --------------------------------------------------------------------------------