Skip to main content

ferogram_mtsender/
sender_task.rs

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