ronami_core/adaptors/throttle.rs
1/// `ThrottlingRequest` and `ThrottlingSend` structures
2mod request;
3/// Lock that allows requests to wait until they are allowed to be sent
4mod request_lock;
5/// `impl Requester for Throttle<_>`
6mod requester_impl;
7/// `Settings` and `Limits` structures
8mod settings;
9/// "Worker" that checks the limits
10mod worker;
11
12use std::{
13 future::Future,
14 hash::{Hash, Hasher},
15};
16
17use tokio::sync::{
18 mpsc,
19 oneshot::{self},
20};
21
22use crate::{errors::AsResponseParameters, requests::Requester, types::*};
23
24use self::{
25 request_lock::{channel, RequestLock},
26 worker::{worker, FreezeUntil, InfoMessage},
27};
28
29pub use request::{ThrottlingRequest, ThrottlingSend};
30pub use settings::{Limits, Settings};
31
32/// Automatic request limits respecting mechanism.
33///
34/// Telegram has strict [limits], which, if exceeded will sooner or later cause
35/// `RequestError::RetryAfter(_)` errors. These errors can cause users of your
36/// bot to never receive responses from the bot or receive them in a wrong
37/// order.
38///
39/// This bot wrapper automatically checks for limits, suspending requests until
40/// they could be sent without exceeding limits (request order in chats is not
41/// changed).
42///
43/// It's recommended to use this wrapper before other wrappers (i.e.:
44/// `SomeWrapper<Throttle<Bot>>` not `Throttle<SomeWrapper<Bot>>`) because if
45/// done otherwise inner wrappers may cause `Throttle` to miscalculate limits
46/// usage.
47///
48/// [limits]: https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this
49///
50/// ## Examples
51///
52/// ```no_run (throttle fails to spawn task without tokio runtime)
53/// use ronami_core::{adaptors::throttle::Limits, requests::RequesterExt, Bot};
54///
55/// let bot = Bot::new("TOKEN")
56/// .throttle(Limits::default());
57///
58/// /* send many requests here */
59/// ```
60///
61/// ## Note about send-by-@channelusername
62///
63/// Telegram have limits on sending messages to _the same chat_. To check them
64/// we store `chat_id`s of several last requests. _However_ there is no good way
65/// to tell if given `ChatId::Id(x)` corresponds to the same chat as
66/// `ChatId::ChannelUsername(u)`.
67///
68/// Our current approach is to just give up and check `chat_id_a == chat_id_b`.
69/// This may give incorrect results.
70///
71/// As such, we encourage not to use `ChatId::ChannelUsername(u)` with this bot
72/// wrapper.
73#[derive(Clone, Debug)]
74pub struct Throttle<B> {
75 bot: B,
76 // `RequestLock` allows to unlock requests (allowing them to be sent).
77 queue: mpsc::Sender<(ChatIdHash, RequestLock)>,
78 info_tx: mpsc::Sender<InfoMessage>,
79}
80
81impl<B> Throttle<B> {
82 /// Creates new [`Throttle`] alongside with worker future.
83 ///
84 /// Note: [`Throttle`] will only send requests if returned worker is
85 /// polled/spawned/awaited.
86 pub fn new(bot: B, limits: Limits) -> (Self, impl Future<Output = ()>)
87 where
88 B: Requester + Clone,
89 B::Err: AsResponseParameters,
90 {
91 let settings = Settings { limits, ..<_>::default() };
92 Self::with_settings(bot, settings)
93 }
94
95 /// Creates new [`Throttle`] alongside with worker future.
96 ///
97 /// Note: [`Throttle`] will only send requests if returned worker is
98 /// polled/spawned/awaited.
99 pub fn with_settings(bot: B, settings: Settings) -> (Self, impl Future<Output = ()>)
100 where
101 B: Requester + Clone,
102 B::Err: AsResponseParameters,
103 {
104 let (tx, rx) = mpsc::channel(settings.limits.messages_per_sec_overall as usize);
105 let (info_tx, info_rx) = mpsc::channel(2);
106
107 let worker = worker(settings, rx, info_rx, bot.clone());
108 let this = Self { bot, queue: tx, info_tx };
109
110 (this, worker)
111 }
112
113 /// Creates new [`Throttle`] spawning the worker with `tokio::spawn`
114 ///
115 /// Note: it's recommended to use [`RequesterExt::throttle`] instead.
116 ///
117 /// [`RequesterExt::throttle`]: crate::requests::RequesterExt::throttle
118 pub fn new_spawn(bot: B, limits: Limits) -> Self
119 where
120 B: Requester + Clone + Send + Sync + 'static,
121 B::Err: AsResponseParameters,
122 B::GetChat: Send,
123 {
124 let (this, worker) = Self::new(bot, limits);
125
126 tokio::spawn(worker);
127
128 this
129 }
130
131 /// Creates new [`Throttle`] spawning the worker with `tokio::spawn`
132 pub fn spawn_with_settings(bot: B, settings: Settings) -> Self
133 where
134 B: Requester + Clone + Send + Sync + 'static,
135 B::Err: AsResponseParameters,
136 B::GetChat: Send,
137 {
138 let (this, worker) = Self::with_settings(bot, settings);
139
140 tokio::spawn(worker);
141 this
142 }
143
144 /// Allows to access inner bot
145 pub fn inner(&self) -> &B {
146 &self.bot
147 }
148
149 /// Unwraps inner bot
150 pub fn into_inner(self) -> B {
151 self.bot
152 }
153
154 /// Returns currently used [`Limits`].
155 pub async fn limits(&self) -> Limits {
156 const WORKER_DIED: &str = "worker died before last `Throttle` instance";
157
158 let (tx, rx) = oneshot::channel();
159
160 self.info_tx.send(InfoMessage::GetLimits { response: tx }).await.expect(WORKER_DIED);
161
162 rx.await.expect(WORKER_DIED)
163 }
164
165 /// Sets new limits.
166 ///
167 /// Note: changes may not be applied immediately.
168 pub async fn set_limits(&self, new: Limits) {
169 let (tx, rx) = oneshot::channel();
170
171 self.info_tx.send(InfoMessage::SetLimits { new, response: tx }).await.ok();
172
173 rx.await.ok();
174 }
175}
176
177/// An ID used in the worker.
178///
179/// It is used instead of `ChatId` to make copying cheap even in case of
180/// usernames. (It is just a hashed username.)
181#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
182enum ChatIdHash {
183 Id(ChatId),
184 ChannelUsernameHash(u64),
185}
186
187impl ChatIdHash {
188 fn is_channel_or_supergroup(&self) -> bool {
189 match self {
190 &Self::Id(id) => id.is_channel_or_supergroup(),
191 Self::ChannelUsernameHash(_) => true,
192 }
193 }
194}
195
196impl From<&ChatId> for ChatIdHash {
197 fn from(value: &ChatId) -> Self {
198 ChatIdHash::Id(*value)
199 }
200}
201
202impl From<&Recipient> for ChatIdHash {
203 fn from(value: &Recipient) -> Self {
204 match value {
205 Recipient::Id(id) => ChatIdHash::Id(*id),
206 Recipient::ChannelUsername(username) => {
207 // FIXME: this could probably use a faster hasher, `DefaultHasher` is known to
208 // be slow (it's not like we _need_ this to be fast, but still)
209 let mut hasher = std::collections::hash_map::DefaultHasher::new();
210 username.hash(&mut hasher);
211 let hash = hasher.finish();
212 ChatIdHash::ChannelUsernameHash(hash)
213 }
214 }
215 }
216}