Skip to main content

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}