| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package webhook |
| 4 | |
| 5 | import "time" |
| 6 | |
| 7 | // BackoffBase is the smallest retry delay; doubles per attempt. |
| 8 | const BackoffBase = 30 * time.Second |
| 9 | |
| 10 | // BackoffMax caps the delay so a stale subscriber doesn't disappear |
| 11 | // for days at a time. The spec calls for 24 hours. |
| 12 | const BackoffMax = 24 * time.Hour |
| 13 | |
| 14 | // Backoff returns the delay before retrying delivery `attempt` (1- |
| 15 | // indexed: the just-failed attempt counts as 1). Applies ±20% jitter |
| 16 | // so a fleet of webhooks pointed at one outage doesn't synchronize |
| 17 | // retries. `jitter` returns a value in [0, 1); pass `nil` for no jitter |
| 18 | // (deterministic schedule, useful in tests). |
| 19 | func Backoff(attempt int, jitter func() float64) time.Duration { |
| 20 | if attempt < 1 { |
| 21 | attempt = 1 |
| 22 | } |
| 23 | d := BackoffBase |
| 24 | for i := 1; i < attempt; i++ { |
| 25 | d *= 2 |
| 26 | if d >= BackoffMax { |
| 27 | d = BackoffMax |
| 28 | break |
| 29 | } |
| 30 | } |
| 31 | if d > BackoffMax { |
| 32 | d = BackoffMax |
| 33 | } |
| 34 | if jitter != nil { |
| 35 | mult := 0.8 + 0.4*jitter() |
| 36 | d = time.Duration(float64(d) * mult) |
| 37 | } |
| 38 | return d |
| 39 | } |
| 40 |