1pub 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
55const FIRST_COMMAND_TIMEOUT: Duration = Duration::from_secs(300);
62
63const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
67
68const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
72
73const CONNECTION_CAP_REFUSAL: &[u8] = b"OUT_OF_MEMORY\r\n";
76
77const 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 let task_owner = app_state.clone();
162 task_owner
163 .spawn_server_task(server_id, async move {
164 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#[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
203struct 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
246struct Tubes {
249 used: String,
250 watched: Vec<String>,
251}
252
253enum Next {
255 Continue,
256 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 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 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 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 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 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 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 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
726fn 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
840const 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 Line(String, usize),
863 Eof,
864 TimedOut,
865 TooLong,
867 Failed(std::io::Error),
868}
869
870struct 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 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 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 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 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 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}