cover/Elixir.WalletIntegrations.RetryPolicy.html

1 defmodule WalletIntegrations.RetryPolicy do
2 @moduledoc """
3 Timeout, retry, and backoff configuration for outbound provider operations (ADR 0008).
4
5 Each operation class has distinct timeout bounds appropriate to its latency profile.
6 Retry is permitted only for transient/retryable failures.
7 """
8
9 # Operation classes and their timeout policy (milliseconds)
10 @policies %{
11 initiate_payment: %{
12 connect_timeout: 5_000,
13 recv_timeout: 15_000,
14 deadline: 20_000,
15 max_attempts: 3,
16 base_delay_ms: 1_000,
17 max_delay_ms: 30_000
18 },
19 get_payment_status: %{
20 connect_timeout: 3_000,
21 recv_timeout: 8_000,
22 deadline: 12_000,
23 max_attempts: 5,
24 base_delay_ms: 500,
25 max_delay_ms: 15_000
26 },
27 cancel_payment: %{
28 connect_timeout: 5_000,
29 recv_timeout: 10_000,
30 deadline: 15_000,
31 max_attempts: 2,
32 base_delay_ms: 2_000,
33 max_delay_ms: 10_000
34 },
35 refund_payment: %{
36 connect_timeout: 5_000,
37 recv_timeout: 15_000,
38 deadline: 20_000,
39 max_attempts: 2,
40 base_delay_ms: 2_000,
41 max_delay_ms: 20_000
42 }
43 }
44
45 # HTTP status codes that indicate a transient failure safe to retry
46 @retryable_status_codes [429, 500, 502, 503, 504]
47
48 @doc "Return the timeout/retry policy map for a given operation atom."
49 @spec for_operation(operation :: atom()) :: map()
50 def for_operation(operation) do
51 5 Map.get(@policies, operation, %{
52 connect_timeout: 5_000,
53 recv_timeout: 15_000,
54 deadline: 20_000,
55 max_attempts: 3,
56 base_delay_ms: 1_000,
57 max_delay_ms: 30_000
58 })
59 end
60
61 @doc """
62 Calculate exponential backoff with full jitter for a given attempt number.
63 Returns delay in milliseconds.
64 Formula: jitter(0, min(cap, base * 2^attempt))
65 """
66 @spec calculate_backoff(attempt :: non_neg_integer(), base_ms :: pos_integer(), cap_ms :: pos_integer()) ::
67 pos_integer()
68
:-(
def calculate_backoff(attempt, base_ms \\ 1_000, cap_ms \\ 30_000) do
69 35 ceiling = min(cap_ms, trunc(base_ms * :math.pow(2, attempt)))
70 # Full jitter: random in [0, ceiling] + 1 to ensure positive
71 35 :rand.uniform(max(ceiling, 1))
72 end
73
74 @doc "True when the HTTP status code indicates a transient, retryable failure."
75 @spec retryable_status_code?(code :: non_neg_integer()) :: boolean()
76 10 def retryable_status_code?(code), do: code in @retryable_status_codes
77
78 @doc "True when the error reason indicates a transient, retryable condition."
79 @spec retryable_error?(reason :: term()) :: boolean()
80 1 def retryable_error?(:timeout), do: true
81 1 def retryable_error?(:connect_timeout), do: true
82 1 def retryable_error?(:econnrefused), do: true
83 1 def retryable_error?(:closed), do: true
84 1 def retryable_error?({:exit, _}), do: true
85 3 def retryable_error?(_), do: false
86 end
Line Hits Source