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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
//
// ferogram: async Telegram MTProto client in Rust
// https://github.com/ankit-chaubey/ferogram
//
// Licensed under either the MIT License or the Apache License 2.0.
// See the LICENSE-MIT or LICENSE-APACHE file in this repository:
// https://github.com/ankit-chaubey/ferogram
//
// Feel free to use, modify, and share this code.
// Please keep this notice when redistributing.
//! User-tunable transfer concurrency: how hard Ferogram is allowed to push
//! when uploading or downloading a file.
//!
//! Telegram's own file API docs describe two independent ways to speed up a
//! transfer: open more connections, and keep more than one request in
//! flight per connection. This module is Ferogram's implementation of both,
//! plus a global cap so a client running several transfers at once doesn't
//! open an unbounded number of sockets.
//!
//! ## The highway model
//!
//! It helps to picture a file transfer as trucks travelling on highways.
//!
//! - A **truck** is one in-flight chunk request: `upload.saveFilePart`,
//! `upload.saveBigFilePart`, or `upload.getFile`.
//! - The **load a truck carries** is the chunk size: how many bytes one
//! request moves. This is decided by file size alone (see the tables in
//! [`crate::media`]), not by anything in this module - it isn't part of
//! what you configure here, but it's worth naming since Y and X both
//! multiply against it to get actual throughput.
//! - The **length of the highway** is the round-trip time (RTT): how long
//! one truck takes to reach Telegram and come back with a response. This
//! is set by the network path between you and the datacenter, not by
//! Ferogram or by anything you configure - a highway to a nearby DC is
//! short, a highway to one on the other side of the world is long. You
//! cannot shorten the road. What you *can* do is put more trucks on it
//! at once, which is exactly what X is for.
//! - A **highway** is one dedicated MTProto connection (TCP socket) opened
//! just for this transfer. Trucks travel on a highway one after another,
//! but a highway can hold several trucks moving at once if you let it.
//! - **Y**, i.e. [`TransferLimits::download_tcp_connections`] and
//! [`TransferLimits::upload_tcp_connections`], is how many highways a
//! *single* transfer is allowed to build for itself. A small file might
//! only need one highway. A large file can split itself across several,
//! each one carrying a different slice of the file at the same time.
//! - **X**, i.e. [`TransferLimits::download_pipeline_depth`] and
//! [`TransferLimits::upload_pipeline_depth`], is how many trucks are
//! allowed on one highway at once. Without this, a connection sends one
//! truck, waits for it to travel the whole road and come back (one full
//! RTT), then sends the next - so on a long highway (high RTT), most of
//! the connection's time is spent watching an empty road instead of
//! moving trucks. With X > 1, several trucks are on the road at the same
//! time, so the road is never empty while a response is in transit.
//! - [`TransferLimits::max_tcp_connections`] is a city-wide speed limit on
//! *total* highways: every transfer's highways, upload and download
//! alike, draw from this one shared pool. A big download and a big
//! upload running at the same time both count against it.
//!
//! Put together: **total trucks in flight for one transfer = Y × X**, and
//! **total bytes in flight = Y × X × chunk size**. A transfer using 3
//! highways at a pipeline depth of 4 can have up to 12 chunks moving
//! simultaneously. That is the whole tuning surface - there
//! is no third secret dial. Everything Ferogram does to make a transfer
//! faster or slower comes down to moving one of these two numbers.
//!
//! ## Why Y and X are not the same knob
//!
//! It would be simpler to have one "concurrency" number, but Y and X cost
//! different things and help with different problems, so they are kept
//! separate on purpose:
//!
//! - **Y costs the server something.** Each additional highway is a real
//! socket and a real MTProto session Telegram has to track. Open too
//! many for one file and Telegram starts shedding connections with an
//! early-EOF instead of politely refusing - that is why Y has a hard
//! ceiling ([`media::MAX_WORKERS_PER_FILE`](crate::media::MAX_WORKERS_PER_FILE))
//! that no config value can exceed.
//! - **X costs the client something.** Every truck in flight is a chunk
//! buffer sitting in memory until its response arrives. Raising X only
//! costs you RAM, not server goodwill, so its ceiling
//! ([`media::MAX_PIPELINE_DEPTH`](crate::media::MAX_PIPELINE_DEPTH)) exists
//! to bound memory use, not to protect Telegram.
//! - **Y helps most when bandwidth is the bottleneck** - more parallel
//! sockets means more of your link's total bandwidth gets used at once.
//! - **X helps most when round-trip time is the bottleneck** - on a
//! high-latency link (mobile data, satellite, a far-away datacenter),
//! most of a single connection's time is spent waiting for a reply, and
//! pipelining is what fills that dead time with useful work.
//!
//! In practice most people never need to touch either: Ferogram already
//! picks sensible values per file size (see the size tables in
//! [`crate::media`]) and clamps everything to numbers Ankit has already
//! verified are safe. These fields exist for the minority of cases where
//! you know something about your own network or device that the size
//! heuristic can't see.
//!
//! ## Setting it up
//!
//! Most people should change nothing. If you do want to tune it, use the
//! [`ClientBuilder`](crate::builder::ClientBuilder) shorthands - each one
//! changes exactly one field and leaves the rest at their defaults:
//!
//! ```rust,no_run
//! use ferogram::Client;
//!
//! # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let (client, _sd) = Client::builder()
//! .api_id(12345)
//! .api_hash("abc")
//! // Slow, high-latency mobile link: fewer highways, deeper pipeline.
//! .download_tcp_connections(2)
//! .upload_tcp_connections(2)
//! .download_pipeline_depth(6)
//! .upload_pipeline_depth(6)
//! .connect().await?;
//! # Ok(()) }
//! ```
//!
//! Or set every field together with [`TransferLimits`] and
//! [`ClientBuilder::transfer_limits`](crate::builder::ClientBuilder::transfer_limits)
//! if you want to reason about the whole picture at once instead of one
//! field at a time. See the struct-level docs below for what each field
//! does and what it defaults to.
//!
//! [`TransferLimits::bypass_tcp_allotments`] is a separate, more drastic
//! switch: instead of letting file size decide Y (see the tables in
//! [`crate::media`]), it forces every transfer in that direction to use
//! your configured ceiling directly, no matter how small the file is. Read
//! its own docs and the responsibility section below carefully before
//! turning it on.
//!
//! ## Overriding defaults is your responsibility
//!
//! Ferogram's defaults were not picked at random. Ankit tuned them against
//! how Telegram's servers actually behave, not just what the docs say is
//! technically allowed. Every field in [`TransferLimits`] is still clamped
//! to a hard ceiling that Ankit controls, so you cannot accidentally
//! configure something that breaks the protocol outright. But *within*
//! those ceilings, Ferogram gives you real room to push harder than the
//! defaults, and if you do, you are the one deciding to trade safety
//! margin for speed. That trade is yours to make and yours to own -
//! Ferogram will not silently protect you from a choice you asked it to
//! make.
//!
//! The most common way this shows up is `FLOOD_WAIT`. Telegram enforces
//! per-account, per-DC rate limits that are not published anywhere and can
//! change without notice. Pushing Y and X higher, or turning on
//! [`bypass_tcp_allotments`](TransferLimits::bypass_tcp_allotments) so
//! every transfer opens its full ceiling of connections regardless of
//! size, means more simultaneous requests hitting the same account and DC.
//! Ferogram already handles an individual `FLOOD_WAIT` correctly - it backs
//! off for the time Telegram asks for and retries the affected chunk, so a
//! single transfer will not fail outright because of one flood wait. But
//! if you are seeing `FLOOD_WAIT` regularly, that is Telegram telling you,
//! directly, that the concurrency you configured is higher than it is
//! currently willing to tolerate for this account or DC.
//!
//! **If that happens, the fix is on your side, not Ferogram's: turn the
//! dial back down.** Reduce X first (pipeline depth), since it is the
//! cheaper adjustment and does not change how many sockets you hold open.
//! If flood waits continue, reduce Y (connections per file) next, and turn
//! off `bypass_tcp_allotments` if you had it on, so small files go back to
//! using only as many connections as they actually need. There is no
//! configuration that makes Telegram's rate limits go away - only one that
//! respects them. Ferogram and Ankit give you the maximum control the
//! protocol allows; using that control responsibly, and noticing when it
//! is time to back off, is on you.
//!
//! ## What's deliberately not here
//!
//! Chunk-size tiers, the big-file threshold, the free-tier part-count
//! ceiling, and the *absolute* per-file worker ceiling itself all follow
//! Telegram's wire-format rules and Ferogram's own empirically-tuned
//! defaults - see [`crate::media`]. Those stay constant. Getting them wrong
//! risks invalid requests or the server shedding connections with
//! early-EOF. [`TransferLimits`] only lets you move within the range that's
//! already known to be safe.
/// User-tunable ceilings for concurrent transfer connections.
///
/// Set via [`ClientBuilder::transfer_limits`](crate::builder::ClientBuilder::transfer_limits),
/// or the [`download_tcp_connections`](crate::builder::ClientBuilder::download_tcp_connections) /
/// [`upload_tcp_connections`](crate::builder::ClientBuilder::upload_tcp_connections) /
/// [`max_tcp_connections`](crate::builder::ClientBuilder::max_tcp_connections) /
/// [`download_pipeline_depth`](crate::builder::ClientBuilder::download_pipeline_depth) /
/// [`upload_pipeline_depth`](crate::builder::ClientBuilder::upload_pipeline_depth)
/// shorthands.
///
/// All fields are clamped on [`connect`](crate::Client::connect):
/// `download_tcp_connections` / `upload_tcp_connections`
/// never exceed [`media::MAX_WORKERS_PER_FILE`](crate::media::MAX_WORKERS_PER_FILE),
/// `download_pipeline_depth` / `upload_pipeline_depth` never exceed
/// [`media::MAX_PIPELINE_DEPTH`](crate::media::MAX_PIPELINE_DEPTH), and
/// `max_tcp_connections` is never allowed below 1. See the [module
/// docs](self) for the highway/trucks model this controls, and its
/// "Overriding defaults is your responsibility" section before pushing
/// any of these past their defaults.
///
/// # Example
/// ```rust,no_run
/// use ferogram::{Client, TransferLimits};
///
/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let (client, _sd) = Client::builder()
/// .api_id(12345)
/// .api_hash("abc")
/// .transfer_limits(TransferLimits {
/// download_tcp_connections: 2, // low-memory device: fewer highways per download
/// upload_tcp_connections: 4, // uploads keep the default
/// max_tcp_connections: 6, // fewer total sockets across the client
/// download_pipeline_depth: 2, // fewer trucks in flight per highway
/// upload_pipeline_depth: 2,
/// bypass_tcp_allotments: false,
/// })
/// .connect().await?;
/// # Ok(()) }
/// ```