View Javadoc
1   package net.secodo.jcircuitbreaker.breakstrategy.impl;
2   
3   import java.util.Collection;
4   import java.util.Collections;
5   import java.util.List;
6   import java.util.stream.Collectors;
7   
8   import net.secodo.jcircuitbreaker.breaker.execution.ExecutedTask;
9   import net.secodo.jcircuitbreaker.breaker.execution.ExecutionContext;
10  import net.secodo.jcircuitbreaker.breakstrategy.BreakStrategy;
11  import net.secodo.jcircuitbreaker.task.Task;
12  import net.secodo.jcircuitbreaker.util.TimeUtil;
13  
14  
15  /**
16   * An implementation of {@link BreakStrategy} which causes the circuit breaker to break (not execute the Task) in
17   * case all tasks currently running inside the circuit breaker takes long time on average.
18   *
19   * <p>In constructor you define (in milliseconds) how long on average you expect the execution of single Task to take.
20   * If on average all currently running tasks take longer than this strategy will decide to break (not to execute
21   * another Task).
22   *
23   * <p>You may additionally define {@link #percentageOfMaxTimesToSkip} which is how many percent of the executions with
24   * highest execution time (longest executions) should not be considered when taking calculating the average.
25   *
26   * <p>For example this strategy:
27   *
28   * <p>new LimitedCurrentAverageExecutionTimeStrategy(1000, 10);
29   *
30   * <p>breaks (not allows execution of target-method) in case 90% of other executions (currently in
31   * progress) takes more than one  is 1 second (1000 ms). Example figures: there are 20 executions (threads)
32   * currently going through circuit breaker. 2 are running for 6 seconds, 18 are running for 900 milliseconds. We skip
33   * 10 % of longest executions - this is 2 executions (10% * 20  = 2) and we calculate average from the remaining 18
34   * executions. The average is 900 ms. This is less than allowed maximum = 1000 ms therefore next execution will be
35   * allowed.
36   *
37   * <p>NOTE: in above example, if the average was above 1000ms than this strategy would return false to circuit breaker
38   * and circuit breaker would execute {@link net.secodo.jcircuitbreaker.breakhandler.BreakHandler} to handle this
39   * situation
40   *
41   * @param <R> the return type of <i>real-method</i> that is executed by circuit breaker
42   */
43  public class LimitedCurrentAverageExecutionTimeStrategy<R> implements BreakStrategy<R> {
44    private final TimeUtil timeUtil;
45  
46    /**
47     * Protected access to allow extending classes to dynamically change it,
48     * e.g. via MBean
49     */
50    protected float maxAllowedExecutionTimeMillis;
51  
52    /**
53     * Protected access to allow extending classes to dynamically change it,
54     * e.g. via MBean
55     */
56    protected float percentageOfMaxTimesToSkip;
57  
58    /**
59     * Constructs new strategy with given maximum allowed execution. This is the execution of the threads which are
60     * currently going through the circuit breaker.
61     *
62     * @param maxAllowedAverageExecutionTimeMillis max time which does not result in "break" (which allows execution of
63     *                                             target-method)
64     * @param percentageOfLongestTimesToSkip       how many percent of longest times should not be considered when
65     *                                             calculating the average
66     */
67    public LimitedCurrentAverageExecutionTimeStrategy(long maxAllowedAverageExecutionTimeMillis,
68                                                      int percentageOfLongestTimesToSkip) {
69      this.maxAllowedExecutionTimeMillis = maxAllowedAverageExecutionTimeMillis;
70      timeUtil = new TimeUtil();
71  
72      this.percentageOfMaxTimesToSkip = ((percentageOfLongestTimesToSkip >= 0) && (percentageOfLongestTimesToSkip <= 100))
73        ? (0.01f * (float) percentageOfLongestTimesToSkip) : 0;
74    }
75  
76  
77    /**
78     * Constructs new strategy with given maximum allowed execution. This is the execution of the threads which are
79     * currently going through the circuit breaker.
80     *
81     * @param maxAllowedAverageExecutionTimeMillis max time which does not result in "break" (which allows execution of
82     */
83    public LimitedCurrentAverageExecutionTimeStrategy(long maxAllowedAverageExecutionTimeMillis) {
84      this(maxAllowedAverageExecutionTimeMillis, 0);
85    }
86  
87    @Override
88    public boolean shouldBreak(Task<R> task, ExecutionContext<R> executionContext) {
89      final Collection<ExecutedTask<R>> executionsInProgress = executionContext.getExecutionsInProgress();
90  
91      if (executionsInProgress.isEmpty()) {
92        return false;
93      }
94  
95      final List<Long> startTimestamps = executionsInProgress.stream()
96        .map(ExecutedTask::getExecutionStartedTimestamp)
97        .collect(Collectors.toList());
98  
99      Collections.sort(startTimestamps);
100 
101     final List<Long> interestingTimestamps = startTimestamps.subList(0,
102       (int) ((1f - percentageOfMaxTimesToSkip) * (float) startTimestamps.size()));
103 
104     if (interestingTimestamps.isEmpty()) {
105       return false;
106     }
107 
108 
109     final long currentTime = timeUtil.getCurrentTimeMilis();
110 
111     final Long sumOfInterestingTimestamps = interestingTimestamps.stream().mapToLong(t -> currentTime - t).sum();
112 
113 
114     final float currentAverageExecutionTime = (float) sumOfInterestingTimestamps / (float) interestingTimestamps.size();
115 
116 
117     return currentAverageExecutionTime > maxAllowedExecutionTimeMillis;
118   }
119 }