acemq_amqp.retry
When to try again, where to wait, and when to stop.
The schedule arithmetic here is part of the cross-language contract rather than
a local choice: the same policy must produce the same delays in Java, Go, .NET
and Python, because the same message can be retried by a consumer written in
any of them. The jitter is the one exception — it is random by definition —
so RetryPolicy.schedule() exposes the delays without it, which is what
to read when deciding whether a policy is the one you meant.
Where the waiting happens is the other half. A short wait is spent in the
consumer, holding the delivery; a long one is spent in the broker, in a queue
whose message TTL is the wait. The threshold between them is
DEFAULT_BROKER_WAIT_THRESHOLD unless a policy says otherwise, and the
reason for having a threshold at all rather than one rule is in
RetryPolicy.
1# Copyright 2026 AceMQ. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""When to try again, where to wait, and when to stop. 16 17The schedule arithmetic here is part of the cross-language contract rather than 18a local choice: the same policy must produce the same delays in Java, Go, .NET 19and Python, because the same message can be retried by a consumer written in 20any of them. The jitter is the one exception — it is random by definition — 21so :meth:`RetryPolicy.schedule` exposes the delays *without* it, which is what 22to read when deciding whether a policy is the one you meant. 23 24Where the waiting happens is the other half. A short wait is spent in the 25consumer, holding the delivery; a long one is spent in the broker, in a queue 26whose message TTL is the wait. The threshold between them is 27:data:`DEFAULT_BROKER_WAIT_THRESHOLD` unless a policy says otherwise, and the 28reason for having a threshold at all rather than one rule is in 29:class:`RetryPolicy`. 30""" 31 32from __future__ import annotations 33 34import random 35from dataclasses import dataclass, replace 36from datetime import timedelta 37 38ZERO = timedelta(0) 39 40#: Waits this long or longer are spent in the broker rather than in the 41#: consumer. Thirty seconds is roughly where the two costs cross: below it, the 42#: seconds a restart loses are only seconds and a held prefetch slot is cheap; 43#: above it, a consumer that restarts mid-wait loses the wait entirely, because 44#: the broker redelivers the unacknowledged message at once and a five-minute 45#: backoff becomes instant. 46DEFAULT_BROKER_WAIT_THRESHOLD = timedelta(seconds=30) 47 48 49@dataclass(frozen=True, slots=True) 50class Wait: 51 """How long before the next attempt, and where the message spends it. 52 53 Two answers rather than one because they cannot be worked out separately: 54 jitter applies only to a wait spent in the consumer, so a caller given a 55 delay alone could not tell whether it had already been moved — and a 56 jittered delay does not name a rung queue. 57 58 :param delay: how long the message waits 59 :param in_broker: whether it waits on a rung queue rather than here 60 """ 61 62 delay: timedelta 63 in_broker: bool 64 65 66@dataclass(frozen=True, slots=True) 67class RetryPolicy: 68 """How many times to try, how long to wait, and where. 69 70 Waits below ``broker_wait_threshold`` are spent in the consumer, which holds 71 the delivery and one prefetch slot for the duration. Waits at or above it 72 are spent in a ``{queue}.retry.{delay}`` queue whose ``x-message-ttl`` is the 73 wait and whose dead-letter target is the source queue, so the broker returns 74 the message when the time is up and a consumer restart cannot shorten it. 75 76 Splitting at a threshold rather than picking one of the two takes the 77 durability where it is worth its complexity and leaves the simplicity where 78 it is not. A queue's TTL is fixed at declaration and so cannot express a 79 jittered delay, which is why jitter applies below the threshold only; above 80 it the spread comes free, because each message's TTL starts when it enters 81 the rung, so a fleet that failed over ten seconds is released over ten 82 seconds. 83 84 :param max_attempts: total deliveries including the first. 1 means no retry 85 :param initial_delay: the wait before the second attempt 86 :param multiplier: what the delay is multiplied by each time 87 :param max_delay: the ceiling, or zero for none 88 :param jitter_factor: how far a delay may move either side, 0 to 1 89 :param max_message_age: give up on anything older, or zero for never 90 :param broker_wait_threshold: waits this long or longer are spent in the 91 broker, or zero to spend every wait in the consumer 92 """ 93 94 max_attempts: int = 1 95 initial_delay: timedelta = ZERO 96 multiplier: float = 2.0 97 max_delay: timedelta = ZERO 98 jitter_factor: float = 0.0 99 max_message_age: timedelta = ZERO 100 broker_wait_threshold: timedelta = DEFAULT_BROKER_WAIT_THRESHOLD 101 102 def give_up_after(self, age: timedelta) -> RetryPolicy: 103 """A copy that abandons a message older than ``age``. 104 105 The honest limit when a queue has been paused: attempts say nothing 106 about how long a message has been waiting, and a message four days old 107 is usually one nobody wants delivered now. 108 """ 109 return replace(self, max_message_age=age) 110 111 def with_jitter(self, factor: float) -> RetryPolicy: 112 """A copy using a different jitter factor, between 0 and 1.""" 113 return replace(self, jitter_factor=factor) 114 115 def wait_in_broker_from(self, threshold: timedelta) -> RetryPolicy: 116 """A copy that moves the line between waiting here and waiting there. 117 118 Zero is the way out: with no threshold nothing is long enough to reach 119 the broker, so every wait is spent in the consumer and no rung queue is 120 needed. That is the right setting for a service whose broker it may not 121 declare queues on, and the wrong one for a policy with delays measured 122 in minutes. 123 124 :param threshold: waits this long or longer go to a rung queue, or zero 125 for none of them 126 :returns: a new policy 127 """ 128 return replace(self, broker_wait_threshold=threshold) 129 130 def waits_in_broker(self, delay: timedelta) -> bool: 131 """Whether a wait of this length belongs on a rung queue. 132 133 :param delay: an unjittered delay, as :meth:`schedule` reports them 134 :returns: whether the broker rather than the consumer should hold it 135 """ 136 return ( 137 self.broker_wait_threshold > ZERO 138 and delay > ZERO 139 and delay >= self.broker_wait_threshold 140 ) 141 142 def broker_rungs(self) -> list[timedelta]: 143 """The delays this policy needs a rung queue for, longest last. 144 145 Exactly the entries of :meth:`schedule` that are at or above the 146 threshold, with repeats removed — a fixed policy that waits a minute 147 three times needs one queue, not three. It is a finite list because the 148 schedule is, which is what makes the queues declarable up front rather 149 than conjured by a consumer at the moment it first fails. 150 151 :returns: one delay per queue, in the order the schedule reaches them 152 """ 153 return list(dict.fromkeys(d for d in self.schedule() if self.waits_in_broker(d))) 154 155 def next_delay(self, attempt: int, message_age: timedelta = ZERO) -> timedelta | None: 156 """How long to wait before the next attempt, or ``None`` to give up. 157 158 :param attempt: the attempt that has just failed, starting at 1 159 :param message_age: how old the message is 160 :returns: the delay, or ``None`` when there should be no further attempt 161 """ 162 wait = self.next_wait(attempt, message_age) 163 return None if wait is None else wait.delay 164 165 def next_wait(self, attempt: int, message_age: timedelta = ZERO) -> Wait | None: 166 """The whole answer: how long to wait, and where. 167 168 :param attempt: the attempt that has just failed, starting at 1 169 :param message_age: how old the message is 170 :returns: the wait, or ``None`` when there should be no further attempt 171 """ 172 if attempt >= self.max_attempts: 173 return None 174 if self.max_message_age > ZERO and message_age >= self.max_message_age: 175 return None 176 177 delay = self._unjittered(attempt) 178 179 if self.waits_in_broker(delay): 180 # Deliberately not jittered. A rung queue's TTL is fixed when it is 181 # declared, so a moved delay would name a queue that does not exist; 182 # and the spread jitter buys is already there, because each message's 183 # TTL starts when it arrives rather than when the batch failed. 184 return Wait(delay, in_broker=True) 185 186 if self.jitter_factor > 0 and delay > ZERO: 187 # Both directions, so a fleet of consumers that failed together 188 # does not come back together. One-sided jitter only ever delays, 189 # which turns a thundering herd into a slower thundering herd. 190 factor = 1 + ((random.random() * 2 - 1) * self.jitter_factor) 191 delay = delay * factor 192 193 return Wait(max(delay, ZERO), in_broker=False) 194 195 def _unjittered(self, attempt: int) -> timedelta: 196 """The delay after ``attempt``, before jitter and before the threshold.""" 197 delay = self.initial_delay 198 for _ in range(1, attempt): 199 delay = delay * self.multiplier 200 if self.max_delay > ZERO and delay > self.max_delay: 201 delay = self.max_delay 202 break 203 if self.max_delay > ZERO and delay > self.max_delay: 204 delay = self.max_delay 205 return delay 206 207 def schedule(self) -> list[timedelta]: 208 """The delays this policy would use, without jitter. 209 210 What to look at when deciding whether a policy is the one you meant: 211 ``ExponentialRetry(5, 1s, 1m).schedule()`` is ``[1s, 2s, 4s, 8s]``, and 212 four numbers are easier to argue with than three parameters. 213 """ 214 delays: list[timedelta] = [] 215 delay = self.initial_delay 216 for _ in range(1, max(self.max_attempts, 0)): 217 capped = delay 218 if self.max_delay > ZERO and capped > self.max_delay: 219 capped = self.max_delay 220 delays.append(capped) 221 delay = delay * self.multiplier 222 return delays 223 224 225def no_retry() -> RetryPolicy: 226 """One delivery, no second chance.""" 227 return RetryPolicy(max_attempts=1) 228 229 230def exponential_retry( 231 max_attempts: int, 232 initial_delay: timedelta, 233 max_delay: timedelta = ZERO, 234) -> RetryPolicy: 235 """Doubling delays with 20% jitter, which is the sane default. 236 237 :param max_attempts: total deliveries including the first 238 :param initial_delay: the wait before the second attempt 239 :param max_delay: the ceiling, or zero for none 240 """ 241 return RetryPolicy( 242 max_attempts=max_attempts, 243 initial_delay=initial_delay, 244 multiplier=2.0, 245 max_delay=max_delay, 246 jitter_factor=0.2, 247 ) 248 249 250def fixed_retry(max_attempts: int, delay: timedelta) -> RetryPolicy: 251 """The same wait every time, with no jitter.""" 252 return RetryPolicy(max_attempts=max_attempts, initial_delay=delay, multiplier=1.0)
50@dataclass(frozen=True, slots=True) 51class Wait: 52 """How long before the next attempt, and where the message spends it. 53 54 Two answers rather than one because they cannot be worked out separately: 55 jitter applies only to a wait spent in the consumer, so a caller given a 56 delay alone could not tell whether it had already been moved — and a 57 jittered delay does not name a rung queue. 58 59 :param delay: how long the message waits 60 :param in_broker: whether it waits on a rung queue rather than here 61 """ 62 63 delay: timedelta 64 in_broker: bool
How long before the next attempt, and where the message spends it.
Two answers rather than one because they cannot be worked out separately: jitter applies only to a wait spent in the consumer, so a caller given a delay alone could not tell whether it had already been moved — and a jittered delay does not name a rung queue.
Parameters
- delay: how long the message waits
- in_broker: whether it waits on a rung queue rather than here
67@dataclass(frozen=True, slots=True) 68class RetryPolicy: 69 """How many times to try, how long to wait, and where. 70 71 Waits below ``broker_wait_threshold`` are spent in the consumer, which holds 72 the delivery and one prefetch slot for the duration. Waits at or above it 73 are spent in a ``{queue}.retry.{delay}`` queue whose ``x-message-ttl`` is the 74 wait and whose dead-letter target is the source queue, so the broker returns 75 the message when the time is up and a consumer restart cannot shorten it. 76 77 Splitting at a threshold rather than picking one of the two takes the 78 durability where it is worth its complexity and leaves the simplicity where 79 it is not. A queue's TTL is fixed at declaration and so cannot express a 80 jittered delay, which is why jitter applies below the threshold only; above 81 it the spread comes free, because each message's TTL starts when it enters 82 the rung, so a fleet that failed over ten seconds is released over ten 83 seconds. 84 85 :param max_attempts: total deliveries including the first. 1 means no retry 86 :param initial_delay: the wait before the second attempt 87 :param multiplier: what the delay is multiplied by each time 88 :param max_delay: the ceiling, or zero for none 89 :param jitter_factor: how far a delay may move either side, 0 to 1 90 :param max_message_age: give up on anything older, or zero for never 91 :param broker_wait_threshold: waits this long or longer are spent in the 92 broker, or zero to spend every wait in the consumer 93 """ 94 95 max_attempts: int = 1 96 initial_delay: timedelta = ZERO 97 multiplier: float = 2.0 98 max_delay: timedelta = ZERO 99 jitter_factor: float = 0.0 100 max_message_age: timedelta = ZERO 101 broker_wait_threshold: timedelta = DEFAULT_BROKER_WAIT_THRESHOLD 102 103 def give_up_after(self, age: timedelta) -> RetryPolicy: 104 """A copy that abandons a message older than ``age``. 105 106 The honest limit when a queue has been paused: attempts say nothing 107 about how long a message has been waiting, and a message four days old 108 is usually one nobody wants delivered now. 109 """ 110 return replace(self, max_message_age=age) 111 112 def with_jitter(self, factor: float) -> RetryPolicy: 113 """A copy using a different jitter factor, between 0 and 1.""" 114 return replace(self, jitter_factor=factor) 115 116 def wait_in_broker_from(self, threshold: timedelta) -> RetryPolicy: 117 """A copy that moves the line between waiting here and waiting there. 118 119 Zero is the way out: with no threshold nothing is long enough to reach 120 the broker, so every wait is spent in the consumer and no rung queue is 121 needed. That is the right setting for a service whose broker it may not 122 declare queues on, and the wrong one for a policy with delays measured 123 in minutes. 124 125 :param threshold: waits this long or longer go to a rung queue, or zero 126 for none of them 127 :returns: a new policy 128 """ 129 return replace(self, broker_wait_threshold=threshold) 130 131 def waits_in_broker(self, delay: timedelta) -> bool: 132 """Whether a wait of this length belongs on a rung queue. 133 134 :param delay: an unjittered delay, as :meth:`schedule` reports them 135 :returns: whether the broker rather than the consumer should hold it 136 """ 137 return ( 138 self.broker_wait_threshold > ZERO 139 and delay > ZERO 140 and delay >= self.broker_wait_threshold 141 ) 142 143 def broker_rungs(self) -> list[timedelta]: 144 """The delays this policy needs a rung queue for, longest last. 145 146 Exactly the entries of :meth:`schedule` that are at or above the 147 threshold, with repeats removed — a fixed policy that waits a minute 148 three times needs one queue, not three. It is a finite list because the 149 schedule is, which is what makes the queues declarable up front rather 150 than conjured by a consumer at the moment it first fails. 151 152 :returns: one delay per queue, in the order the schedule reaches them 153 """ 154 return list(dict.fromkeys(d for d in self.schedule() if self.waits_in_broker(d))) 155 156 def next_delay(self, attempt: int, message_age: timedelta = ZERO) -> timedelta | None: 157 """How long to wait before the next attempt, or ``None`` to give up. 158 159 :param attempt: the attempt that has just failed, starting at 1 160 :param message_age: how old the message is 161 :returns: the delay, or ``None`` when there should be no further attempt 162 """ 163 wait = self.next_wait(attempt, message_age) 164 return None if wait is None else wait.delay 165 166 def next_wait(self, attempt: int, message_age: timedelta = ZERO) -> Wait | None: 167 """The whole answer: how long to wait, and where. 168 169 :param attempt: the attempt that has just failed, starting at 1 170 :param message_age: how old the message is 171 :returns: the wait, or ``None`` when there should be no further attempt 172 """ 173 if attempt >= self.max_attempts: 174 return None 175 if self.max_message_age > ZERO and message_age >= self.max_message_age: 176 return None 177 178 delay = self._unjittered(attempt) 179 180 if self.waits_in_broker(delay): 181 # Deliberately not jittered. A rung queue's TTL is fixed when it is 182 # declared, so a moved delay would name a queue that does not exist; 183 # and the spread jitter buys is already there, because each message's 184 # TTL starts when it arrives rather than when the batch failed. 185 return Wait(delay, in_broker=True) 186 187 if self.jitter_factor > 0 and delay > ZERO: 188 # Both directions, so a fleet of consumers that failed together 189 # does not come back together. One-sided jitter only ever delays, 190 # which turns a thundering herd into a slower thundering herd. 191 factor = 1 + ((random.random() * 2 - 1) * self.jitter_factor) 192 delay = delay * factor 193 194 return Wait(max(delay, ZERO), in_broker=False) 195 196 def _unjittered(self, attempt: int) -> timedelta: 197 """The delay after ``attempt``, before jitter and before the threshold.""" 198 delay = self.initial_delay 199 for _ in range(1, attempt): 200 delay = delay * self.multiplier 201 if self.max_delay > ZERO and delay > self.max_delay: 202 delay = self.max_delay 203 break 204 if self.max_delay > ZERO and delay > self.max_delay: 205 delay = self.max_delay 206 return delay 207 208 def schedule(self) -> list[timedelta]: 209 """The delays this policy would use, without jitter. 210 211 What to look at when deciding whether a policy is the one you meant: 212 ``ExponentialRetry(5, 1s, 1m).schedule()`` is ``[1s, 2s, 4s, 8s]``, and 213 four numbers are easier to argue with than three parameters. 214 """ 215 delays: list[timedelta] = [] 216 delay = self.initial_delay 217 for _ in range(1, max(self.max_attempts, 0)): 218 capped = delay 219 if self.max_delay > ZERO and capped > self.max_delay: 220 capped = self.max_delay 221 delays.append(capped) 222 delay = delay * self.multiplier 223 return delays
How many times to try, how long to wait, and where.
Waits below broker_wait_threshold are spent in the consumer, which holds
the delivery and one prefetch slot for the duration. Waits at or above it
are spent in a {queue}acemq_amqp.retry.{delay} queue whose x-message-ttl is the
wait and whose dead-letter target is the source queue, so the broker returns
the message when the time is up and a consumer restart cannot shorten it.
Splitting at a threshold rather than picking one of the two takes the durability where it is worth its complexity and leaves the simplicity where it is not. A queue's TTL is fixed at declaration and so cannot express a jittered delay, which is why jitter applies below the threshold only; above it the spread comes free, because each message's TTL starts when it enters the rung, so a fleet that failed over ten seconds is released over ten seconds.
Parameters
- max_attempts: total deliveries including the first. 1 means no retry
- initial_delay: the wait before the second attempt
- multiplier: what the delay is multiplied by each time
- max_delay: the ceiling, or zero for none
- jitter_factor: how far a delay may move either side, 0 to 1
- max_message_age: give up on anything older, or zero for never
- broker_wait_threshold: waits this long or longer are spent in the broker, or zero to spend every wait in the consumer
103 def give_up_after(self, age: timedelta) -> RetryPolicy: 104 """A copy that abandons a message older than ``age``. 105 106 The honest limit when a queue has been paused: attempts say nothing 107 about how long a message has been waiting, and a message four days old 108 is usually one nobody wants delivered now. 109 """ 110 return replace(self, max_message_age=age)
A copy that abandons a message older than age.
The honest limit when a queue has been paused: attempts say nothing about how long a message has been waiting, and a message four days old is usually one nobody wants delivered now.
112 def with_jitter(self, factor: float) -> RetryPolicy: 113 """A copy using a different jitter factor, between 0 and 1.""" 114 return replace(self, jitter_factor=factor)
A copy using a different jitter factor, between 0 and 1.
116 def wait_in_broker_from(self, threshold: timedelta) -> RetryPolicy: 117 """A copy that moves the line between waiting here and waiting there. 118 119 Zero is the way out: with no threshold nothing is long enough to reach 120 the broker, so every wait is spent in the consumer and no rung queue is 121 needed. That is the right setting for a service whose broker it may not 122 declare queues on, and the wrong one for a policy with delays measured 123 in minutes. 124 125 :param threshold: waits this long or longer go to a rung queue, or zero 126 for none of them 127 :returns: a new policy 128 """ 129 return replace(self, broker_wait_threshold=threshold)
A copy that moves the line between waiting here and waiting there.
Zero is the way out: with no threshold nothing is long enough to reach the broker, so every wait is spent in the consumer and no rung queue is needed. That is the right setting for a service whose broker it may not declare queues on, and the wrong one for a policy with delays measured in minutes.
Parameters
- threshold: waits this long or longer go to a rung queue, or zero for none of them :returns: a new policy
131 def waits_in_broker(self, delay: timedelta) -> bool: 132 """Whether a wait of this length belongs on a rung queue. 133 134 :param delay: an unjittered delay, as :meth:`schedule` reports them 135 :returns: whether the broker rather than the consumer should hold it 136 """ 137 return ( 138 self.broker_wait_threshold > ZERO 139 and delay > ZERO 140 and delay >= self.broker_wait_threshold 141 )
Whether a wait of this length belongs on a rung queue.
Parameters
- delay: an unjittered delay, as
schedule()reports them :returns: whether the broker rather than the consumer should hold it
143 def broker_rungs(self) -> list[timedelta]: 144 """The delays this policy needs a rung queue for, longest last. 145 146 Exactly the entries of :meth:`schedule` that are at or above the 147 threshold, with repeats removed — a fixed policy that waits a minute 148 three times needs one queue, not three. It is a finite list because the 149 schedule is, which is what makes the queues declarable up front rather 150 than conjured by a consumer at the moment it first fails. 151 152 :returns: one delay per queue, in the order the schedule reaches them 153 """ 154 return list(dict.fromkeys(d for d in self.schedule() if self.waits_in_broker(d)))
The delays this policy needs a rung queue for, longest last.
Exactly the entries of schedule() that are at or above the
threshold, with repeats removed — a fixed policy that waits a minute
three times needs one queue, not three. It is a finite list because the
schedule is, which is what makes the queues declarable up front rather
than conjured by a consumer at the moment it first fails.
:returns: one delay per queue, in the order the schedule reaches them
156 def next_delay(self, attempt: int, message_age: timedelta = ZERO) -> timedelta | None: 157 """How long to wait before the next attempt, or ``None`` to give up. 158 159 :param attempt: the attempt that has just failed, starting at 1 160 :param message_age: how old the message is 161 :returns: the delay, or ``None`` when there should be no further attempt 162 """ 163 wait = self.next_wait(attempt, message_age) 164 return None if wait is None else wait.delay
How long to wait before the next attempt, or None to give up.
Parameters
- attempt: the attempt that has just failed, starting at 1
- message_age: how old the message is
:returns: the delay, or
Nonewhen there should be no further attempt
166 def next_wait(self, attempt: int, message_age: timedelta = ZERO) -> Wait | None: 167 """The whole answer: how long to wait, and where. 168 169 :param attempt: the attempt that has just failed, starting at 1 170 :param message_age: how old the message is 171 :returns: the wait, or ``None`` when there should be no further attempt 172 """ 173 if attempt >= self.max_attempts: 174 return None 175 if self.max_message_age > ZERO and message_age >= self.max_message_age: 176 return None 177 178 delay = self._unjittered(attempt) 179 180 if self.waits_in_broker(delay): 181 # Deliberately not jittered. A rung queue's TTL is fixed when it is 182 # declared, so a moved delay would name a queue that does not exist; 183 # and the spread jitter buys is already there, because each message's 184 # TTL starts when it arrives rather than when the batch failed. 185 return Wait(delay, in_broker=True) 186 187 if self.jitter_factor > 0 and delay > ZERO: 188 # Both directions, so a fleet of consumers that failed together 189 # does not come back together. One-sided jitter only ever delays, 190 # which turns a thundering herd into a slower thundering herd. 191 factor = 1 + ((random.random() * 2 - 1) * self.jitter_factor) 192 delay = delay * factor 193 194 return Wait(max(delay, ZERO), in_broker=False)
The whole answer: how long to wait, and where.
Parameters
- attempt: the attempt that has just failed, starting at 1
- message_age: how old the message is
:returns: the wait, or
Nonewhen there should be no further attempt
208 def schedule(self) -> list[timedelta]: 209 """The delays this policy would use, without jitter. 210 211 What to look at when deciding whether a policy is the one you meant: 212 ``ExponentialRetry(5, 1s, 1m).schedule()`` is ``[1s, 2s, 4s, 8s]``, and 213 four numbers are easier to argue with than three parameters. 214 """ 215 delays: list[timedelta] = [] 216 delay = self.initial_delay 217 for _ in range(1, max(self.max_attempts, 0)): 218 capped = delay 219 if self.max_delay > ZERO and capped > self.max_delay: 220 capped = self.max_delay 221 delays.append(capped) 222 delay = delay * self.multiplier 223 return delays
The delays this policy would use, without jitter.
What to look at when deciding whether a policy is the one you meant:
ExponentialRetry(5, 1s, 1m).schedule() is [1s, 2s, 4s, 8s], and
four numbers are easier to argue with than three parameters.
226def no_retry() -> RetryPolicy: 227 """One delivery, no second chance.""" 228 return RetryPolicy(max_attempts=1)
One delivery, no second chance.
231def exponential_retry( 232 max_attempts: int, 233 initial_delay: timedelta, 234 max_delay: timedelta = ZERO, 235) -> RetryPolicy: 236 """Doubling delays with 20% jitter, which is the sane default. 237 238 :param max_attempts: total deliveries including the first 239 :param initial_delay: the wait before the second attempt 240 :param max_delay: the ceiling, or zero for none 241 """ 242 return RetryPolicy( 243 max_attempts=max_attempts, 244 initial_delay=initial_delay, 245 multiplier=2.0, 246 max_delay=max_delay, 247 jitter_factor=0.2, 248 )
Doubling delays with 20% jitter, which is the sane default.
Parameters
- max_attempts: total deliveries including the first
- initial_delay: the wait before the second attempt
- max_delay: the ceiling, or zero for none
251def fixed_retry(max_attempts: int, delay: timedelta) -> RetryPolicy: 252 """The same wait every time, with no jitter.""" 253 return RetryPolicy(max_attempts=max_attempts, initial_delay=delay, multiplier=1.0)
The same wait every time, with no jitter.