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}