Skip to main content

netget/server/gearman/
mod.rs

1//! Gearman job server — the model is the worker for every function.
2//!
3//! Clients speak the binary protocol (`\0REQ` packets) and administrators the text protocol
4//! (`status`, `workers`, …); a leading `\0` says which one the next message is, so a connection
5//! may mix them. A submitted job gets a NetGet-generated handle in `JOB_CREATED` at once, then
6//! raises `gearman_job_submitted`; the model's answer becomes `WORK_STATUS`/`WORK_DATA` and one
7//! outcome packet.
8//!
9//! Five properties worth knowing before changing the loop:
10//!
11//! 1. **Sizes are judged before anything is allocated.** A packet's declared size is checked
12//!    against [`wire::MAX_PACKET_BYTES`] from its 12-byte header; an admin line is capped at
13//!    [`wire::MAX_ADMIN_LINE`]. Either overrun is refused and the connection closed.
14//! 2. **The model cannot write a packet, and cannot answer another job.** Its answers are
15//!    rendered by [`wire`] with the handle of the job being answered; an answer that names
16//!    another handle, or a packet that is not a job answer, is refused. A foreground job always
17//!    ends with exactly one outcome: if the model gave none, NetGet sends `WORK_FAIL`.
18//! 3. **Worker connections are refused, not half-served.** `CAN_DO`, `GRAB_JOB` and the rest
19//!    get `ERROR not_supported` and the connection closes: the model is the worker, and a real
20//!    worker that registered here would sleep forever waiting for a job NetGet never queues.
21//! 4. **The deadlines wrap reads, not answers.** A client waiting for its job's result — the
22//!    model thinking, or the event parked for a human — is not idle.
23//! 5. **NetGet keeps only the jobs in flight** (handle, function, last progress), for
24//!    `GET_STATUS` and the admin `status`; a job leaves the table when its answer is written.
25pub mod actions;
26pub mod wire;
27
28use crate::llm::action_helper::call_llm;
29use crate::llm::actions::protocol_trait::ActionResult;
30use crate::llm::ollama_client::OllamaClient;
31use crate::logging::emit::Log;
32use crate::protocol::Event;
33use crate::server::connection::ConnectionId;
34use crate::state::app_state::AppState;
35use actions::Work;
36use anyhow::Result;
37use std::collections::BTreeMap;
38use std::net::SocketAddr;
39use std::sync::atomic::{AtomicU64, Ordering};
40use std::sync::Arc;
41use std::time::Duration;
42use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
43use tokio::net::TcpListener;
44use tokio::sync::{mpsc, Mutex};
45
46pub use wire::MAX_PACKET_BYTES;
47
48/// How long a new connection may send nothing. The `gearman` CLI writes its request at once;
49/// 300 s is the window a `manual` rule gives a human, for a NetGet TCP client parked on its
50/// operator. Lower it through `first_byte_timeout_secs` for a public listener.
51const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(300);
52
53/// How long the server waits for the next message after answering one, and for the rest of a
54/// message that has started arriving.
55const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
56
57/// Concurrent connections admitted before new ones are refused — the house default.
58const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
59
60/// Fixed `WORK_EXCEPTION` texts for a backend failure, for clients that enabled exceptions.
61const EXCEPTION_UNAVAILABLE: &[u8] = b"job server backend unavailable";
62const EXCEPTION_OVERLOADED: &[u8] = b"job server backend at capacity";
63
64/// A job the model is working on.
65#[derive(Debug, Clone)]
66struct InFlight {
67    function: String,
68    numerator: u64,
69    denominator: u64,
70}
71
72/// The jobs in flight on one server, by handle, and the handle counter.
73#[derive(Default)]
74struct Jobs {
75    next: AtomicU64,
76    table: std::sync::Mutex<BTreeMap<Vec<u8>, InFlight>>,
77}
78
79impl Jobs {
80    fn start(&self, function: &str) -> Vec<u8> {
81        let n = self.next.fetch_add(1, Ordering::SeqCst) + 1;
82        let handle = format!("H:netget:{n}").into_bytes();
83        self.table.lock().unwrap().insert(
84            handle.clone(),
85            InFlight {
86                function: function.to_string(),
87                numerator: 0,
88                denominator: 0,
89            },
90        );
91        handle
92    }
93    fn finish(&self, handle: &[u8]) {
94        self.table.lock().unwrap().remove(handle);
95    }
96    fn get(&self, handle: &[u8]) -> Option<InFlight> {
97        self.table.lock().unwrap().get(handle).cloned()
98    }
99    /// `(function, total, running, available workers)` for the admin `status`.
100    fn by_function(&self) -> Vec<(String, u64, u64, u64)> {
101        let mut counts: BTreeMap<String, u64> = BTreeMap::new();
102        for job in self.table.lock().unwrap().values() {
103            *counts.entry(job.function.clone()).or_default() += 1;
104        }
105        counts.into_iter().map(|(f, n)| (f, n, n, 0)).collect()
106    }
107}
108
109pub struct GearmanServer;
110
111impl GearmanServer {
112    pub async fn spawn_with_llm_actions(
113        listen_addr: SocketAddr,
114        llm_client: OllamaClient,
115        app_state: Arc<AppState>,
116        status_tx: mpsc::UnboundedSender<String>,
117        server_id: crate::state::ServerId,
118        first_byte_timeout_secs: Option<u64>,
119        idle_timeout_secs: Option<u64>,
120    ) -> Result<SocketAddr> {
121        let first_timeout = first_byte_timeout_secs
122            .map(Duration::from_secs)
123            .unwrap_or(FIRST_BYTE_TIMEOUT);
124        let idle_timeout = idle_timeout_secs
125            .map(Duration::from_secs)
126            .unwrap_or(IDLE_TIMEOUT);
127        let listener = TcpListener::bind(listen_addr).await?;
128        let local_addr = listener.local_addr()?;
129
130        Log::new(Some(&status_tx)).info(format!("Gearman job server listening on {}", local_addr));
131
132        let protocol = Arc::new(actions::GearmanProtocol::new());
133        let jobs = Arc::new(Jobs::default());
134        let task_registrar = app_state.clone();
135        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
136        let accept_handle = tokio::spawn(async move {
137            loop {
138                // A peer over the cap gets no bytes: a packet it did not ask for would be read
139                // as the answer to its first request.
140                match crate::server::accept_bounded::accept_bounded(
141                    &listener,
142                    &limiter,
143                    b"",
144                    "Gearman",
145                    Some(&status_tx),
146                )
147                .await
148                {
149                    Ok((socket, peer_addr, permit)) => {
150                        let connection_id =
151                            ConnectionId::new(app_state.get_next_unified_id().await);
152                        use crate::state::server::{
153                            ConnectionState as ServerConnectionState, ConnectionStatus,
154                            ProtocolConnectionInfo,
155                        };
156                        let now = crate::utils::clock::Instant::now();
157                        app_state
158                            .add_connection_to_server(
159                                server_id,
160                                ServerConnectionState {
161                                    id: connection_id,
162                                    remote_addr: peer_addr,
163                                    local_addr,
164                                    bytes_sent: 0,
165                                    bytes_received: 0,
166                                    packets_sent: 0,
167                                    packets_received: 0,
168                                    last_activity: now,
169                                    status: ConnectionStatus::Active,
170                                    status_changed_at: now,
171                                    protocol_info: ProtocolConnectionInfo::empty(),
172                                },
173                            )
174                            .await;
175                        let _ = status_tx.send("__UPDATE_UI__".to_string());
176                        Log::new(Some(&status_tx))
177                            .info(format!("Gearman client connected from {}", peer_addr));
178
179                        let session = Session {
180                            peer_addr,
181                            llm_client: llm_client.clone(),
182                            app_state: app_state.clone(),
183                            status_tx: status_tx.clone(),
184                            server_id,
185                            protocol: protocol.clone(),
186                            connection_id,
187                            first_timeout,
188                            idle_timeout,
189                            jobs: jobs.clone(),
190                        };
191                        // Tracked, not detached: stop_server must abort this task too.
192                        let task_owner = app_state.clone();
193                        task_owner
194                            .spawn_server_task(server_id, async move {
195                                let _permit = permit;
196                                session.run(socket).await
197                            })
198                            .await;
199                    }
200                    Err(e) => {
201                        Log::new(Some(&status_tx)).error(format!("Gearman accept error: {}", e));
202                        break;
203                    }
204                }
205            }
206        });
207
208        task_registrar
209            .register_server_task(server_id, accept_handle)
210            .await;
211        Ok(local_addr)
212    }
213}
214
215type Writer = Arc<Mutex<tokio::io::WriteHalf<tokio::net::TcpStream>>>;
216
217struct Session {
218    peer_addr: SocketAddr,
219    llm_client: OllamaClient,
220    app_state: Arc<AppState>,
221    status_tx: mpsc::UnboundedSender<String>,
222    server_id: crate::state::ServerId,
223    protocol: Arc<actions::GearmanProtocol>,
224    connection_id: ConnectionId,
225    first_timeout: Duration,
226    idle_timeout: Duration,
227    jobs: Arc<Jobs>,
228}
229
230/// One message read from the connection.
231enum Message {
232    Packet(wire::Header, Vec<u8>),
233    Admin(String),
234}
235
236enum ReadError {
237    Eof,
238    TimedOut,
239    TooLarge(u32),
240    BadMagic,
241    LineTooLong,
242    Io(std::io::Error),
243}
244
245impl Session {
246    async fn run(self, socket: tokio::net::TcpStream) {
247        let (mut reader, write_half) = tokio::io::split(socket);
248        let writer: Writer = Arc::new(Mutex::new(write_half));
249
250        let peer_rx = crate::server::peer_support::register_peer_channel(
251            &self.app_state,
252            self.server_id,
253            self.connection_id.as_u32(),
254        )
255        .await;
256        crate::server::peer_support::spawn_peer_command_task(
257            peer_rx,
258            self.protocol.clone(),
259            self.app_state.clone(),
260            self.server_id,
261            self.connection_id.as_u32(),
262            writer.clone(),
263            self.status_tx.clone(),
264        );
265
266        {
267            let mut framer = Framer::new(&mut reader);
268            self.session(&mut framer, &writer).await;
269        }
270
271        self.app_state
272            .remove_peer_handle(self.server_id, self.connection_id.as_u32())
273            .await;
274        let _ = writer.lock().await.shutdown().await;
275        linger(&mut reader).await;
276        self.app_state
277            .update_connection_status(
278                self.server_id,
279                self.connection_id,
280                crate::state::server::ConnectionStatus::Closed,
281            )
282            .await;
283        let _ = self.status_tx.send("__UPDATE_UI__".to_string());
284    }
285
286    async fn write(&self, writer: &Writer, data: &[u8]) -> bool {
287        let ok = {
288            let mut w = writer.lock().await;
289            w.write_all(data).await.is_ok() && w.flush().await.is_ok()
290        };
291        if ok {
292            self.app_state
293                .update_connection_stats(
294                    self.server_id,
295                    self.connection_id,
296                    None,
297                    Some(data.len() as u64),
298                    None,
299                    Some(1),
300                )
301                .await;
302        }
303        ok
304    }
305
306    async fn error(&self, writer: &Writer, code: &str, text: &str) -> bool {
307        match wire::error(code, text) {
308            Ok(packet) => self.write(writer, &packet).await,
309            Err(_) => false,
310        }
311    }
312
313    async fn session<R: AsyncRead + Unpin>(&self, framer: &mut Framer<'_, R>, writer: &Writer) {
314        let log = Log::new(Some(&self.status_tx));
315        let mut exceptions = false;
316        let mut first = true;
317
318        loop {
319            let timeout = if first {
320                self.first_timeout
321            } else {
322                self.idle_timeout
323            };
324            let message = match framer.next_message(timeout, self.idle_timeout).await {
325                Ok(m) => m,
326                Err(ReadError::Eof) => {
327                    log.info(format!("Gearman client {} disconnected", self.peer_addr));
328                    return;
329                }
330                Err(ReadError::TimedOut) => {
331                    log.info(format!(
332                        "Gearman client {} sent nothing within {}s; closing",
333                        self.peer_addr,
334                        timeout.as_secs()
335                    ));
336                    return;
337                }
338                Err(ReadError::TooLarge(size)) => {
339                    log.warn(format!(
340                        "Gearman packet from {} declared {} bytes, over {} \
341                         decision=fail_closed_too_large",
342                        self.peer_addr,
343                        size,
344                        wire::MAX_PACKET_BYTES
345                    ));
346                    self.error(
347                        writer,
348                        "too_large",
349                        "packet exceeds the server's size limit",
350                    )
351                    .await;
352                    return;
353                }
354                Err(ReadError::BadMagic) => {
355                    log.warn(format!(
356                        "Gearman peer {} sent a packet without \\0REQ decision=fail_closed_bad_magic",
357                        self.peer_addr
358                    ));
359                    return;
360                }
361                Err(ReadError::LineTooLong) => {
362                    log.warn(format!(
363                        "Gearman admin line from {} exceeded {} bytes \
364                         decision=fail_closed_line_too_long",
365                        self.peer_addr,
366                        wire::MAX_ADMIN_LINE
367                    ));
368                    let _ = self
369                        .write(
370                            writer,
371                            wire::admin_error("LINE_TOO_LONG", "Command line too long").as_bytes(),
372                        )
373                        .await;
374                    return;
375                }
376                Err(ReadError::Io(e)) => {
377                    log.error(format!("Gearman read error from {}: {}", self.peer_addr, e));
378                    return;
379                }
380            };
381            first = false;
382
383            let keep_going = match message {
384                Message::Admin(line) => self.admin(&line, writer).await,
385                Message::Packet(header, data) => {
386                    self.app_state
387                        .update_connection_stats(
388                            self.server_id,
389                            self.connection_id,
390                            Some((wire::HEADER_LEN + data.len()) as u64),
391                            None,
392                            Some(1),
393                            None,
394                        )
395                        .await;
396                    self.packet(&header, &data, &mut exceptions, writer).await
397                }
398            };
399            if !keep_going {
400                return;
401            }
402        }
403    }
404
405    /// Answer one admin line. `false` closes the connection.
406    async fn admin(&self, line: &str, writer: &Writer) -> bool {
407        let log = Log::new(Some(&self.status_tx));
408        if line.trim().is_empty() {
409            return true;
410        }
411        let reply = match wire::parse_admin(line) {
412            wire::AdminCommand::Status => wire::admin_status(&self.jobs.by_function()),
413            wire::AdminCommand::Workers => ".\n".to_string(),
414            wire::AdminCommand::Version => {
415                format!("OK netget-{}\n", env!("CARGO_PKG_VERSION"))
416            }
417            wire::AdminCommand::MaxQueue | wire::AdminCommand::Shutdown => {
418                log.warn(format!(
419                    "Gearman admin {:?} from {} refused decision=fail_closed_admin_write",
420                    line.trim(),
421                    self.peer_addr
422                ));
423                wire::admin_error("NOT_SUPPORTED", "This server does not change its state")
424            }
425            wire::AdminCommand::Unknown => {
426                wire::admin_error("UNKNOWN_COMMAND", "Unknown server command")
427            }
428        };
429        self.write(writer, reply.as_bytes()).await
430    }
431
432    /// Answer one binary packet. `false` closes the connection.
433    async fn packet(
434        &self,
435        header: &wire::Header,
436        data: &[u8],
437        exceptions: &mut bool,
438        writer: &Writer,
439    ) -> bool {
440        let log = Log::new(Some(&self.status_tx));
441        let request = match wire::parse_request(header, data) {
442            Ok(r) => r,
443            Err(e) => {
444                log.warn(format!(
445                    "Gearman packet type {} from {}: {:?} decision=fail_closed_bad_arguments",
446                    header.packet_type, self.peer_addr, e
447                ));
448                return self
449                    .error(writer, "invalid_arguments", "malformed packet arguments")
450                    .await;
451            }
452        };
453        match request {
454            wire::Request::Echo(data) => {
455                self.write(writer, &wire::response(wire::ECHO_RES, &[&data]))
456                    .await
457            }
458            wire::Request::Option(name) if name == b"exceptions" => {
459                *exceptions = true;
460                self.write(writer, &wire::response(wire::OPTION_RES, &[&name]))
461                    .await
462            }
463            wire::Request::Option(_) => {
464                self.error(writer, "unknown_option", "the only option is exceptions")
465                    .await
466            }
467            wire::Request::SetClientId => true,
468            wire::Request::GetStatus(handle) => {
469                let reply = match self.jobs.get(&handle) {
470                    Some(job) => {
471                        wire::status_res(&handle, true, true, job.numerator, job.denominator)
472                    }
473                    None => wire::status_res(&handle, false, false, 0, 0),
474                };
475                self.write(writer, &reply).await
476            }
477            wire::Request::Worker(t) => {
478                log.warn(format!(
479                    "Gearman worker packet {} from {} refused decision=fail_closed_worker",
480                    t, self.peer_addr
481                ));
482                self.error(
483                    writer,
484                    "not_supported",
485                    "this server runs jobs itself; worker connections are not accepted",
486                )
487                .await;
488                false
489            }
490            wire::Request::Unsupported(t) => {
491                log.warn(format!(
492                    "Gearman packet type {} from {} is not supported \
493                     decision=fail_closed_unsupported",
494                    t, self.peer_addr
495                ));
496                self.error(writer, "not_supported", "packet type not supported")
497                    .await
498            }
499            wire::Request::Submit {
500                function,
501                unique,
502                workload,
503                priority,
504                background,
505            } => {
506                let handle = self.jobs.start(&function);
507                if !self.write(writer, &wire::job_created(&handle)).await {
508                    self.jobs.finish(&handle);
509                    return false;
510                }
511                let event = Event::new(
512                    &actions::GEARMAN_JOB_SUBMITTED_EVENT,
513                    serde_json::json!({
514                        "function": function,
515                        "unique_id": unique,
516                        "workload": String::from_utf8_lossy(&workload),
517                        "workload_bytes": workload.len(),
518                        "priority": priority.name(),
519                        "background": background,
520                        "job_handle": String::from_utf8_lossy(&handle),
521                    }),
522                );
523                let (packets, close) = self.run_job(&event, &handle, background, *exceptions).await;
524                let mut ok = true;
525                if !background {
526                    for packet in &packets {
527                        if let Some((wire::WORK_STATUS, args)) = wire::read_response(packet) {
528                            let n = String::from_utf8_lossy(&args[1]).parse().unwrap_or(0);
529                            let d = String::from_utf8_lossy(&args[2]).parse().unwrap_or(0);
530                            if let Some(job) =
531                                self.jobs.table.lock().unwrap().get_mut(handle.as_slice())
532                            {
533                                job.numerator = n;
534                                job.denominator = d;
535                            }
536                        }
537                        if !self.write(writer, packet).await {
538                            ok = false;
539                            break;
540                        }
541                    }
542                }
543                self.jobs.finish(&handle);
544                ok && !close
545            }
546        }
547    }
548
549    /// Ask the model to run a job; returns the packets to write (empty for a background job)
550    /// and whether to close afterwards.
551    async fn run_job(
552        &self,
553        event: &Event,
554        handle: &[u8],
555        background: bool,
556        exceptions: bool,
557    ) -> (Vec<Vec<u8>>, bool) {
558        let log = Log::new(Some(&self.status_tx));
559        let shown = String::from_utf8_lossy(handle).into_owned();
560        let result = match call_llm(
561            &self.llm_client,
562            &self.app_state,
563            self.server_id,
564            Some(self.connection_id),
565            event,
566            self.protocol.as_ref(),
567        )
568        .await
569        {
570            Ok(result) => result,
571            Err(e) => {
572                let (category, text) = match crate::utils::WireFailure::classify(&e) {
573                    crate::utils::WireFailure::Overloaded => ("overloaded", EXCEPTION_OVERLOADED),
574                    crate::utils::WireFailure::Unavailable => {
575                        ("unavailable", EXCEPTION_UNAVAILABLE)
576                    }
577                };
578                log.warn(format!(
579                    "Gearman job {} from {} decision=fail_closed_llm_error category={}",
580                    shown, self.peer_addr, category
581                ));
582                log.debug(format!("Gearman LLM call failed: {}", e));
583                let packet = if exceptions {
584                    wire::work_exception(handle, text)
585                } else {
586                    wire::work_fail(handle)
587                };
588                return (if background { vec![] } else { vec![packet] }, false);
589            }
590        };
591        for message in &result.messages {
592            log.info(message);
593        }
594
595        let mut items = Vec::new();
596        let mut close = false;
597        let mut stack = result.protocol_results;
598        stack.reverse();
599        while let Some(item) = stack.pop() {
600            match item {
601                ActionResult::CloseConnection => close = true,
602                ActionResult::Multiple(inner) => stack.extend(inner.into_iter().rev()),
603                other => items.push(other),
604            }
605        }
606
607        let mut packets: Vec<Vec<u8>> = Vec::new();
608        let mut outcome: Option<u32> = None;
609        let mut mismatch = None;
610        for item in items {
611            if outcome.is_some() {
612                log.warn(format!(
613                    "Gearman job {shown}: the model answered after the job's outcome; dropped"
614                ));
615                break;
616            }
617            let packet = match item {
618                ActionResult::Output(bytes) => match wire::read_response(&bytes) {
619                    Some((wire::ERROR, _)) => bytes,
620                    Some((
621                        wire::WORK_STATUS
622                        | wire::WORK_DATA
623                        | wire::WORK_COMPLETE
624                        | wire::WORK_FAIL
625                        | wire::WORK_EXCEPTION,
626                        args,
627                    )) if args[0] == handle => bytes,
628                    other => {
629                        mismatch = Some(format!("{:?}", other.map(|(t, _)| t)));
630                        break;
631                    }
632                },
633                ActionResult::Custom { name, data } if name == actions::WORK_RESULT => {
634                    match Work::from_json(&data) {
635                        Some(work) => work.render(handle),
636                        None => {
637                            mismatch = Some("unreadable work result".to_string());
638                            break;
639                        }
640                    }
641                }
642                other => {
643                    mismatch = Some(format!("{other:?}"));
644                    break;
645                }
646            };
647            let t = wire::read_response(&packet).map(|(t, _)| t).unwrap_or(0);
648            let packet = if t == wire::WORK_EXCEPTION && !exceptions {
649                // The client did not ask for exceptions: gearmand sends it WORK_FAIL.
650                wire::work_fail(handle)
651            } else {
652                packet
653            };
654            if matches!(
655                t,
656                wire::WORK_COMPLETE | wire::WORK_FAIL | wire::WORK_EXCEPTION | wire::ERROR
657            ) {
658                outcome = Some(t);
659            }
660            packets.push(packet);
661        }
662
663        let decision = if let Some(what) = mismatch {
664            log.warn(format!(
665                "Gearman job {shown} from {} decision=fail_closed_mismatched_reply ({what}); \
666                 answering WORK_FAIL",
667                self.peer_addr
668            ));
669            packets = vec![wire::work_fail(handle)];
670            None
671        } else {
672            match outcome {
673                Some(wire::WORK_COMPLETE) => Some("model_answer"),
674                Some(_) => Some("model_reject"),
675                None if packets.is_empty() && !close => {
676                    log.warn(format!(
677                        "Gearman job {shown} from {} decision=model_silent ({} failed \
678                         action(s)); answering WORK_FAIL",
679                        self.peer_addr,
680                        result.failures.len()
681                    ));
682                    packets.push(wire::work_fail(handle));
683                    None
684                }
685                None if close => Some("model_close"),
686                None => {
687                    log.warn(format!(
688                        "Gearman job {shown} from {} decision=fail_closed_unfinished: \
689                         progress without an outcome; answering WORK_FAIL",
690                        self.peer_addr
691                    ));
692                    packets.push(wire::work_fail(handle));
693                    None
694                }
695            }
696        };
697        if let Some(decision) = decision {
698            log.info(format!(
699                "Gearman job {shown} from {} decision={} background={}",
700                self.peer_addr, decision, background
701            ));
702        }
703        (if background { Vec::new() } else { packets }, close)
704    }
705}
706
707/// Keep reading briefly after the last reply and the half-close, so the close is a FIN rather
708/// than an RST that could destroy that reply. Bounded both ways.
709const LINGER_TIME: Duration = Duration::from_secs(2);
710const LINGER_BYTES: usize = 64 * 1024;
711
712async fn linger<R: AsyncRead + Unpin>(reader: &mut R) {
713    let deadline = tokio::time::Instant::now() + LINGER_TIME;
714    let mut sink = [0u8; 4096];
715    let mut drained = 0usize;
716    while drained < LINGER_BYTES {
717        match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
718            Ok(Ok(n)) if n > 0 => drained += n,
719            _ => break,
720        }
721    }
722}
723
724/// Reads packets and admin lines, keeping anything past what was asked for.
725struct Framer<'a, R> {
726    reader: &'a mut R,
727    pending: Vec<u8>,
728    chunk: Vec<u8>,
729}
730
731impl<'a, R: AsyncRead + Unpin> Framer<'a, R> {
732    fn new(reader: &'a mut R) -> Self {
733        Self {
734            reader,
735            pending: Vec::new(),
736            chunk: vec![0u8; 8192],
737        }
738    }
739
740    async fn fill(&mut self, timeout: Duration) -> Result<(), ReadError> {
741        match tokio::time::timeout(timeout, self.reader.read(&mut self.chunk)).await {
742            Err(_) => Err(ReadError::TimedOut),
743            Ok(Ok(0)) => Err(ReadError::Eof),
744            Ok(Ok(n)) => {
745                self.pending.extend_from_slice(&self.chunk[..n]);
746                Ok(())
747            }
748            Ok(Err(e)) => Err(ReadError::Io(e)),
749        }
750    }
751
752    /// `first` bounds the wait for the message's first byte; `idle` every wait after it.
753    async fn next_message(
754        &mut self,
755        first: Duration,
756        idle: Duration,
757    ) -> Result<Message, ReadError> {
758        if self.pending.is_empty() {
759            self.fill(first).await?;
760        }
761        if self.pending[0] == 0 {
762            while self.pending.len() < wire::HEADER_LEN {
763                self.fill(idle).await?;
764            }
765            let header = match wire::parse_header(&self.pending[..wire::HEADER_LEN]) {
766                Ok(h) => h,
767                Err(wire::HeaderError::TooLarge(size)) => return Err(ReadError::TooLarge(size)),
768                Err(wire::HeaderError::BadMagic) => return Err(ReadError::BadMagic),
769            };
770            // Bounded by parse_header: at most MAX_PACKET_BYTES.
771            let total = wire::HEADER_LEN + header.size as usize;
772            while self.pending.len() < total {
773                self.fill(idle).await?;
774            }
775            let packet: Vec<u8> = self.pending.drain(..total).collect();
776            return Ok(Message::Packet(header, packet[wire::HEADER_LEN..].to_vec()));
777        }
778        loop {
779            if let Some(at) = self.pending.iter().position(|b| *b == b'\n') {
780                if at + 1 > wire::MAX_ADMIN_LINE {
781                    return Err(ReadError::LineTooLong);
782                }
783                let line: Vec<u8> = self.pending.drain(..=at).collect();
784                let text = String::from_utf8_lossy(&line[..at]);
785                return Ok(Message::Admin(text.trim_end_matches('\r').to_string()));
786            }
787            if self.pending.len() >= wire::MAX_ADMIN_LINE {
788                return Err(ReadError::LineTooLong);
789            }
790            self.fill(idle).await?;
791        }
792    }
793}