exponential/policy.go (158 lines of code) (raw):

package exponential import ( "errors" "strings" "time" "github.com/jedib0t/go-pretty/v6/table" "github.com/sanity-io/litter" ) // Policy is the configuration for the backoff policy. Generally speaking you should use the // default policy, but you can create your own if you want to customize it. But think long and // hard about it before you do, as the default policy is a good mechanism for avoiding thundering // herd problems, which are always remote calls. If not doing remote calls, you should question the use // of this package. Note that a Policy is ignored if the service returns a delay in the error message. type Policy struct { // InitialInterval is how long to wait after the first failure before retrying. Must be // greater than 0. // Defaults to 100ms. InitialInterval time.Duration // Multiplier is used to increase the delay after each failure. Must be greater than 1. // Defaults to 2.0. Multiplier float64 // RandomizationFactor is used to randomize the delay. This prevents problems where multiple // clients are all retrying at the same intervals, and thus all hammering the server at the same time. // This is a value between 0 and 1. Zero(0) means no randomization, 1 means randomize by the entire interval. // The randomization factor sets a range of randomness in the positive and negative direction with a maximum // window of +/= RandomizationFactor * Interval. For example, if the RandomizationFactor is 0.5, the interval // will be randomized by up to 50% in the positive and negative direction. If the interval is 1s, the randomization // window is 0.5s to 1.5s. // Randomization can push the interval above the MaxInterval. The factor can be both positive and negative. // Defaults to 0.5 RandomizationFactor float64 // MaxInterval is the maximum amount of time to wait between retries. Must be > 0. // Defaults to 60s. MaxInterval time.Duration // MaxAttempts is the maximum number of attempts to make before giving up. If 0, then there is no limit. // Defaults to 0. When this occurs, the error returned will contain ErrPermanent. MaxAttempts int } func (p Policy) validate() error { if p.InitialInterval <= 0 { return errors.New("Policy.InitialInterval must be greater than 0") } if p.Multiplier <= 1 { return errors.New("Policy.Multiplier must be greater than 1") } if p.RandomizationFactor < 0 || p.RandomizationFactor > 1 { return errors.New("Policy.RandomizationFactor must be between 0 and 1") } if p.MaxInterval <= 0 { return errors.New("Policy.MaxInterval must be greater than 0") } if p.InitialInterval > p.MaxInterval { return errors.New("Policy.InitialInterval must be less than or equal to Policy.MaxInterval") } return nil } // TimeTableEntry is an entry in the time table. type TimeTableEntry struct { // Attempt is the attempt number that this entry is for. Attempt int // Interval is the interval to wait before the next attempt. However, this is // not the actual interval. The actual interval is the Interval plus or minus // the RandomizationFactor. Interval time.Duration // MinInterval is the minimum interval to wait before the next attempt. This is // Interval minus the maximum randomization factor. MinInterval time.Duration // MaxInterval is the maximum interval to wait before the next attempt. This is // Interval plus the maximum randomization factor. MaxInterval time.Duration } // TimeTable is a table of intervals describing the wait time between retries. This is useful for // both testing and understanding what a policy will do. type TimeTable struct { // MinTime is the minimum time a program will have to wait if every attempt gets the minimum interval // when calculating the RandomizationFactor. This value changes depending // on if Policy.TimeTable() had attempts set >= 0 or < 0. If attempts is >= 0, then MinTime // is the sum of all the MinInterval values up through the attempts. If attempts is < 0, then // MinTime is the sum of all the MinInterval values until we reach our maximum interval setting. MinTime time.Duration // MaxTime is the maximum time a program will have to wait if every attempt gets the maximum interval // when calculating the RandomizationFactor. This value changes depending on // if Policy.TimeTable() had attempts set >= 0 or < 0. If attempts is >= 0, then MaxTime // is the sum of all the MaxInterval values up through the attempts. If attempts is < 0, then // MaxTime is the sum of all the MaxInterval values until we reach our maximum interval setting. MaxTime time.Duration // Entries is the list of minimum and maximum intervals for each attempt. Entries []TimeTableEntry } // String implements fmt.Stringer. func (t TimeTable) String() string { var b strings.Builder w := table.NewWriter() w.SetOutputMirror(&b) b.WriteString("=============\n") b.WriteString("= TimeTable =\n") b.WriteString("=============\n") w.AppendHeader(table.Row{"Attempt", "Interval", "MinInterval", "MaxInterval"}) for _, e := range t.Entries { w.AppendRow(table.Row{e.Attempt, e.Interval, e.MinInterval, e.MaxInterval}) } w.AppendFooter(table.Row{"", "MinTime", "MaxTime"}) w.AppendFooter(table.Row{"", "", t.MinTime, t.MaxTime}) w.Render() return b.String() } var litterConf = litter.Options{ StripPackageNames: true, HidePrivateFields: true, Separator: "\t", StrictGo: true, } // Litter writes the TimeTable as a Go struct that can be used to recreate the TimeTable. // For use in internal testing only. func (t TimeTable) Litter() string { return litterConf.Sdump(t) } // TimeTable will return a TimeTable for the Policy. If attempts is >= 0, then the TimeTable will // be for that number of attempts. If attempts is < 0, then the TimeTable will be for all entries // until the maximum interval is reached. This should only be used in tools and testing. func (p Policy) TimeTable(attempts int) TimeTable { if attempts >= 0 { return p.timeTableWithAttempts(attempts) } return p.timeTable() } // timeTableWithAttempts creates a TimeTable with the given number of attempts which must be >= 0. func (p Policy) timeTableWithAttempts(attempts int) TimeTable { if attempts < 0 { panic("BUG: attempts must be >= 0") } tt := TimeTable{ Entries: []TimeTableEntry{ { Attempt: 1, Interval: 0, MinInterval: 0, MaxInterval: 0, }, }, } interval := p.InitialInterval for i := 2; i <= attempts; i++ { minInterval := interval - time.Duration(float64(interval)*p.RandomizationFactor) maxInterval := interval + time.Duration(float64(interval)*p.RandomizationFactor) entry := TimeTableEntry{ Attempt: i, Interval: interval, MinInterval: minInterval, MaxInterval: maxInterval, } tt.MinTime += minInterval tt.MaxTime += maxInterval tt.Entries = append(tt.Entries, entry) interval = time.Duration(float64(interval) * p.Multiplier) if interval > p.MaxInterval { interval = p.MaxInterval } } return tt } // timeTable creates a TimeTable for the Policy. This is for all attempts until the maximum interval // is reached. func (p Policy) timeTable() TimeTable { tt := TimeTable{ Entries: []TimeTableEntry{ { Attempt: 1, Interval: 0, MinInterval: 0, MaxInterval: 0, }, }, } interval := p.InitialInterval var i int for i = 2; interval != p.MaxInterval; i++ { minInterval := interval - time.Duration(float64(interval)*p.RandomizationFactor) maxInterval := interval + time.Duration(float64(interval)*p.RandomizationFactor) entry := TimeTableEntry{ Attempt: i, Interval: interval, MinInterval: minInterval, MaxInterval: maxInterval, } tt.MinTime += minInterval tt.MaxTime += maxInterval tt.Entries = append(tt.Entries, entry) interval = time.Duration(float64(interval) * p.Multiplier) if interval > p.MaxInterval { interval = p.MaxInterval } } // This is the final entry at the maximum interval. entry := TimeTableEntry{ Attempt: i, Interval: interval, MinInterval: interval - time.Duration(float64(interval)*p.RandomizationFactor), MaxInterval: interval + time.Duration(float64(interval)*p.RandomizationFactor), } tt.MinTime += entry.MinInterval tt.MaxTime += entry.MaxInterval tt.Entries = append(tt.Entries, entry) return tt } // defaults creates a new Policy with the default values. func defaults() Policy { // progression will be: // 100ms, 200ms, 400ms, 800ms, 1.6s, 3.2s, 6.4s, 12.8s, 25.6s, 51.2s, 60s // Not counting a randomization factor which will be +/- up to 50% of the interval. return Policy{ InitialInterval: 100 * time.Millisecond, Multiplier: 2, RandomizationFactor: 0.5, MaxInterval: 60 * time.Second, } }