ferogram_mtsender/sender_task.rs
1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15//! The sender task: a single `tokio::spawn`-ed loop that owns [`MtpSender`]
16//! and is the only entity that touches the TCP socket.
17//!
18//! External callers interact via two channels:
19//!
20//! - [`RpcEnqueue`]: send a pre-serialised TL body + oneshot::Sender to the task.
21//! The task enqueues it into `MtpSender`, and the oneshot is fulfilled when the
22//! server responds. This replaces the old `do_rpc_call` + `Mutex<ConnectionWriter>`
23//! + `pending` HashMap pattern.
24//!
25//! - [`ReconnectRequest`]: send a new `(TcpStream, EncryptedSession, FrameKind,
26//! Option<perm_key>)` to the task after a reconnect completes. The task calls
27//! `MtpSender::set_stream` and resumes the loop.
28//!
29//! The task forwards raw update bodies (everything `MtpSender::step()` returns
30//! that is not an rpc_result) via [`FrameEvent`] to the client's dispatch path.
31
32use std::sync::Arc;
33use std::sync::atomic::AtomicBool;
34
35use ferogram_connect::FrameKind;
36use ferogram_mtproto::EncryptedSession;
37use tokio::net::TcpStream;
38use tokio::sync::{mpsc, oneshot};
39
40use crate::errors::InvocationError;
41use crate::mtp_sender::MtpSender;
42
43/// A single RPC request sent from any caller to the sender task.
44pub struct RpcEnqueue {
45 /// Pre-serialised TL body (output of `EncryptedSession::pack_body_with_msg_id`
46 /// or any raw TL bytes; the sender task will re-encrypt via MtpSender).
47 pub body: Vec<u8>,
48 /// Fulfilled with the raw rpc_result body (or an error) when the server responds.
49 pub tx: oneshot::Sender<Result<Vec<u8>, InvocationError>>,
50}
51
52/// Reconnect request: replace the TCP stream inside the sender task.
53pub struct ReconnectRequest {
54 pub stream: TcpStream,
55 pub enc: EncryptedSession,
56 pub frame_kind: FrameKind,
57 pub perm_auth_key: Option<[u8; 256]>,
58}
59
60/// Events the sender task sends back to the client.
61pub enum FrameEvent {
62 /// A raw update body (Updates, UpdateShort, etc.) to dispatch.
63 Update(Vec<u8>),
64 /// The connection failed; the client must reconnect and send a ReconnectRequest.
65 Error(InvocationError),
66 /// Session info after initial connect or reconnect (for session saving).
67 Connected {
68 auth_key: Box<[u8; 256]>,
69 first_salt: i64,
70 time_offset: i32,
71 session_id: i64,
72 },
73}
74
75/// Sender-side handles given to the client after spawning the sender task.
76pub struct SenderHandle {
77 /// Enqueue RPC requests here.
78 pub rpc_tx: mpsc::Sender<RpcEnqueue>,
79 /// Send a new stream here after reconnect.
80 pub reconnect_tx: mpsc::Sender<ReconnectRequest>,
81}
82
83/// Spawn the sender task. Returns a [`SenderHandle`] for the client and an
84/// `mpsc::Receiver<FrameEvent>` for receiving update bodies and errors.
85pub fn spawn_sender_task(
86 stream: TcpStream,
87 enc: EncryptedSession,
88 frame_kind: FrameKind,
89 perm_auth_key: Option<[u8; 256]>,
90) -> (SenderHandle, mpsc::Receiver<FrameEvent>) {
91 let (rpc_tx, rpc_rx) = mpsc::channel::<RpcEnqueue>(512);
92 let (reconnect_tx, reconnect_rx) = mpsc::channel::<ReconnectRequest>(4);
93 let (frame_tx, frame_rx) = mpsc::channel::<FrameEvent>(256);
94
95 let sender = MtpSender::new(stream, enc, frame_kind, perm_auth_key);
96
97 tokio::spawn(sender_loop(sender, rpc_rx, reconnect_rx, frame_tx));
98
99 (
100 SenderHandle {
101 rpc_tx,
102 reconnect_tx,
103 },
104 frame_rx,
105 )
106}
107
108async fn sender_loop(
109 mut sender: MtpSender,
110 mut rpc_rx: mpsc::Receiver<RpcEnqueue>,
111 mut reconnect_rx: mpsc::Receiver<ReconnectRequest>,
112 frame_tx: mpsc::Sender<FrameEvent>,
113) {
114 // Notify the client that we are connected and ready.
115 let _ = frame_tx
116 .send(FrameEvent::Connected {
117 auth_key: Box::new(sender.auth_key_bytes()),
118 first_salt: sender.first_salt(),
119 time_offset: sender.time_offset(),
120 session_id: sender.session_id(),
121 })
122 .await;
123
124 loop {
125 // Drain all pending RPC enqueues before stepping (non-blocking).
126 loop {
127 match rpc_rx.try_recv() {
128 Ok(enqueue) => sender.enqueue(enqueue.body, enqueue.tx),
129 Err(mpsc::error::TryRecvError::Empty) => break,
130 Err(mpsc::error::TryRecvError::Disconnected) => {
131 // Client dropped all handles: shut down cleanly.
132 return;
133 }
134 }
135 }
136
137 tokio::select! {
138 biased;
139
140 // New RPC enqueue arrived while we were waiting in step().
141 Some(enqueue) = rpc_rx.recv() => {
142 sender.enqueue(enqueue.body, enqueue.tx);
143 // Loop back immediately so step() can send it.
144 continue;
145 }
146
147 // Reconnect request: swap the stream.
148 Some(req) = reconnect_rx.recv() => {
149 tracing::info!("[ferogram::sender] reconnect: new stream received, swapping");
150 sender.set_stream(req.stream, req.enc, req.frame_kind, req.perm_auth_key);
151 let _ = frame_tx
152 .send(FrameEvent::Connected {
153 auth_key: Box::new(sender.auth_key_bytes()),
154 first_salt: sender.first_salt(),
155 time_offset: sender.time_offset(),
156 session_id: sender.session_id(),
157 })
158 .await;
159 continue;
160 }
161
162 // Drive one network event.
163 result = sender.step() => {
164 match result {
165 Ok(updates) => {
166 for body in updates {
167 if frame_tx.send(FrameEvent::Update(body)).await.is_err() {
168 // Client gone.
169 return;
170 }
171 }
172 }
173 Err(e) => {
174 tracing::warn!("[ferogram::sender] connection error, failing pending requests and waiting for reconnect: {e}");
175 // Fail all pending requests immediately.
176 sender.fail_all(&e);
177 // Notify the client; it will reconnect and send ReconnectRequest.
178 if frame_tx.send(FrameEvent::Error(e)).await.is_err() {
179 return;
180 }
181 // Wait for a reconnect before driving step() again.
182 match reconnect_rx.recv().await {
183 Some(req) => {
184 tracing::info!("[ferogram::sender] reconnect received, resuming send loop");
185 sender.set_stream(
186 req.stream,
187 req.enc,
188 req.frame_kind,
189 req.perm_auth_key,
190 );
191 // Drain RPCs that queued up in rpc_rx while we were
192 // waiting for reconnect. They were submitted against
193 // the dead session and must not go out before
194 // initConnection on the new one. Fail them so callers
195 // resubmit after seeing FrameEvent::Connected.
196 while let Ok(stale) = rpc_rx.try_recv() {
197 let _ = stale.tx.send(Err(InvocationError::Dropped));
198 }
199 let _ = frame_tx
200 .send(FrameEvent::Connected {
201 auth_key: Box::new(sender.auth_key_bytes()),
202 first_salt: sender.first_salt(),
203 time_offset: sender.time_offset(),
204 session_id: sender.session_id(),
205 })
206 .await;
207 }
208 None => {
209 // Client dropped reconnect handle: shut down.
210 return;
211 }
212 }
213 }
214 }
215 }
216 }
217 }
218}
219
220/// A pipelined transfer connection: multiple chunk requests can be enqueued
221/// and in flight simultaneously, instead of a blocking one-at-a-time model.
222///
223/// Backed by a background sender task (see [`spawn_pipelined`]) that owns
224/// the socket; this struct is just a cheap handle (an mpsc sender + a
225/// liveness flag) and can be cloned freely if multiple call sites need to
226/// share one pipelined connection.
227#[derive(Clone)]
228pub struct PipelinedSender {
229 rpc_tx: mpsc::Sender<RpcEnqueue>,
230 /// Flipped to `false` by the background drain task once the sender
231 /// task reports a connection error. Callers check this after a failed
232 /// `enqueue` to decide whether to open a fresh `PipelinedSender` rather
233 /// than keep retrying on a dead connection.
234 alive: Arc<AtomicBool>,
235}
236
237impl PipelinedSender {
238 /// `true` if the underlying sender task is still running. Does not
239 /// guarantee the *next* request will succeed (the connection could die
240 /// between this check and the next `enqueue`), but is enough to decide
241 /// whether to keep using this sender or fall back to opening a new one.
242 pub fn is_alive(&self) -> bool {
243 self.alive.load(std::sync::atomic::Ordering::Acquire)
244 }
245
246 /// Enqueue a pre-serialised TL request body and return a future that
247 /// resolves when the server responds. Does **not** wait for the
248 /// response itself - callers can enqueue several of these before
249 /// awaiting any of them, which is exactly what gives this connection
250 /// X > 1 (multiple chunk requests in flight at once on one socket).
251 ///
252 /// Returns an error immediately if the sender task has already shut
253 /// down (e.g. the connection died); otherwise returns a future that
254 /// resolves to the eventual RPC result or a connection-failure error.
255 pub async fn enqueue(
256 &self,
257 body: Vec<u8>,
258 ) -> Result<
259 impl std::future::Future<Output = Result<Vec<u8>, InvocationError>> + Send + use<>,
260 InvocationError,
261 > {
262 let (tx, rx) = oneshot::channel();
263 self.rpc_tx
264 .send(RpcEnqueue { body, tx })
265 .await
266 .map_err(|_| InvocationError::Deserialize("pipelined sender task shut down".into()))?;
267 Ok(async move {
268 rx.await
269 .map_err(|_| InvocationError::Deserialize("pipelined rpc channel closed".into()))?
270 })
271 }
272
273 /// Enqueue and immediately await a single request - convenience for
274 /// call sites that don't need explicit pipelining (e.g. the final part
275 /// of a transfer, or error-recovery paths).
276 pub async fn call(&self, body: Vec<u8>) -> Result<Vec<u8>, InvocationError> {
277 self.enqueue(body).await?.await
278 }
279}
280
281/// Spawn the sender task for an already-connected DC and wrap it as a
282/// [`PipelinedSender`]: X > 1 chunk requests can be enqueued and in flight
283/// simultaneously on the one socket. This is the "X pieces in flight" half
284/// of Telegram's documented upload/download performance recommendation
285/// (the worker-count axis is "Y queues", handled by the caller opening
286/// several of these).
287///
288/// Reconnect is not supported on pipelined transfer connections: on failure
289/// the caller's retry loop should open a fresh `PipelinedSender` from
290/// scratch, same as it already does for `DcConnection` failures.
291pub fn spawn_pipelined(
292 stream: TcpStream,
293 enc: EncryptedSession,
294 frame_kind: FrameKind,
295 perm_auth_key: Option<[u8; 256]>,
296) -> PipelinedSender {
297 let (handle, mut frame_rx) = spawn_sender_task(stream, enc, frame_kind, perm_auth_key);
298
299 // Dropping reconnect_tx lets the sender task shut down cleanly on error
300 // instead of waiting for a reconnect that will never come.
301 drop(handle.reconnect_tx);
302
303 let alive = Arc::new(AtomicBool::new(true));
304 let alive_for_drain = alive.clone();
305 tokio::spawn(async move {
306 while let Some(event) = frame_rx.recv().await {
307 if let FrameEvent::Error(e) = event {
308 tracing::debug!("[ferogram-mtsender] pipelined worker conn dropped: {e}");
309 alive_for_drain.store(false, std::sync::atomic::Ordering::Release);
310 break;
311 }
312 // Update / Connected events: transfer connections don't
313 // dispatch updates, nothing to do with them.
314 }
315 });
316
317 PipelinedSender {
318 rpc_tx: handle.rpc_tx,
319 alive,
320 }
321}