1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dialer.common.backoff;
18 
19 import com.android.dialer.common.Assert;
20 
21 /**
22  * Given an initial delay, D, a maximum total backoff time, T, and a maximum number of backoffs, N,
23  * this class calculates a base multiplier, b, and a scaling factor, g, such that the initial
24  * backoff is g*b = D and the sum of the scaled backoffs is T = g*b + g*b^2 + g*b^3 + ... g*b^N
25  *
26  * <p>T/D = (1 - b^N)/(1 - b) but this cannot be written as a simple equation for b in terms of T, N
27  * and D so instead use Newton's method (https://en.wikipedia.org/wiki/Newton%27s_method) to find an
28  * approximate value for b.
29  *
30  * <p>Example usage using the {@code ExponentialBackoff} would be:
31  *
32  * <pre>
33  *   // Retry with exponential backoff for up to 2 minutes, with an initial delay of 100 millis
34  *   // and a maximum of 10 retries
35  *   long initialDelayMillis = 100;
36  *   int maxTries = 10;
37  *   double base = ExponentialBaseCalculator.findBase(
38  *       initialDelayMillis,
39  *       TimeUnit.MINUTES.toMillis(2),
40  *       maxTries);
41  *   ExponentialBackoff backoff = new ExponentialBackoff(initialDelayMillis, base, maxTries);
42  *   while (backoff.isInRange()) {
43  *     ...
44  *     long delay = backoff.getNextBackoff();
45  *     // Wait for the indicated time...
46  *   }
47  * </pre>
48  */
49 public final class ExponentialBaseCalculator {
50   private static final int MAX_STEPS = 1000;
51   private static final double DEFAULT_TOLERANCE_MILLIS = 1;
52 
53   /**
54    * Calculate an exponential backoff base multiplier such that the first backoff delay will be as
55    * specified and the sum of the delays after doing the indicated maximum number of backoffs will
56    * be as specified.
57    *
58    * @throws IllegalArgumentException if the initial delay is greater than the total backoff time
59    * @throws IllegalArgumentException if the maximum number of backoffs is not greater than 1
60    * @throws IllegalStateException if it fails to find an acceptable base multiplier
61    */
findBase( long initialDelayMillis, long totalBackoffTimeMillis, int maximumBackoffs)62   public static double findBase(
63       long initialDelayMillis, long totalBackoffTimeMillis, int maximumBackoffs) {
64     Assert.checkArgument(initialDelayMillis < totalBackoffTimeMillis);
65     Assert.checkArgument(maximumBackoffs > 1);
66     long scaledTotalTime = Math.round(((double) totalBackoffTimeMillis) / initialDelayMillis);
67     double scaledTolerance = DEFAULT_TOLERANCE_MILLIS / initialDelayMillis;
68     return getBaseImpl(scaledTotalTime, maximumBackoffs, scaledTolerance);
69   }
70 
71   /**
72    * T/D = (1 - b^N)/(1 - b) but this cannot be written as a simple equation for b in terms of T, D
73    * and N so instead we use Newtons method to find an approximate value for b.
74    *
75    * <p>Let f(b) = (1 - b^N)/(1 - b) - T/D then we want to find b* such that f(b*) = 0, or more
76    * precisely |f(b*)| < tolerance
77    *
78    * <p>Using Newton's method we can interatively find b* as follows: b1 = b0 - f(b0)/f'(b0), where
79    * b0 is the current best guess for b* and b1 is the next best guess.
80    *
81    * <p>f'(b) = (f(b) + T/D - N*b^(N - 1))/(1 - b)
82    *
83    * <p>so
84    *
85    * <p>b1 = b0 - f(b0)(1 - b0)/(f(b0) + T/D - N*b0^(N - 1))
86    */
getBaseImpl(long t, int n, double tolerance)87   private static double getBaseImpl(long t, int n, double tolerance) {
88     double b0 = 2; // Initial guess for b*
89     double b0n = Math.pow(b0, n);
90     double fb0 = f(b0, t, b0n);
91     if (Math.abs(fb0) < tolerance) {
92       // Initial guess was pretty good
93       return b0;
94     }
95 
96     for (int i = 0; i < MAX_STEPS; i++) {
97       double fpb0 = fp(b0, t, n, fb0, b0n);
98       double b1 = b0 - fb0 / fpb0;
99       double b1n = Math.pow(b1, n);
100       double fb1 = f(b1, t, b1n);
101 
102       if (Math.abs(fb1) < tolerance) {
103         // Found an acceptable value
104         return b1;
105       }
106 
107       b0 = b1;
108       b0n = b1n;
109       fb0 = fb1;
110     }
111 
112     throw new IllegalStateException("Failed to find base. Too many iterations.");
113   }
114 
115   // Evaluate f(b), the function we are trying to find the zero for.
116   // Note: passing b^N as a parameter so it only has to be calculated once
f(double b, long t, double bn)117   private static double f(double b, long t, double bn) {
118     return (1 - bn) / (1 - b) - t;
119   }
120 
121   // Evaluate f'(b), the derivative of the function we are trying to find the zero for.
122   // Note: passing f(b) and b^N as parameters for efficiency
fp(double b, long t, int n, double fb, double bn)123   private static double fp(double b, long t, int n, double fb, double bn) {
124     return (fb + t - n * bn / b) / (1 - b);
125   }
126 }
127