Skip to main content

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}