|
1 | 1 | import functools
|
2 | 2 | import itertools
|
3 | 3 | import logging
|
| 4 | +import math |
4 | 5 | import random
|
5 | 6 | import time
|
6 | 7 | from abc import ABC, abstractmethod
|
@@ -66,6 +67,49 @@ def delay(attempt: int) -> float:
|
66 | 67 | return delay
|
67 | 68 |
|
68 | 69 |
|
| 70 | +def sigmoid_delay(offset: int = -5, midpoint: int = 0, step: int = 1) -> Callable[[int], float]: |
| 71 | + """ |
| 72 | + Returns an S-Curve function. |
| 73 | +
|
| 74 | + A sigmoid is the intersection of these two behaviors: |
| 75 | + `while(true): retry() # immediate retry` |
| 76 | + and |
| 77 | + `while(true): sleep(1); retry() # static-wait then retry` |
| 78 | +
|
| 79 | + The intersection of these two worlds is an exponential function which |
| 80 | + gradually ramps the program up to (or down to) a stable state (the s-curve). |
| 81 | + The sharpness of the curse is controlled with step. A step of 0 flattens the |
| 82 | + curve. A step of infinity turns the curve into a step change (a vertical |
| 83 | + line). |
| 84 | +
|
| 85 | + The sigmoid is more difficult to intuit than a simple exponential delay but it |
| 86 | + allows you to cap the maximum amount of time you're willing to wait between |
| 87 | + retries. The cap is _always_ 1 second regardless of the value of the other |
| 88 | + arguments. If you want to wait longer than one second multiply the result of |
| 89 | + the function by something! |
| 90 | +
|
| 91 | + Consider this program: |
| 92 | + [sigmoid_delay()(i) for i in range(-5, 5)] |
| 93 | + is equivalent to: |
| 94 | + [0.006, 0.017, 0.0474, 0.119, 0.268, 0.5, 0.731, 0.880, 0.952, 0.982] |
| 95 | +
|
| 96 | + You get the same results with: |
| 97 | + [sigmoid_delay()(i) for i in range(10)] |
| 98 | + except the window has changed: |
| 99 | + [0.5, 0.731, 0.880, 0.952, 0.982, ...] |
| 100 | +
|
| 101 | + Now you see further along the curve. This explains the utility of the `offset` |
| 102 | + parameter. The offset allows you to slide along the window. A smaller offset |
| 103 | + gives you faster retries. A larger offset gives you slower retries. An offset |
| 104 | + pushed too far past the midpoint reduces this function to a static wait. |
| 105 | + """ |
| 106 | + |
| 107 | + def delay(attempt: int) -> float: |
| 108 | + return 1 / (1 + math.exp(-step * ((attempt + offset) - midpoint))) |
| 109 | + |
| 110 | + return delay |
| 111 | + |
| 112 | + |
69 | 113 | class ConditionalRetryPolicy(RetryPolicy):
|
70 | 114 | """
|
71 | 115 | A basic policy that can be used to retry a callable based on the result
|
|
0 commit comments