1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Shared 429 retry-with-backoff helper for teloxide `Bot` requests.
//!
//! [`crate::common::http_retry`] provides this same resilience for the raw `reqwest`-based
//! REST clients (Discord, Slack, [`crate::telegram_api_ext::TelegramApiClient`]). `TelegramChannel`'s
//! primary send path instead goes through teloxide's typed `Bot` API (for `MarkdownV2` parsing,
//! message-id tracking, etc.), which surfaces rate-limiting as
//! [`teloxide::RequestError::RetryAfter`] rather than an HTTP 429 status — this module mirrors
//! `http_retry`'s backoff semantics for that error shape instead.
use Duration;
use ;
/// Upper bound applied to any `RetryAfter` duration reported by Telegram.
const MAX_RETRY_SECS: u64 = 60;
/// Maximum number of retry attempts before giving up and surfacing the error.
const MAX_RETRIES: u32 = 3;
/// Sends a teloxide request, retrying with backoff on [`teloxide::RequestError::RetryAfter`].
///
/// Uses [`Request::send_ref`] so the same request value is resent on each attempt rather than
/// rebuilding it. On any other error the failure is returned immediately.
///
/// `context` is a short label (e.g. `"telegram"`) attached to the warning logs.
///
/// # Timing
///
/// Under sustained rate-limiting, the worst-case wall-clock across all attempts is
/// approximately `MAX_RETRIES * MAX_RETRY_SECS` (the backoff sleeps between attempts), on the
/// order of minutes with the current constants — the same shape as
/// [`crate::common::http_retry::send_with_retry`]'s documented worst case. This is deliberate for
/// `TelegramChannel::send`/`flush_chunks` (the actual response content): a real reply is worth
/// retrying to completion rather than dropping. The one caller that cannot tolerate this,
/// `Channel::send_status` (an ephemeral status label with no value after a few seconds), is not
/// bounded here — `zeph_core::channel::Channel::send_status_best_effort` wraps the whole
/// `send_status` call (including any retry loop reached through this function) in its own much
/// shorter, separately-configured timeout at the `Channel` trait level instead of this module
/// applying one internally. Callers of `send`/`flush_chunks` (which do not go through
/// `send_status_best_effort`) intentionally have no outer timeout and may legitimately block for
/// the full worst case above.
///
/// # Errors
///
/// Returns the underlying [`teloxide::RequestError`] when a non-`RetryAfter` error occurs or
/// when retries are exhausted.
pub async