Home | History | Annotate | Download | only in backoff
      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    */
     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    */
     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
    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
    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