Skip to main content

netget/server/beanstalkd/
mod.rs

1//! Beanstalkd work-queue server — the model is the queue.
2//!
3//! The client speaks first, one command per CRLF line; `put` is followed by a body of the
4//! declared length. The commands whose answer is *queue content* — put, reserve, the job
5//! commands, stats, list-tubes — raise an event and the model answers with a structured action.
6//! The commands whose answer is *this connection's own state* — `use`, `watch`, `ignore`,
7//! `list-tube-used`, `list-tubes-watched`, `quit` — and every malformed or unknown line are
8//! answered here without consulting anyone.
9//!
10//! Five properties worth knowing before changing the loop:
11//!
12//! 1. **Sizes are checked before anything is buffered.** A command line is capped at upstream's
13//!    224 bytes ([`wire::MAX_LINE_BYTES`]); a `put` body at 65535 ([`wire::MAX_JOB_BYTES`]),
14//!    judged from the *declared* `<bytes>` before a single body byte is read.
15//! 2. **The model cannot write framing, and cannot answer the wrong command.** Its actions are
16//!    rendered by [`wire`]; the loop then checks the reply fits the command
17//!    ([`wire::reply_fits`]). A reply that does not fit, no reply, and a backend failure all
18//!    answer `INTERNAL_ERROR` (`OUT_OF_MEMORY` — upstream's "try again later" — when the
19//!    backend is at capacity), and the session continues: every one of those is a complete
20//!    beanstalkd answer to one command.
21//! 3. **A worker waiting in `reserve` is not idle.** When the model answers `reserve` with
22//!    `wait_for_beanstalkd_job`, the server owes the worker an answer, so the idle deadline
23//!    does not apply. `reserve-with-timeout` is answered `TIMED_OUT` by NetGet when its own
24//!    timeout runs out; a plain `reserve` waits until a reply is written to the connection from
25//!    elsewhere (the dashboard's `[ message ]`) or the worker hangs up. The connection cap is
26//!    what bounds how many can wait.
27//! 4. **The deadlines wrap the read, not the answer.** A command parked for a human under a
28//!    `manual` rule is closed by neither `first_byte_timeout_secs` nor `idle_timeout_secs`.
29//! 5. **Every close lingers** (see [`linger`]), so a refusal written just before closing reaches
30//!    a peer that still has pipelined input in flight instead of being destroyed by an RST.
31pub mod actions;
32pub mod wire;
33
34use crate::llm::action_helper::call_llm;
35use crate::llm::actions::protocol_trait::ActionResult;
36use crate::llm::ollama_client::OllamaClient;
37use crate::logging::emit::Log;
38use crate::protocol::{Event, EventType};
39use crate::server::connection::ConnectionId;
40use crate::state::app_state::AppState;
41use anyhow::Result;
42use std::net::SocketAddr;
43use std::pin::Pin;
44use std::sync::atomic::{AtomicU64, Ordering};
45use std::sync::Arc;
46use std::task::{Context, Poll};
47use std::time::Duration;
48use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
49use tokio::net::TcpListener;
50use tokio::sync::{mpsc, Mutex, Notify};
51use wire::Command;
52
53pub use wire::{MAX_JOB_BYTES, MAX_LINE_BYTES};
54
55/// How long a new connection may take to send its first command.
56///
57/// beanstalkd is client-speaks-first and real clients send a command as soon as they connect
58/// (greenstalk sends `use`/`watch` from its constructor, or nothing until the first call). 300
59/// seconds is the window a `manual` rule gives a human, for a NetGet TCP client parked on its
60/// operator. A listener exposed to strangers should lower it through `first_byte_timeout_secs`.
61const FIRST_COMMAND_TIMEOUT: Duration = Duration::from_secs(300);
62
63/// How long the server waits for the next command after answering one — also the bound on
64/// each read of a `put` body. Upstream has no idle timeout at all; a producer that goes quiet
65/// for five minutes reconnects. A worker waiting in `reserve` is exempt (see the module docs).
66const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
67
68/// Concurrent connections admitted before new ones are refused — the house default. It is
69/// also what bounds the number of workers that can wait in `reserve` at once, since a waiting
70/// reserve has no deadline of its own.
71const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
72
73/// What a peer over [`MAX_CONNECTIONS`] reads as the answer to its first command:
74/// `OUT_OF_MEMORY`, upstream's "the server cannot take this now; try again later".
75const CONNECTION_CAP_REFUSAL: &[u8] = b"OUT_OF_MEMORY\r\n";
76
77/// The peer-visible answers when the model could not or did not answer. Fixed literals, so
78/// nothing derived from an error reaches the wire.
79const UNAVAILABLE_REPLY: &[u8] = b"INTERNAL_ERROR\r\n";
80const OVERLOADED_REPLY: &[u8] = b"OUT_OF_MEMORY\r\n";
81
82pub struct BeanstalkdServer;
83
84impl BeanstalkdServer {
85    pub async fn spawn_with_llm_actions(
86        listen_addr: SocketAddr,
87        llm_client: OllamaClient,
88        app_state: Arc<AppState>,
89        status_tx: mpsc::UnboundedSender<String>,
90        server_id: crate::state::ServerId,
91        first_byte_timeout_secs: Option<u64>,
92        idle_timeout_secs: Option<u64>,
93    ) -> Result<SocketAddr> {
94        let first_timeout = first_byte_timeout_secs
95            .map(Duration::from_secs)
96            .unwrap_or(FIRST_COMMAND_TIMEOUT);
97        let idle_timeout = idle_timeout_secs
98            .map(Duration::from_secs)
99            .unwrap_or(IDLE_TIMEOUT);
100        let listener = TcpListener::bind(listen_addr).await?;
101        let local_addr = listener.local_addr()?;
102
103        Log::new(Some(&status_tx)).info(format!("Beanstalkd server listening on {}", local_addr));
104
105        let protocol = Arc::new(actions::BeanstalkdProtocol::new());
106        let task_registrar = app_state.clone();
107        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
108        let accept_handle = tokio::spawn(async move {
109            loop {
110                match crate::server::accept_bounded::accept_bounded(
111                    &listener,
112                    &limiter,
113                    CONNECTION_CAP_REFUSAL,
114                    "Beanstalkd",
115                    Some(&status_tx),
116                )
117                .await
118                {
119                    Ok((socket, peer_addr, permit)) => {
120                        let connection_id =
121                            ConnectionId::new(app_state.get_next_unified_id().await);
122                        use crate::state::server::{
123                            ConnectionState as ServerConnectionState, ConnectionStatus,
124                            ProtocolConnectionInfo,
125                        };
126                        let now = crate::utils::clock::Instant::now();
127                        app_state
128                            .add_connection_to_server(
129                                server_id,
130                                ServerConnectionState {
131                                    id: connection_id,
132                                    remote_addr: peer_addr,
133                                    local_addr,
134                                    bytes_sent: 0,
135                                    bytes_received: 0,
136                                    packets_sent: 0,
137                                    packets_received: 0,
138                                    last_activity: now,
139                                    status: ConnectionStatus::Active,
140                                    status_changed_at: now,
141                                    protocol_info: ProtocolConnectionInfo::empty(),
142                                },
143                            )
144                            .await;
145                        let _ = status_tx.send("__UPDATE_UI__".to_string());
146                        Log::new(Some(&status_tx))
147                            .info(format!("Beanstalkd client connected from {}", peer_addr));
148
149                        let session = Session {
150                            peer_addr,
151                            llm_client: llm_client.clone(),
152                            app_state: app_state.clone(),
153                            status_tx: status_tx.clone(),
154                            server_id,
155                            protocol: protocol.clone(),
156                            connection_id,
157                            first_timeout,
158                            idle_timeout,
159                        };
160                        // Tracked, not detached: stop_server must abort this task too.
161                        let task_owner = app_state.clone();
162                        task_owner
163                            .spawn_server_task(server_id, async move {
164                                // Released when the connection ends; this task is the whole
165                                // connection, so MAX_CONNECTIONS caps live connections.
166                                let _permit = permit;
167                                session.run(socket).await
168                            })
169                            .await;
170                    }
171                    Err(e) => {
172                        Log::new(Some(&status_tx)).error(format!("Beanstalkd accept error: {}", e));
173                        break;
174                    }
175                }
176            }
177        });
178
179        task_registrar
180            .register_server_task(server_id, accept_handle)
181            .await;
182        Ok(local_addr)
183    }
184}
185
186/// Counts writes to one connection and wakes whoever is waiting on them.
187///
188/// A waiting `reserve` must end when *anything* answers it — including a reply injected from
189/// the dashboard, which is written by `peer_support`'s own task and never passes through the
190/// session loop. Watching the writer is the one place both paths meet.
191#[derive(Default)]
192struct WriteWatch {
193    writes: AtomicU64,
194    notify: Notify,
195}
196
197impl WriteWatch {
198    fn count(&self) -> u64 {
199        self.writes.load(Ordering::SeqCst)
200    }
201}
202
203/// The connection's write half, reporting every non-empty write to its [`WriteWatch`].
204struct WatchedWriter<W> {
205    inner: W,
206    watch: Arc<WriteWatch>,
207}
208
209impl<W: AsyncWrite + Unpin> AsyncWrite for WatchedWriter<W> {
210    fn poll_write(
211        mut self: Pin<&mut Self>,
212        cx: &mut Context<'_>,
213        buf: &[u8],
214    ) -> Poll<std::io::Result<usize>> {
215        let polled = Pin::new(&mut self.inner).poll_write(cx, buf);
216        if let Poll::Ready(Ok(n)) = &polled {
217            if *n > 0 {
218                self.watch.writes.fetch_add(1, Ordering::SeqCst);
219                self.watch.notify.notify_waiters();
220            }
221        }
222        polled
223    }
224    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
225        Pin::new(&mut self.inner).poll_flush(cx)
226    }
227    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
228        Pin::new(&mut self.inner).poll_shutdown(cx)
229    }
230}
231
232type Writer = Arc<Mutex<WatchedWriter<tokio::io::WriteHalf<tokio::net::TcpStream>>>>;
233
234struct Session {
235    peer_addr: SocketAddr,
236    llm_client: OllamaClient,
237    app_state: Arc<AppState>,
238    status_tx: mpsc::UnboundedSender<String>,
239    server_id: crate::state::ServerId,
240    protocol: Arc<actions::BeanstalkdProtocol>,
241    connection_id: ConnectionId,
242    first_timeout: Duration,
243    idle_timeout: Duration,
244}
245
246/// The connection's own tube state: the one piece of state NetGet keeps, because it is about
247/// this connection rather than about the queue.
248struct Tubes {
249    used: String,
250    watched: Vec<String>,
251}
252
253/// What handling one command decided.
254enum Next {
255    Continue,
256    /// The model left a reserve waiting for a job.
257    Wait,
258    Close,
259}
260
261impl Session {
262    async fn run(self, socket: tokio::net::TcpStream) {
263        let (mut reader, write_half) = tokio::io::split(socket);
264        let watch = Arc::new(WriteWatch::default());
265        let writer: Writer = Arc::new(Mutex::new(WatchedWriter {
266            inner: write_half,
267            watch: watch.clone(),
268        }));
269
270        // Registered before the first command, so the operator can reach this connection
271        // while a command is parked for them or a worker waits in reserve.
272        let peer_rx = crate::server::peer_support::register_peer_channel(
273            &self.app_state,
274            self.server_id,
275            self.connection_id.as_u32(),
276        )
277        .await;
278        crate::server::peer_support::spawn_peer_command_task(
279            peer_rx,
280            self.protocol.clone(),
281            self.app_state.clone(),
282            self.server_id,
283            self.connection_id.as_u32(),
284            writer.clone(),
285            self.status_tx.clone(),
286        );
287
288        {
289            let mut framer = Framer::new(&mut reader);
290            self.session(&mut framer, &writer, &watch).await;
291        }
292
293        self.app_state
294            .remove_peer_handle(self.server_id, self.connection_id.as_u32())
295            .await;
296        let _ = writer.lock().await.shutdown().await;
297        linger(&mut reader).await;
298        self.app_state
299            .update_connection_status(
300                self.server_id,
301                self.connection_id,
302                crate::state::server::ConnectionStatus::Closed,
303            )
304            .await;
305        let _ = self.status_tx.send("__UPDATE_UI__".to_string());
306    }
307
308    async fn write(&self, writer: &Writer, data: &[u8]) -> std::io::Result<()> {
309        {
310            let mut w = writer.lock().await;
311            w.write_all(data).await?;
312            w.flush().await?;
313        }
314        self.record_sent(data.len()).await;
315        Ok(())
316    }
317
318    async fn record_sent(&self, n: usize) {
319        self.app_state
320            .update_connection_stats(
321                self.server_id,
322                self.connection_id,
323                None,
324                Some(n as u64),
325                None,
326                Some(1),
327            )
328            .await;
329    }
330
331    async fn record_received(&self, n: usize) {
332        self.app_state
333            .update_connection_stats(
334                self.server_id,
335                self.connection_id,
336                Some(n as u64),
337                None,
338                Some(1),
339                None,
340            )
341            .await;
342    }
343
344    async fn session<R: AsyncRead + Unpin>(
345        &self,
346        framer: &mut Framer<'_, R>,
347        writer: &Writer,
348        watch: &Arc<WriteWatch>,
349    ) {
350        let log = Log::new(Some(&self.status_tx));
351        let mut tubes = Tubes {
352            used: "default".to_string(),
353            watched: vec!["default".to_string()],
354        };
355        let mut answered_one = false;
356
357        loop {
358            let timeout = if answered_one {
359                self.idle_timeout
360            } else {
361                self.first_timeout
362            };
363            let (line, n) = match framer.next_line(timeout).await {
364                LineRead::Line(line, n) => (line, n),
365                LineRead::Eof => {
366                    log.info(format!("Beanstalkd client {} disconnected", self.peer_addr));
367                    return;
368                }
369                LineRead::TimedOut => {
370                    log.info(format!(
371                        "Beanstalkd client {} sent no command within {}s; closing",
372                        self.peer_addr,
373                        timeout.as_secs()
374                    ));
375                    return;
376                }
377                LineRead::TooLong => {
378                    log.warn(format!(
379                        "Beanstalkd command from {} exceeded {} bytes \
380                         decision=fail_closed_line_too_long",
381                        self.peer_addr, MAX_LINE_BYTES
382                    ));
383                    let _ = self.write(writer, b"BAD_FORMAT\r\n").await;
384                    return;
385                }
386                LineRead::Failed(e) => {
387                    log.error(format!(
388                        "Beanstalkd read error from {}: {}",
389                        self.peer_addr, e
390                    ));
391                    return;
392                }
393            };
394            self.record_received(n).await;
395            log.trace(format!(
396                "Beanstalkd command from {}: {:?}",
397                self.peer_addr, line
398            ));
399            answered_one = true;
400            let command = wire::parse_command(&line);
401
402            // Commands about this connection's own state, and every malformed line.
403            let fixed: Option<Vec<u8>> = match &command {
404                Command::Use(tube) => {
405                    tubes.used = tube.clone();
406                    Some(format!("USING {tube}\r\n").into_bytes())
407                }
408                Command::Watch(tube) => {
409                    if !tubes.watched.contains(tube) {
410                        tubes.watched.push(tube.clone());
411                    }
412                    Some(format!("WATCHING {}\r\n", tubes.watched.len()).into_bytes())
413                }
414                Command::Ignore(tube) => {
415                    if tubes.watched.len() == 1 && tubes.watched[0] == *tube {
416                        Some(b"NOT_IGNORED\r\n".to_vec())
417                    } else {
418                        tubes.watched.retain(|t| t != tube);
419                        Some(format!("WATCHING {}\r\n", tubes.watched.len()).into_bytes())
420                    }
421                }
422                Command::ListTubeUsed => Some(format!("USING {}\r\n", tubes.used).into_bytes()),
423                Command::ListTubesWatched => Some(
424                    wire::render_tube_list(&tubes.watched)
425                        .map(String::into_bytes)
426                        .unwrap_or_else(|_| UNAVAILABLE_REPLY.to_vec()),
427                ),
428                Command::Quit => return,
429                Command::BadFormat => Some(b"BAD_FORMAT\r\n".to_vec()),
430                Command::Unknown => Some(b"UNKNOWN_COMMAND\r\n".to_vec()),
431                _ => None,
432            };
433            if let Some(reply) = fixed {
434                if self.write(writer, &reply).await.is_err() {
435                    return;
436                }
437                continue;
438            }
439
440            // put: the body, judged by its declared size before any of it is read.
441            let mut body: Option<Vec<u8>> = None;
442            if let Command::Put { bytes, .. } = &command {
443                let declared = *bytes;
444                match declared {
445                    Some(n) if n <= MAX_JOB_BYTES as u64 => {
446                        match framer.read_exact(n as usize + 2, self.idle_timeout).await {
447                            Ok(mut data) => {
448                                self.record_received(data.len()).await;
449                                if !data.ends_with(b"\r\n") {
450                                    log.warn(format!(
451                                        "Beanstalkd put from {}: body not followed by CRLF \
452                                         decision=fail_closed_expected_crlf",
453                                        self.peer_addr
454                                    ));
455                                    if self.write(writer, b"EXPECTED_CRLF\r\n").await.is_err() {
456                                        return;
457                                    }
458                                    continue;
459                                }
460                                data.truncate(n as usize);
461                                body = Some(data);
462                            }
463                            Err(reason) => {
464                                log.info(format!(
465                                    "Beanstalkd put from {}: body never arrived ({reason}); \
466                                     closing",
467                                    self.peer_addr
468                                ));
469                                return;
470                            }
471                        }
472                    }
473                    _ => {
474                        log.warn(format!(
475                            "Beanstalkd put from {} declared {} bytes, over max-job-size {} \
476                             decision=fail_closed_job_too_big",
477                            self.peer_addr,
478                            declared
479                                .map(|n| n.to_string())
480                                .unwrap_or_else(|| "more than 2^64".to_string()),
481                            MAX_JOB_BYTES
482                        ));
483                        if self.write(writer, b"JOB_TOO_BIG\r\n").await.is_err() {
484                            return;
485                        }
486                        // Stay in step with a client that sends the body anyway, as upstream
487                        // does — unless the body is so large that skipping it is not worth it.
488                        match declared.map(|n| n.saturating_add(2)) {
489                            Some(skip) if skip <= wire::MAX_DISCARD_BYTES => {
490                                if !framer.discard(skip, self.idle_timeout).await {
491                                    return;
492                                }
493                                continue;
494                            }
495                            _ => return,
496                        }
497                    }
498                }
499            }
500
501            match self
502                .answer_with_model(&command, body.as_deref(), &tubes, writer)
503                .await
504            {
505                Next::Continue => {}
506                Next::Close => return,
507                Next::Wait => {
508                    if !self.wait_in_reserve(&command, framer, writer, watch).await {
509                        return;
510                    }
511                }
512            }
513        }
514    }
515
516    /// Hold a worker whose reserve the model left waiting. Returns `false` when the connection
517    /// should close.
518    async fn wait_in_reserve<R: AsyncRead + Unpin>(
519        &self,
520        command: &Command,
521        framer: &mut Framer<'_, R>,
522        writer: &Writer,
523        watch: &Arc<WriteWatch>,
524    ) -> bool {
525        let log = Log::new(Some(&self.status_tx));
526        let deadline = match command {
527            Command::ReserveWithTimeout(secs) => {
528                Some(tokio::time::Instant::now() + Duration::from_secs(u64::from(*secs)))
529            }
530            _ => None,
531        };
532        let start = watch.count();
533        loop {
534            let notified = watch.notify.notified();
535            tokio::pin!(notified);
536            notified.as_mut().enable();
537            if watch.count() != start {
538                log.info(format!(
539                    "Beanstalkd reserve from {} answered by a reply written to the connection",
540                    self.peer_addr
541                ));
542                return true;
543            }
544            let timer = async {
545                match deadline {
546                    Some(at) => tokio::time::sleep_until(at).await,
547                    None => std::future::pending::<()>().await,
548                }
549            };
550            tokio::select! {
551                read = framer.fill() => match read {
552                    Ok(0) => {
553                        log.info(format!(
554                            "Beanstalkd client {} hung up while waiting in reserve",
555                            self.peer_addr
556                        ));
557                        return false;
558                    }
559                    Ok(n) => {
560                        log.warn(format!(
561                            "Beanstalkd client {} sent {n} bytes while its reserve was waiting; \
562                             the reserve is abandoned and the command is read",
563                            self.peer_addr
564                        ));
565                        return true;
566                    }
567                    Err(e) => {
568                        log.error(format!("Beanstalkd read error from {}: {}", self.peer_addr, e));
569                        return false;
570                    }
571                },
572                _ = timer => {
573                    // Written only if nothing answered the reserve meanwhile — checked under
574                    // the writer lock, which an injected reply also takes.
575                    let wrote = {
576                        let mut w = writer.lock().await;
577                        if watch.count() != start {
578                            false
579                        } else {
580                            if w.write_all(b"TIMED_OUT\r\n").await.is_err()
581                                || w.flush().await.is_err()
582                            {
583                                return false;
584                            }
585                            true
586                        }
587                    };
588                    if wrote {
589                        self.record_sent(11).await;
590                        log.info(format!(
591                            "Beanstalkd reserve from {} decision=model_wait_timed_out",
592                            self.peer_addr
593                        ));
594                    }
595                    return true;
596                }
597                _ = &mut notified => continue,
598            }
599        }
600    }
601
602    /// Raise the command's event and write what the model decided.
603    async fn answer_with_model(
604        &self,
605        command: &Command,
606        body: Option<&[u8]>,
607        tubes: &Tubes,
608        writer: &Writer,
609    ) -> Next {
610        let log = Log::new(Some(&self.status_tx));
611        let Some((event_type, data)) = event_for(command, body, tubes) else {
612            return Next::Continue;
613        };
614        let event = Event::new(event_type, data);
615
616        let result = match call_llm(
617            &self.llm_client,
618            &self.app_state,
619            self.server_id,
620            Some(self.connection_id),
621            &event,
622            self.protocol.as_ref(),
623        )
624        .await
625        {
626            Ok(result) => result,
627            Err(e) => {
628                let (category, reply) = match crate::utils::WireFailure::classify(&e) {
629                    crate::utils::WireFailure::Overloaded => ("overloaded", OVERLOADED_REPLY),
630                    crate::utils::WireFailure::Unavailable => ("unavailable", UNAVAILABLE_REPLY),
631                };
632                log.warn(format!(
633                    "Beanstalkd {:?} from {} decision=fail_closed_llm_error category={}",
634                    command, self.peer_addr, category
635                ));
636                log.debug(format!("Beanstalkd LLM call failed: {}", e));
637                return match self.write(writer, reply).await {
638                    Ok(()) => Next::Continue,
639                    Err(_) => Next::Close,
640                };
641            }
642        };
643        for message in &result.messages {
644            log.info(message);
645        }
646
647        let mut replies: Vec<Vec<u8>> = Vec::new();
648        let mut close = false;
649        let mut wait = false;
650        let mut stack = result.protocol_results;
651        stack.reverse();
652        while let Some(item) = stack.pop() {
653            match item {
654                ActionResult::Output(bytes) => replies.push(bytes),
655                ActionResult::CloseConnection => close = true,
656                ActionResult::WaitForMore => wait = true,
657                ActionResult::Multiple(items) => stack.extend(items.into_iter().rev()),
658                _ => {}
659            }
660        }
661
662        let is_reserve = matches!(command, Command::Reserve | Command::ReserveWithTimeout(_));
663        let Some(reply) = replies.first() else {
664            if wait && is_reserve {
665                log.info(format!(
666                    "Beanstalkd {:?} from {} decision=model_wait",
667                    command, self.peer_addr
668                ));
669                return if close { Next::Close } else { Next::Wait };
670            }
671            let decision = if wait {
672                "fail_closed_mismatched_reply"
673            } else {
674                "model_silent"
675            };
676            log.warn(format!(
677                "Beanstalkd {:?} from {} decision={} ({} failed action(s)); answering \
678                 INTERNAL_ERROR",
679                command,
680                self.peer_addr,
681                decision,
682                result.failures.len()
683            ));
684            return match self.write(writer, UNAVAILABLE_REPLY).await {
685                Ok(()) if !close => Next::Continue,
686                _ => Next::Close,
687            };
688        };
689
690        if !wire::reply_fits(command, reply) {
691            let (word, _) = wire::reply_head(reply);
692            log.warn(format!(
693                "Beanstalkd {:?} from {} decision=fail_closed_mismatched_reply reply={}; \
694                 answering INTERNAL_ERROR",
695                command, self.peer_addr, word
696            ));
697            return match self.write(writer, UNAVAILABLE_REPLY).await {
698                Ok(()) if !close => Next::Continue,
699                _ => Next::Close,
700            };
701        }
702        if replies.len() > 1 {
703            log.warn(format!(
704                "Beanstalkd {:?}: the model produced {} replies to one command; sending the first",
705                command,
706                replies.len()
707            ));
708        }
709        let (word, _) = wire::reply_head(reply);
710        let decision = if wire::is_refusal(reply) {
711            "model_reject"
712        } else {
713            "model_answer"
714        };
715        log.info(format!(
716            "Beanstalkd {:?} from {} decision={} reply={}",
717            command, self.peer_addr, decision, word
718        ));
719        if self.write(writer, reply).await.is_err() || close {
720            return Next::Close;
721        }
722        Next::Continue
723    }
724}
725
726/// The event a command raises and its data, for the commands the model answers.
727fn event_for(
728    command: &Command,
729    body: Option<&[u8]>,
730    tubes: &Tubes,
731) -> Option<(&'static EventType, serde_json::Value)> {
732    use actions::{
733        BEANSTALKD_JOB_COMMAND_EVENT, BEANSTALKD_PUT_EVENT, BEANSTALKD_RESERVE_EVENT,
734        BEANSTALKD_STATS_EVENT,
735    };
736    use serde_json::json;
737    let used = &tubes.used;
738    let job = |name: &str, id: Option<u64>| {
739        let mut data = json!({"command": name, "tube": used});
740        if let Some(id) = id {
741            data["job_id"] = json!(id);
742        }
743        (&*BEANSTALKD_JOB_COMMAND_EVENT, data)
744    };
745    Some(match command {
746        Command::Put {
747            priority,
748            delay,
749            ttr,
750            ..
751        } => {
752            let body = body.unwrap_or_default();
753            (
754                &*BEANSTALKD_PUT_EVENT,
755                json!({
756                    "tube": used,
757                    "priority": priority,
758                    "delay": delay,
759                    "ttr": ttr,
760                    "body": String::from_utf8_lossy(body),
761                    "body_bytes": body.len(),
762                }),
763            )
764        }
765        Command::Reserve => (
766            &*BEANSTALKD_RESERVE_EVENT,
767            json!({
768                "tubes": tubes.watched,
769                "answer_with": actions::reserve_answer_with(&tubes.watched),
770            }),
771        ),
772        Command::ReserveWithTimeout(secs) => (
773            &*BEANSTALKD_RESERVE_EVENT,
774            json!({
775                "tubes": tubes.watched,
776                "timeout_secs": secs,
777                "answer_with": actions::reserve_answer_with(&tubes.watched),
778            }),
779        ),
780        Command::ReserveJob(id) => job("reserve-job", Some(*id)),
781        Command::Delete(id) => job("delete", Some(*id)),
782        Command::Release {
783            id,
784            priority,
785            delay,
786        } => {
787            let (event, mut data) = job("release", Some(*id));
788            data["priority"] = json!(priority);
789            data["delay"] = json!(delay);
790            (event, data)
791        }
792        Command::Bury { id, priority } => {
793            let (event, mut data) = job("bury", Some(*id));
794            data["priority"] = json!(priority);
795            (event, data)
796        }
797        Command::Touch(id) => job("touch", Some(*id)),
798        Command::Peek(id) => job("peek", Some(*id)),
799        Command::PeekReady => job("peek-ready", None),
800        Command::PeekDelayed => job("peek-delayed", None),
801        Command::PeekBuried => job("peek-buried", None),
802        Command::Kick(bound) => {
803            let (event, mut data) = job("kick", None);
804            data["bound"] = json!(bound);
805            (event, data)
806        }
807        Command::KickJob(id) => job("kick-job", Some(*id)),
808        Command::PauseTube { tube, delay } => (
809            &*BEANSTALKD_JOB_COMMAND_EVENT,
810            json!({"command": "pause-tube", "tube": tube, "delay": delay}),
811        ),
812        Command::Stats => (
813            &*BEANSTALKD_STATS_EVENT,
814            json!({"scope": "server", "answer_with": actions::stats_answer_with("server")}),
815        ),
816        Command::StatsTube(tube) => (
817            &*BEANSTALKD_STATS_EVENT,
818            json!({
819                "scope": "tube",
820                "tube": tube,
821                "answer_with": actions::stats_answer_with("tube"),
822            }),
823        ),
824        Command::StatsJob(id) => (
825            &*BEANSTALKD_STATS_EVENT,
826            json!({
827                "scope": "job",
828                "job_id": id,
829                "answer_with": actions::stats_answer_with("job"),
830            }),
831        ),
832        Command::ListTubes => (
833            &*BEANSTALKD_STATS_EVENT,
834            json!({"scope": "tubes", "answer_with": actions::stats_answer_with("tubes")}),
835        ),
836        _ => return None,
837    })
838}
839
840/// How long, and how much, the server keeps reading after it has sent its last reply and
841/// half-closed. Closing a socket with unread input makes the kernel send RST instead of FIN,
842/// and an RST can destroy a reply the peer has not read yet — a `BAD_FORMAT` for an over-long
843/// line with pipelined commands behind it would arrive as "connection reset". Bounded both
844/// ways so a peer that keeps sending cannot hold the task open.
845const LINGER_TIME: Duration = Duration::from_secs(2);
846const LINGER_BYTES: usize = 64 * 1024;
847
848async fn linger<R: AsyncRead + Unpin>(reader: &mut R) {
849    let deadline = tokio::time::Instant::now() + LINGER_TIME;
850    let mut sink = [0u8; 4096];
851    let mut drained = 0usize;
852    while drained < LINGER_BYTES {
853        match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
854            Ok(Ok(n)) if n > 0 => drained += n,
855            _ => break,
856        }
857    }
858}
859
860enum LineRead {
861    /// A line without its terminator, and the wire bytes it consumed.
862    Line(String, usize),
863    Eof,
864    TimedOut,
865    /// The line exceeded [`MAX_LINE_BYTES`] including its CRLF.
866    TooLong,
867    Failed(std::io::Error),
868}
869
870/// Accumulates reads into lines and bodies, keeping anything past what was asked for — a
871/// client sends `put …\r\n<body>\r\n` in one write, and may pipeline further commands.
872///
873/// `pending` never holds more than one read past [`MAX_LINE_BYTES`] while a line is being
874/// looked for, or one read past `MAX_JOB_BYTES + 2` while a body is.
875struct Framer<'a, R> {
876    reader: &'a mut R,
877    pending: Vec<u8>,
878    chunk: Vec<u8>,
879}
880
881impl<'a, R: AsyncRead + Unpin> Framer<'a, R> {
882    fn new(reader: &'a mut R) -> Self {
883        Self {
884            reader,
885            pending: Vec::new(),
886            chunk: vec![0u8; 4096],
887        }
888    }
889
890    /// One read into `pending`. `Ok(0)` is EOF. Cancel-safe: a read that has not completed
891    /// has consumed nothing.
892    async fn fill(&mut self) -> std::io::Result<usize> {
893        let n = self.reader.read(&mut self.chunk).await?;
894        self.pending.extend_from_slice(&self.chunk[..n]);
895        Ok(n)
896    }
897
898    /// `timeout` bounds the wait for *more bytes*, not the whole line.
899    async fn next_line(&mut self, timeout: Duration) -> LineRead {
900        loop {
901            if let Some(idx) = self.pending.iter().position(|b| *b == b'\n') {
902                let consumed = idx + 1;
903                if consumed > MAX_LINE_BYTES {
904                    return LineRead::TooLong;
905                }
906                let line: Vec<u8> = self.pending.drain(..consumed).collect();
907                let text = String::from_utf8_lossy(&line[..idx]);
908                return LineRead::Line(text.trim_end_matches('\r').to_string(), consumed);
909            }
910            // No newline in MAX_LINE_BYTES bytes: even if the next byte is one, the line is
911            // longer than upstream allows.
912            if self.pending.len() >= MAX_LINE_BYTES {
913                return LineRead::TooLong;
914            }
915            match tokio::time::timeout(timeout, self.fill()).await {
916                Err(_) => return LineRead::TimedOut,
917                Ok(Ok(0)) => {
918                    if self.pending.is_empty() {
919                        return LineRead::Eof;
920                    }
921                    let line = std::mem::take(&mut self.pending);
922                    let consumed = line.len();
923                    let text = String::from_utf8_lossy(&line);
924                    return LineRead::Line(text.trim_end_matches('\r').to_string(), consumed);
925                }
926                Ok(Ok(_)) => {}
927                Ok(Err(e)) => return LineRead::Failed(e),
928            }
929        }
930    }
931
932    /// Exactly `n` bytes; `timeout` bounds each wait for more.
933    async fn read_exact(&mut self, n: usize, timeout: Duration) -> Result<Vec<u8>, String> {
934        while self.pending.len() < n {
935            match tokio::time::timeout(timeout, self.fill()).await {
936                Err(_) => return Err(format!("no bytes for {}s", timeout.as_secs())),
937                Ok(Ok(0)) => return Err("EOF".to_string()),
938                Ok(Ok(_)) => {}
939                Ok(Err(e)) => return Err(e.to_string()),
940            }
941        }
942        Ok(self.pending.drain(..n).collect())
943    }
944
945    /// Read and drop `n` bytes without keeping them. `false` if they never came.
946    async fn discard(&mut self, mut n: u64, timeout: Duration) -> bool {
947        loop {
948            let take = (self.pending.len() as u64).min(n) as usize;
949            self.pending.drain(..take);
950            n -= take as u64;
951            if n == 0 {
952                return true;
953            }
954            match tokio::time::timeout(timeout, self.fill()).await {
955                Ok(Ok(k)) if k > 0 => {}
956                _ => return false,
957            }
958        }
959    }
960}