Skip to main content

netget/server/nsq/
mod.rs

1//! NSQ broker (nsqd's TCP protocol, V2) — the model decides what is accepted and delivered.
2//!
3//! A client sends the `"  V2"` magic, then commands. NetGet answers IDENTIFY, NOP, CLS, TOUCH,
4//! AUTH, heartbeats and every malformed or out-of-state command itself; PUB/MPUB/DPUB raise
5//! `nsq_publish` and SUB raises `nsq_subscribe`, which the model accepts or refuses; RDY, FIN
6//! and REQ raise `nsq_ready`, `nsq_finish` and `nsq_requeue`, which the model answers with the
7//! messages the subscriber should receive, or with nothing.
8//!
9//! Six properties worth knowing before changing the loop:
10//!
11//! 1. **Sizes are judged before anything is allocated.** Every body's declared size, and an
12//!    MPUB's declared message count, is checked against nsqd's defaults by
13//!    [`wire::parse_command`] from the size field alone; a command line is capped at
14//!    [`wire::MAX_LINE`]. An overrun gets nsqd's own error and the connection closes.
15//! 2. **The model cannot write a frame, and cannot over-deliver.** Its answers are structured;
16//!    NetGet assigns each message its id and timestamp and sends no more than the client's RDY
17//!    count minus the messages in flight. The rest wait, in order, in a bounded per-connection
18//!    queue until a FIN or REQ makes room.
19//! 3. **Heartbeats run while the model thinks.** `_heartbeat_` is written on the negotiated
20//!    interval whether the loop is waiting for a command or for an answer, because a go-nsq
21//!    client times out its reads at twice the interval.
22//! 4. **Silence is measured between commands, not during answers.** A client silent for two
23//!    heartbeat intervals is closed (nsqd's rule); one that disabled heartbeats is held to
24//!    `idle_timeout_secs`. The clock restarts after each answer, so a publish parked for a human
25//!    is never closed for the time the human took.
26//! 5. **NetGet is not a queue.** Nothing published is stored or routed to other connections;
27//!    the model decides what each subscriber receives. NetGet keeps only the connection's flow
28//!    state: its subscription, RDY count, the messages in flight (for FIN/REQ/TOUCH) and the
29//!    queue of messages the model already chose.
30//! 6. **Every close lingers**, so an error frame written before closing over unread input is not
31//!    destroyed by an RST.
32pub mod actions;
33pub mod wire;
34
35use crate::llm::action_helper::call_llm;
36use crate::llm::actions::executor::ExecutionResult;
37use crate::llm::actions::protocol_trait::ActionResult;
38use crate::llm::ollama_client::OllamaClient;
39use crate::logging::emit::Log;
40use crate::protocol::{Event, EventType};
41use crate::server::connection::ConnectionId;
42use crate::state::app_state::AppState;
43use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
44use actions::{Answer, Delivery};
45use anyhow::Result;
46use serde_json::json;
47use std::collections::{BTreeMap, VecDeque};
48use std::net::SocketAddr;
49use std::sync::atomic::{AtomicU64, Ordering};
50use std::sync::Arc;
51use std::time::Duration;
52use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt, WriteHalf};
53use tokio::net::{TcpListener, TcpStream};
54use tokio::sync::mpsc;
55
56/// How long a new connection may send nothing, not even the magic. nsq clients send it at
57/// once; 300 s is the window a `manual` rule gives a human, for a NetGet TCP client parked on
58/// its operator. Lower it through `first_byte_timeout_secs` for a public listener.
59pub const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(300);
60
61/// nsqd's default heartbeat interval (half its 60 s client timeout).
62pub const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
63
64/// How long a client that disabled heartbeats may stay silent between commands.
65pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
66
67/// Concurrent connections admitted before new ones are refused — the house default.
68const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
69
70/// The most messages the model may have waiting for RDY capacity on one connection, and their
71/// total size. Past either, further deliveries are dropped and logged.
72pub const MAX_PENDING_MESSAGES: usize = 1000;
73pub const MAX_PENDING_BYTES: usize = 16 * 1024 * 1024;
74
75/// How much of a publish the model is shown: the first 20 bodies, each cut at 1000 bytes.
76const EVENT_MESSAGES: usize = 20;
77const EVENT_BODY_BYTES: usize = 1000;
78
79/// The fixed text a backend failure is answered with, by command and category. No error text
80/// reaches the peer.
81fn failed_text(command: &str, overloaded: bool) -> &'static str {
82    match (command, overloaded) {
83        ("MPUB", false) => "MPUB failed: broker backend unavailable",
84        ("MPUB", true) => "MPUB failed: broker backend at capacity",
85        ("DPUB", false) => "DPUB failed: broker backend unavailable",
86        ("DPUB", true) => "DPUB failed: broker backend at capacity",
87        ("SUB", false) => "SUB failed: broker backend unavailable",
88        ("SUB", true) => "SUB failed: broker backend at capacity",
89        (_, false) => "PUB failed: broker backend unavailable",
90        (_, true) => "PUB failed: broker backend at capacity",
91    }
92}
93
94/// The server's bounds, from the startup parameters.
95#[derive(Debug, Clone, Copy)]
96pub struct NsqConfig {
97    pub first_byte_timeout: Duration,
98    pub heartbeat_interval: Duration,
99    pub idle_timeout: Duration,
100}
101
102pub struct NsqServer;
103
104impl NsqServer {
105    pub async fn spawn_with_llm_actions(
106        listen_addr: SocketAddr,
107        llm_client: OllamaClient,
108        app_state: Arc<AppState>,
109        status_tx: mpsc::UnboundedSender<String>,
110        server_id: crate::state::ServerId,
111        config: NsqConfig,
112    ) -> Result<SocketAddr> {
113        let listener = TcpListener::bind(listen_addr).await?;
114        let local_addr = listener.local_addr()?;
115
116        Log::new(Some(&status_tx)).info(format!("NSQ broker listening on {}", local_addr));
117
118        let protocol = Arc::new(actions::NsqProtocol::new());
119        let ids = Arc::new(AtomicU64::new(0));
120        let task_registrar = app_state.clone();
121        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
122        let accept_handle = tokio::spawn(async move {
123            loop {
124                // A peer over the cap gets no bytes: nsqd has no refusal frame for it, and a
125                // frame the client did not ask for would be read as an answer.
126                match crate::server::accept_bounded::accept_bounded(
127                    &listener,
128                    &limiter,
129                    b"",
130                    "NSQ",
131                    Some(&status_tx),
132                )
133                .await
134                {
135                    Ok((socket, peer_addr, permit)) => {
136                        let connection_id =
137                            ConnectionId::new(app_state.get_next_unified_id().await);
138                        use crate::state::server::{
139                            ConnectionState as ServerConnectionState, ConnectionStatus,
140                            ProtocolConnectionInfo,
141                        };
142                        let now = crate::utils::clock::Instant::now();
143                        app_state
144                            .add_connection_to_server(
145                                server_id,
146                                ServerConnectionState {
147                                    id: connection_id,
148                                    remote_addr: peer_addr,
149                                    local_addr,
150                                    bytes_sent: 0,
151                                    bytes_received: 0,
152                                    packets_sent: 0,
153                                    packets_received: 0,
154                                    last_activity: now,
155                                    status: ConnectionStatus::Active,
156                                    status_changed_at: now,
157                                    protocol_info: ProtocolConnectionInfo::empty(),
158                                },
159                            )
160                            .await;
161                        let _ = status_tx.send("__UPDATE_UI__".to_string());
162                        Log::new(Some(&status_tx))
163                            .info(format!("NSQ client connected from {}", peer_addr));
164
165                        let session = Session {
166                            peer_addr,
167                            llm_client: llm_client.clone(),
168                            app_state: app_state.clone(),
169                            status_tx: status_tx.clone(),
170                            server_id,
171                            protocol: protocol.clone(),
172                            connection_id,
173                            config,
174                            ids: ids.clone(),
175                        };
176                        // Tracked, not detached: stop_server must abort this task too.
177                        let task_owner = app_state.clone();
178                        task_owner
179                            .spawn_server_task(server_id, async move {
180                                let _permit = permit;
181                                session.run(socket).await
182                            })
183                            .await;
184                    }
185                    Err(e) => {
186                        Log::new(Some(&status_tx)).error(format!("NSQ accept error: {}", e));
187                        break;
188                    }
189                }
190            }
191        });
192
193        task_registrar
194            .register_server_task(server_id, accept_handle)
195            .await;
196        Ok(local_addr)
197    }
198}
199
200type Writer = WriteHalf<TcpStream>;
201
202struct Session {
203    peer_addr: SocketAddr,
204    llm_client: OllamaClient,
205    app_state: Arc<AppState>,
206    status_tx: mpsc::UnboundedSender<String>,
207    server_id: crate::state::ServerId,
208    protocol: Arc<actions::NsqProtocol>,
209    connection_id: ConnectionId,
210    config: NsqConfig,
211    ids: Arc<AtomicU64>,
212}
213
214/// One connection's flow state. Nothing here outlives the connection.
215struct Flow {
216    heartbeat: Option<Duration>,
217    ticker: Option<tokio::time::Interval>,
218    identified: bool,
219    client_id: String,
220    user_agent: String,
221    sub: Option<(String, String)>,
222    closing: bool,
223    rdy: u64,
224    /// Message id → (body, attempts), for FIN/REQ/TOUCH and a requeue event's body.
225    in_flight: BTreeMap<String, (String, u16)>,
226    pending: VecDeque<Delivery>,
227    pending_bytes: usize,
228}
229
230fn ticker(every: Duration) -> tokio::time::Interval {
231    let mut t = tokio::time::interval_at(tokio::time::Instant::now() + every, every);
232    t.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
233    t
234}
235
236impl Flow {
237    fn new(heartbeat: Duration) -> Self {
238        Self {
239            heartbeat: Some(heartbeat),
240            ticker: Some(ticker(heartbeat)),
241            identified: false,
242            client_id: String::new(),
243            user_agent: String::new(),
244            sub: None,
245            closing: false,
246            rdy: 0,
247            in_flight: BTreeMap::new(),
248            pending: VecDeque::new(),
249            pending_bytes: 0,
250        }
251    }
252
253    /// How long the client may stay silent: two heartbeats, or the idle bound without them.
254    fn silence_window(&self, idle: Duration) -> Duration {
255        match self.heartbeat {
256            Some(hb) => hb * 2,
257            None => idle,
258        }
259    }
260
261    fn can_deliver(&self) -> u64 {
262        if self.sub.is_none() || self.closing {
263            return 0;
264        }
265        self.rdy.saturating_sub(self.in_flight.len() as u64)
266    }
267
268    fn topic(&self) -> (String, String) {
269        self.sub.clone().unwrap_or_default()
270    }
271}
272
273/// Wait for the next heartbeat tick, or forever when heartbeats are off.
274async fn tick(ticker: &mut Option<tokio::time::Interval>) {
275    match ticker {
276        Some(t) => {
277            t.tick().await;
278        }
279        None => std::future::pending::<()>().await,
280    }
281}
282
283/// Wait for the next injected command, or forever once the channel has closed.
284async fn next_injected(rx: &mut Option<mpsc::Receiver<ClientCommand>>) -> Option<ClientCommand> {
285    match rx {
286        Some(r) => r.recv().await,
287        None => std::future::pending().await,
288    }
289}
290
291enum ReadError {
292    Eof,
293    TimedOut,
294    Wire(wire::WireError),
295    Io(std::io::Error),
296}
297
298enum Step {
299    Continue,
300    Close,
301}
302
303enum Wake {
304    Command(Result<(wire::Command, usize), ReadError>),
305    Heartbeat,
306    Injected(Option<ClientCommand>),
307}
308
309impl Session {
310    async fn run(self, socket: TcpStream) {
311        let (mut reader, mut writer) = tokio::io::split(socket);
312        let peer_rx = crate::server::peer_support::register_peer_channel(
313            &self.app_state,
314            self.server_id,
315            self.connection_id.as_u32(),
316        )
317        .await;
318
319        {
320            let mut framer = Framer::new(&mut reader);
321            self.session(&mut framer, &mut writer, peer_rx).await;
322        }
323
324        self.app_state
325            .remove_peer_handle(self.server_id, self.connection_id.as_u32())
326            .await;
327        let _ = writer.shutdown().await;
328        linger(&mut reader).await;
329        self.app_state
330            .update_connection_status(
331                self.server_id,
332                self.connection_id,
333                crate::state::server::ConnectionStatus::Closed,
334            )
335            .await;
336        let _ = self.status_tx.send("__UPDATE_UI__".to_string());
337    }
338
339    async fn write(&self, writer: &mut Writer, data: &[u8]) -> bool {
340        let ok = writer.write_all(data).await.is_ok() && writer.flush().await.is_ok();
341        if ok {
342            self.app_state
343                .update_connection_stats(
344                    self.server_id,
345                    self.connection_id,
346                    None,
347                    Some(data.len() as u64),
348                    None,
349                    Some(1),
350                )
351                .await;
352        }
353        ok
354    }
355
356    /// Write an error frame; `Close` when nsqd would close after it.
357    async fn refuse(&self, writer: &mut Writer, code: &'static str, message: &str) -> Step {
358        let written = self.write(writer, &wire::error_frame(code, message)).await;
359        if written && !wire::is_fatal(code) {
360            Step::Continue
361        } else {
362            Step::Close
363        }
364    }
365
366    async fn session<R: AsyncRead + Unpin>(
367        &self,
368        framer: &mut Framer<'_, R>,
369        writer: &mut Writer,
370        peer_rx: mpsc::Receiver<ClientCommand>,
371    ) {
372        let log = Log::new(Some(&self.status_tx));
373        let magic_deadline = tokio::time::Instant::now() + self.config.first_byte_timeout;
374        match framer.magic(magic_deadline).await {
375            Ok(true) => {}
376            Ok(false) => {
377                log.warn(format!(
378                    "NSQ peer {} did not send the V2 magic decision=fail_closed_bad_magic",
379                    self.peer_addr
380                ));
381                let _ = self
382                    .write(writer, &wire::error_frame("E_BAD_PROTOCOL", ""))
383                    .await;
384                return;
385            }
386            Err(ReadError::TimedOut) => {
387                log.info(format!(
388                    "NSQ client {} sent nothing within {}s; closing",
389                    self.peer_addr,
390                    self.config.first_byte_timeout.as_secs()
391                ));
392                return;
393            }
394            Err(_) => return,
395        }
396
397        let mut flow = Flow::new(self.config.heartbeat_interval);
398        let mut peer_rx = Some(peer_rx);
399        let mut answered_at = tokio::time::Instant::now();
400        let heartbeat = wire::response_frame(wire::HEARTBEAT);
401
402        loop {
403            let window = flow.silence_window(self.config.idle_timeout);
404            let deadline = framer.last_read.max(answered_at) + window;
405            let wake = tokio::select! {
406                r = framer.next_command(deadline) => Wake::Command(r),
407                _ = tick(&mut flow.ticker) => Wake::Heartbeat,
408                c = next_injected(&mut peer_rx) => Wake::Injected(c),
409            };
410            let step = match wake {
411                Wake::Heartbeat => {
412                    if self.write(writer, &heartbeat).await {
413                        Step::Continue
414                    } else {
415                        Step::Close
416                    }
417                }
418                Wake::Injected(None) => {
419                    peer_rx = None;
420                    Step::Continue
421                }
422                Wake::Injected(Some(command)) => self.inject(command, &mut flow, writer).await,
423                Wake::Command(Err(ReadError::Eof)) => {
424                    log.info(format!("NSQ client {} disconnected", self.peer_addr));
425                    Step::Close
426                }
427                Wake::Command(Err(ReadError::TimedOut)) => {
428                    log.info(format!(
429                        "NSQ client {} silent for {}s decision=fail_closed_idle; closing",
430                        self.peer_addr,
431                        window.as_secs()
432                    ));
433                    Step::Close
434                }
435                Wake::Command(Err(ReadError::Io(e))) => {
436                    log.error(format!("NSQ read error from {}: {}", self.peer_addr, e));
437                    Step::Close
438                }
439                Wake::Command(Err(ReadError::Wire(e))) => {
440                    log.warn(format!(
441                        "NSQ command from {} refused: {} {} decision={}",
442                        self.peer_addr,
443                        e.code,
444                        e.message,
445                        e.decision()
446                    ));
447                    let _ = self.write(writer, &e.to_frame()).await;
448                    Step::Close
449                }
450                Wake::Command(Ok((command, used))) => {
451                    self.app_state
452                        .update_connection_stats(
453                            self.server_id,
454                            self.connection_id,
455                            Some(used as u64),
456                            None,
457                            Some(1),
458                            None,
459                        )
460                        .await;
461                    let step = self.command(command, &mut flow, writer).await;
462                    answered_at = tokio::time::Instant::now();
463                    step
464                }
465            };
466            if let Step::Close = step {
467                return;
468            }
469        }
470    }
471
472    /// Answer one command.
473    async fn command(&self, command: wire::Command, flow: &mut Flow, writer: &mut Writer) -> Step {
474        use wire::Command as C;
475        let log = Log::new(Some(&self.status_tx));
476        let name = command.name();
477        let subscribed_or_closing = flow.sub.is_some() || flow.closing;
478        match command {
479            C::Nop => Step::Continue,
480            C::Identify(body) => {
481                if flow.identified {
482                    return self
483                        .refuse(writer, "E_INVALID", "cannot IDENTIFY in current state")
484                        .await;
485                }
486                let identify = match wire::parse_identify(&body) {
487                    Ok(i) => i,
488                    Err(e) => {
489                        log.warn(format!(
490                            "NSQ IDENTIFY from {} refused: {} decision=fail_closed_bad_identify",
491                            self.peer_addr, e.message
492                        ));
493                        let _ = self.write(writer, &e.to_frame()).await;
494                        return Step::Close;
495                    }
496                };
497                flow.identified = true;
498                match identify.heartbeat_ms {
499                    None => {
500                        flow.heartbeat = None;
501                        flow.ticker = None;
502                    }
503                    Some(0) => {}
504                    Some(ms) => {
505                        let every = Duration::from_millis(ms as u64);
506                        flow.heartbeat = Some(every);
507                        flow.ticker = Some(ticker(every));
508                    }
509                }
510                flow.client_id = identify.client_id;
511                flow.user_agent = identify.user_agent;
512                let reply = if identify.feature_negotiation {
513                    wire::identify_response(identify.msg_timeout_ms)
514                } else {
515                    b"OK".to_vec()
516                };
517                log.debug(format!(
518                    "NSQ IDENTIFY from {} (user_agent {:?}, heartbeat {:?})",
519                    self.peer_addr, flow.user_agent, flow.heartbeat
520                ));
521                self.step(self.write(writer, &wire::response_frame(&reply)).await)
522            }
523            C::Auth(_) => {
524                log.warn(format!(
525                    "NSQ AUTH from {} refused decision=fail_closed_auth_disabled",
526                    self.peer_addr
527                ));
528                self.refuse(writer, "E_AUTH_DISABLED", "AUTH disabled")
529                    .await
530            }
531            C::Cls => {
532                if flow.sub.is_none() {
533                    return self
534                        .refuse(writer, "E_INVALID", "cannot CLS in current state")
535                        .await;
536                }
537                flow.closing = true;
538                flow.pending.clear();
539                flow.pending_bytes = 0;
540                self.step(
541                    self.write(writer, &wire::response_frame(b"CLOSE_WAIT"))
542                        .await,
543                )
544            }
545            C::Touch(id) => {
546                if !subscribed_or_closing {
547                    return self
548                        .refuse(writer, "E_INVALID", "cannot TOUCH in current state")
549                        .await;
550                }
551                if flow.in_flight.contains_key(&id) {
552                    Step::Continue
553                } else {
554                    self.refuse(
555                        writer,
556                        "E_TOUCH_FAILED",
557                        &format!("TOUCH {id} failed ID not in flight"),
558                    )
559                    .await
560                }
561            }
562            C::Sub { topic, channel } => {
563                if flow.sub.is_some() || flow.closing {
564                    return self
565                        .refuse(writer, "E_INVALID", "cannot SUB in current state")
566                        .await;
567                }
568                self.subscribe(topic, channel, flow, writer).await
569            }
570            C::Rdy(count) => {
571                if flow.closing {
572                    return Step::Continue;
573                }
574                if flow.sub.is_none() {
575                    return self
576                        .refuse(writer, "E_INVALID", "cannot RDY in current state")
577                        .await;
578                }
579                flow.rdy = count;
580                if !self.flush(flow, writer).await {
581                    return Step::Close;
582                }
583                if flow.can_deliver() == 0 {
584                    return Step::Continue;
585                }
586                let (topic, _) = flow.topic();
587                let mut data = self.flow_data(flow);
588                data["count"] = json!(count);
589                data["answer_with"] = json!(actions::deliver_answer_with(
590                    &topic,
591                    flow.can_deliver(),
592                    flow.pending.len()
593                ));
594                self.flow_event(&actions::NSQ_READY_EVENT, data, name, flow, writer)
595                    .await
596            }
597            C::Fin(id) => {
598                if !subscribed_or_closing {
599                    return self
600                        .refuse(writer, "E_INVALID", "cannot FIN in current state")
601                        .await;
602                }
603                if flow.in_flight.remove(&id).is_none() {
604                    return self
605                        .refuse(
606                            writer,
607                            "E_FIN_FAILED",
608                            &format!("FIN {id} failed ID not in flight"),
609                        )
610                        .await;
611                }
612                if !self.flush(flow, writer).await {
613                    return Step::Close;
614                }
615                if flow.closing {
616                    return Step::Continue;
617                }
618                let (topic, _) = flow.topic();
619                let mut data = self.flow_data(flow);
620                data["message_id"] = json!(id);
621                data["answer_with"] = json!(actions::deliver_answer_with(
622                    &topic,
623                    flow.can_deliver(),
624                    flow.pending.len()
625                ));
626                self.flow_event(&actions::NSQ_FINISH_EVENT, data, name, flow, writer)
627                    .await
628            }
629            C::Req { id, timeout_ms } => {
630                if !subscribed_or_closing {
631                    return self
632                        .refuse(writer, "E_INVALID", "cannot REQ in current state")
633                        .await;
634                }
635                let Some((body, attempts)) = flow.in_flight.remove(&id) else {
636                    return self
637                        .refuse(
638                            writer,
639                            "E_REQ_FAILED",
640                            &format!("REQ {id} failed ID not in flight"),
641                        )
642                        .await;
643                };
644                if !self.flush(flow, writer).await {
645                    return Step::Close;
646                }
647                if flow.closing {
648                    return Step::Continue;
649                }
650                let mut data = self.flow_data(flow);
651                data["message_id"] = json!(id);
652                data["timeout_ms"] = json!(timeout_ms);
653                data["attempts"] = json!(attempts);
654                data["body"] = json!(crate::utils::truncate_for_llm(&body, EVENT_BODY_BYTES));
655                data["answer_with"] = json!(actions::requeue_answer_with(attempts));
656                self.flow_event(&actions::NSQ_REQUEUE_EVENT, data, name, flow, writer)
657                    .await
658            }
659            C::Pub { topic, body } => {
660                self.publish(name, topic, vec![body], None, flow, writer)
661                    .await
662            }
663            C::Dpub {
664                topic,
665                defer_ms,
666                body,
667            } => {
668                self.publish(name, topic, vec![body], Some(defer_ms), flow, writer)
669                    .await
670            }
671            C::Mpub { topic, messages } => {
672                self.publish(name, topic, messages, None, flow, writer)
673                    .await
674            }
675        }
676    }
677
678    fn step(&self, written: bool) -> Step {
679        if written {
680            Step::Continue
681        } else {
682            Step::Close
683        }
684    }
685
686    fn flow_data(&self, flow: &Flow) -> serde_json::Value {
687        let (topic, channel) = flow.topic();
688        json!({
689            "topic": topic,
690            "channel": channel,
691            "ready": flow.rdy,
692            "in_flight": flow.in_flight.len(),
693            "pending": flow.pending.len(),
694            "can_deliver": flow.can_deliver(),
695        })
696    }
697
698    /// Ask the model, writing heartbeats while it thinks.
699    async fn ask(
700        &self,
701        event: &Event,
702        flow: &mut Flow,
703        writer: &mut Writer,
704    ) -> Result<ExecutionResult> {
705        let heartbeat = wire::response_frame(wire::HEARTBEAT);
706        let call = call_llm(
707            &self.llm_client,
708            &self.app_state,
709            self.server_id,
710            Some(self.connection_id),
711            event,
712            self.protocol.as_ref(),
713        );
714        tokio::pin!(call);
715        loop {
716            tokio::select! {
717                result = &mut call => return result,
718                _ = tick(&mut flow.ticker) => {
719                    // A failed write is found again by the answer's own write.
720                    let _ = self.write(writer, &heartbeat).await;
721                }
722            }
723        }
724    }
725
726    /// Flatten the model's results into answers, in order.
727    fn answers(&self, result: ExecutionResult) -> Vec<Answer> {
728        let log = Log::new(Some(&self.status_tx));
729        for message in &result.messages {
730            log.info(message);
731        }
732        let mut out = Vec::new();
733        let mut stack = result.protocol_results;
734        stack.reverse();
735        while let Some(item) = stack.pop() {
736            match item {
737                ActionResult::Multiple(inner) => stack.extend(inner.into_iter().rev()),
738                other => match Answer::from_result(&other) {
739                    Some(answer) => out.push(answer),
740                    None => log.debug(format!("NSQ: ignoring non-NSQ result {other:?}")),
741                },
742            }
743        }
744        out
745    }
746
747    async fn subscribe(
748        &self,
749        topic: String,
750        channel: String,
751        flow: &mut Flow,
752        writer: &mut Writer,
753    ) -> Step {
754        let log = Log::new(Some(&self.status_tx));
755        let event = Event::new(
756            &actions::NSQ_SUBSCRIBE_EVENT,
757            json!({
758                "topic": topic,
759                "channel": channel,
760                "ephemeral": wire::is_ephemeral(&channel),
761                "client_id": flow.client_id,
762                "user_agent": flow.user_agent,
763                "answer_with": actions::subscribe_answer_with(&topic, &channel),
764            }),
765        );
766        let answers = match self.ask(&event, flow, writer).await {
767            Ok(result) => self.answers(result),
768            Err(e) => {
769                let failure = crate::utils::WireFailure::classify(&e);
770                log.warn(format!(
771                    "NSQ SUB {topic} {channel} from {} decision=fail_closed_llm_error category={:?}",
772                    self.peer_addr, failure
773                ));
774                log.debug(format!("NSQ LLM call failed: {}", e));
775                let text = failed_text("SUB", failure.is_overloaded());
776                return self.refuse(writer, "E_INVALID", text).await;
777            }
778        };
779        let step = match reply_of(&answers) {
780            Some(Answer::Ok) => {
781                log.info(format!(
782                    "NSQ SUB {topic} {channel} from {} decision=model_answer",
783                    self.peer_addr
784                ));
785                flow.sub = Some((topic, channel));
786                self.step(self.write(writer, &wire::response_frame(b"OK")).await)
787            }
788            Some(Answer::Error { code, message }) => {
789                log.info(format!(
790                    "NSQ SUB {topic} {channel} from {} decision=model_reject code={code}",
791                    self.peer_addr
792                ));
793                self.refuse(writer, static_code(code), message).await
794            }
795            _ => {
796                log.warn(format!(
797                    "NSQ SUB {topic} {channel} from {} decision=model_silent; refusing",
798                    self.peer_addr
799                ));
800                return self.refuse(writer, "E_INVALID", "SUB failed").await;
801            }
802        };
803        self.finish_answers(step, &answers, flow, writer).await
804    }
805
806    async fn publish(
807        &self,
808        command: &'static str,
809        topic: String,
810        bodies: Vec<Vec<u8>>,
811        defer_ms: Option<i64>,
812        flow: &mut Flow,
813        writer: &mut Writer,
814    ) -> Step {
815        let log = Log::new(Some(&self.status_tx));
816        let failed_code: &'static str = match command {
817            "MPUB" => "E_MPUB_FAILED",
818            "DPUB" => "E_DPUB_FAILED",
819            _ => "E_PUB_FAILED",
820        };
821        let total_bytes: usize = bodies.iter().map(Vec::len).sum();
822        let shown: Vec<String> = bodies
823            .iter()
824            .take(EVENT_MESSAGES)
825            .map(|b| crate::utils::truncate_for_llm(&String::from_utf8_lossy(b), EVENT_BODY_BYTES))
826            .collect();
827        let mut data = json!({
828            "command": command,
829            "topic": topic,
830            "messages": shown,
831            "message_count": bodies.len(),
832            "total_bytes": total_bytes,
833            "answer_with": actions::publish_answer_with(command, &topic, bodies.len()),
834        });
835        if let Some(defer) = defer_ms {
836            data["defer_ms"] = json!(defer);
837        }
838        let event = Event::new(&actions::NSQ_PUBLISH_EVENT, data);
839        let answers = match self.ask(&event, flow, writer).await {
840            Ok(result) => self.answers(result),
841            Err(e) => {
842                let failure = crate::utils::WireFailure::classify(&e);
843                log.warn(format!(
844                    "NSQ {command} {topic} from {} decision=fail_closed_llm_error category={:?}",
845                    self.peer_addr, failure
846                ));
847                log.debug(format!("NSQ LLM call failed: {}", e));
848                let text = failed_text(command, failure.is_overloaded());
849                return self.refuse(writer, failed_code, text).await;
850            }
851        };
852        let step = match reply_of(&answers) {
853            Some(Answer::Ok) => {
854                log.info(format!(
855                    "NSQ {command} {topic} ({} message(s)) from {} decision=model_answer",
856                    bodies.len(),
857                    self.peer_addr
858                ));
859                self.step(self.write(writer, &wire::response_frame(b"OK")).await)
860            }
861            Some(Answer::Error { code, message }) => {
862                log.info(format!(
863                    "NSQ {command} {topic} from {} decision=model_reject code={code}",
864                    self.peer_addr
865                ));
866                self.refuse(writer, static_code(code), message).await
867            }
868            _ => {
869                log.warn(format!(
870                    "NSQ {command} {topic} from {} decision=model_silent; refusing",
871                    self.peer_addr
872                ));
873                return self
874                    .refuse(writer, failed_code, &format!("{command} failed"))
875                    .await;
876            }
877        };
878        self.finish_answers(step, &answers, flow, writer).await
879    }
880
881    /// After a command's one reply: queue any deliveries and honour a close.
882    async fn finish_answers(
883        &self,
884        step: Step,
885        answers: &[Answer],
886        flow: &mut Flow,
887        writer: &mut Writer,
888    ) -> Step {
889        if let Step::Close = step {
890            return Step::Close;
891        }
892        let deliveries: Vec<Delivery> = answers
893            .iter()
894            .filter_map(|a| match a {
895                Answer::Deliver(d) => Some(d.clone()),
896                _ => None,
897            })
898            .flatten()
899            .collect();
900        if !deliveries.is_empty() {
901            self.enqueue(flow, deliveries);
902            if !self.flush(flow, writer).await {
903                return Step::Close;
904            }
905        }
906        if answers.iter().any(|a| matches!(a, Answer::Close)) {
907            return Step::Close;
908        }
909        Step::Continue
910    }
911
912    /// RDY, FIN and REQ: the model delivers messages, refuses, or says nothing. None of the
913    /// three takes a reply in nsqd, so nothing is written unless the model delivers or refuses.
914    async fn flow_event(
915        &self,
916        event_type: &'static EventType,
917        data: serde_json::Value,
918        command: &'static str,
919        flow: &mut Flow,
920        writer: &mut Writer,
921    ) -> Step {
922        let log = Log::new(Some(&self.status_tx));
923        let event = Event::new(event_type, data);
924        let answers = match self.ask(&event, flow, writer).await {
925            Ok(result) => self.answers(result),
926            Err(e) => {
927                log.warn(format!(
928                    "NSQ {command} from {} decision=fail_closed_llm_error category={:?}; \
929                     delivering nothing",
930                    self.peer_addr,
931                    crate::utils::WireFailure::classify(&e)
932                ));
933                log.debug(format!("NSQ LLM call failed: {}", e));
934                return Step::Continue;
935            }
936        };
937        if let Some(Answer::Error { code, message }) =
938            answers.iter().find(|a| matches!(a, Answer::Error { .. }))
939        {
940            log.info(format!(
941                "NSQ {command} from {} decision=model_reject code={code}",
942                self.peer_addr
943            ));
944            if let Step::Close = self.refuse(writer, static_code(code), message).await {
945                return Step::Close;
946            }
947        }
948        let offered: usize = answers
949            .iter()
950            .map(|a| match a {
951                Answer::Deliver(d) => d.len(),
952                _ => 0,
953            })
954            .sum();
955        let step = self
956            .finish_answers(Step::Continue, &answers, flow, writer)
957            .await;
958        if offered > 0 {
959            log.info(format!(
960                "NSQ {command} from {} decision=model_answer offered={offered} in_flight={} \
961                 pending={}",
962                self.peer_addr,
963                flow.in_flight.len(),
964                flow.pending.len()
965            ));
966        } else if !answers.iter().any(|a| matches!(a, Answer::Error { .. })) {
967            log.info(format!(
968                "NSQ {command} from {} decision=model_silent (nothing delivered)",
969                self.peer_addr
970            ));
971        }
972        step
973    }
974
975    /// Queue the model's deliveries behind any already waiting, within the pending bounds.
976    fn enqueue(&self, flow: &mut Flow, deliveries: Vec<Delivery>) {
977        let log = Log::new(Some(&self.status_tx));
978        if flow.sub.is_none() || flow.closing {
979            log.warn(format!(
980                "NSQ: {} message(s) for {} dropped - the connection is not subscribed{} \
981                 decision=fail_closed_not_subscribed",
982                deliveries.len(),
983                self.peer_addr,
984                if flow.closing { " (closing)" } else { "" }
985            ));
986            return;
987        }
988        let mut dropped = 0usize;
989        for delivery in deliveries {
990            if flow.pending.len() >= MAX_PENDING_MESSAGES
991                || flow.pending_bytes + delivery.body.len() > MAX_PENDING_BYTES
992            {
993                dropped += 1;
994                continue;
995            }
996            flow.pending_bytes += delivery.body.len();
997            flow.pending.push_back(delivery);
998        }
999        if dropped > 0 {
1000            log.warn(format!(
1001                "NSQ: {dropped} message(s) for {} dropped over the pending bound ({} messages, \
1002                 {} bytes) decision=fail_closed_pending_full",
1003                self.peer_addr, MAX_PENDING_MESSAGES, MAX_PENDING_BYTES
1004            ));
1005        }
1006    }
1007
1008    /// Send waiting messages while the client's RDY count allows. `false` if a write failed.
1009    async fn flush(&self, flow: &mut Flow, writer: &mut Writer) -> bool {
1010        self.flush_counting(flow, writer).await.is_some()
1011    }
1012
1013    /// [`Session::flush`], returning the bytes written; `None` if a write failed.
1014    async fn flush_counting(&self, flow: &mut Flow, writer: &mut Writer) -> Option<usize> {
1015        let mut written = 0usize;
1016        while flow.can_deliver() > 0 {
1017            let Some(delivery) = flow.pending.pop_front() else {
1018                break;
1019            };
1020            flow.pending_bytes -= delivery.body.len();
1021            let n = self.ids.fetch_add(1, Ordering::SeqCst) + 1;
1022            let id = wire::message_id_for(n);
1023            let timestamp = crate::utils::clock::SystemTime::now()
1024                .duration_since(crate::utils::clock::UNIX_EPOCH)
1025                .map(|d| d.as_nanos() as i64)
1026                .unwrap_or(0);
1027            let frame =
1028                wire::message_frame(timestamp, delivery.attempts, &id, delivery.body.as_bytes());
1029            if !self.write(writer, &frame).await {
1030                return None;
1031            }
1032            written += frame.len();
1033            flow.in_flight.insert(
1034                String::from_utf8_lossy(&id).into_owned(),
1035                (delivery.body, delivery.attempts),
1036            );
1037        }
1038        Some(written)
1039    }
1040
1041    /// An action injected from the dashboard (or `send_to_peer`), outside any event.
1042    async fn inject(&self, command: ClientCommand, flow: &mut Flow, writer: &mut Writer) -> Step {
1043        let action = command.action.clone();
1044        let result = crate::llm::actions::executor::execute_actions(
1045            vec![action.clone()],
1046            &self.app_state,
1047            Some(self.protocol.as_ref()),
1048            Some(self.server_id),
1049            None,
1050        )
1051        .await;
1052        let (outcome, step) = match result {
1053            Err(e) => (Err(e), Step::Continue),
1054            Ok(result) => {
1055                let failures = result.failure_summary();
1056                let answers = self.answers(result);
1057                if answers.is_empty() {
1058                    let error = failures.unwrap_or_else(|| "no NSQ action".to_string());
1059                    (Ok(ClientSendOutcome::Rejected { error }), Step::Continue)
1060                } else {
1061                    let mut bytes = 0usize;
1062                    let mut step = Step::Continue;
1063                    for answer in &answers {
1064                        let frame = match answer {
1065                            Answer::Ok => wire::response_frame(b"OK"),
1066                            Answer::Error { code, message } => {
1067                                let code = static_code(code);
1068                                if wire::is_fatal(code) {
1069                                    step = Step::Close;
1070                                }
1071                                wire::error_frame(code, message)
1072                            }
1073                            Answer::Close => {
1074                                step = Step::Close;
1075                                continue;
1076                            }
1077                            Answer::Deliver(d) => {
1078                                self.enqueue(flow, d.clone());
1079                                match self.flush_counting(flow, writer).await {
1080                                    Some(n) => bytes += n,
1081                                    None => step = Step::Close,
1082                                }
1083                                continue;
1084                            }
1085                        };
1086                        if self.write(writer, &frame).await {
1087                            bytes += frame.len();
1088                        } else {
1089                            step = Step::Close;
1090                        }
1091                    }
1092                    let outcome = match step {
1093                        Step::Close => ClientSendOutcome::Disconnected,
1094                        Step::Continue if bytes > 0 => {
1095                            ClientSendOutcome::Sent { bytes_sent: bytes }
1096                        }
1097                        Step::Continue => ClientSendOutcome::Executed {
1098                            detail: format!("queued; {} pending for RDY", flow.pending.len()),
1099                        },
1100                    };
1101                    (Ok(outcome), step)
1102                }
1103            }
1104        };
1105        let outcome_json = match &outcome {
1106            Ok(o) => serde_json::to_value(o).unwrap_or(serde_json::Value::Null),
1107            Err(e) => json!({"error": e.to_string()}),
1108        };
1109        self.app_state
1110            .record_access_log(
1111                crate::state::AccessLogOwner::Server(self.server_id.as_u32()),
1112                "NSQ",
1113                Some(self.connection_id.as_u32()),
1114                "injected_action",
1115                action,
1116                vec![outcome_json],
1117            )
1118            .await;
1119        let _ = self.status_tx.send("__UPDATE_UI__".to_string());
1120        let _ = command.reply_tx.send(outcome);
1121        step
1122    }
1123}
1124
1125/// The first reply-shaped answer (OK or an error), in the model's order.
1126fn reply_of(answers: &[Answer]) -> Option<&Answer> {
1127    answers
1128        .iter()
1129        .find(|a| matches!(a, Answer::Ok | Answer::Error { .. }))
1130}
1131
1132/// The executor accepts only codes from [`wire::ERROR_CODES`]; map back to the static string.
1133fn static_code(code: &str) -> &'static str {
1134    wire::ERROR_CODES
1135        .iter()
1136        .copied()
1137        .find(|c| *c == code)
1138        .unwrap_or("E_INVALID")
1139}
1140
1141/// Keep reading briefly after the last reply and the half-close, so the close is a FIN rather
1142/// than an RST that could destroy that reply. Bounded both ways.
1143const LINGER_TIME: Duration = Duration::from_secs(2);
1144const LINGER_BYTES: usize = 64 * 1024;
1145
1146async fn linger<R: AsyncRead + Unpin>(reader: &mut R) {
1147    let deadline = tokio::time::Instant::now() + LINGER_TIME;
1148    let mut sink = [0u8; 4096];
1149    let mut drained = 0usize;
1150    while drained < LINGER_BYTES {
1151        match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
1152            Ok(Ok(n)) if n > 0 => drained += n,
1153            _ => break,
1154        }
1155    }
1156}
1157
1158/// Reads the magic, then commands, keeping anything past what was asked for. Parsing is a pure
1159/// function of the buffer, so dropping a `next_command` future (a heartbeat or an injected
1160/// action won the race) loses nothing.
1161struct Framer<'a, R> {
1162    reader: &'a mut R,
1163    pending: Vec<u8>,
1164    chunk: Vec<u8>,
1165    last_read: tokio::time::Instant,
1166}
1167
1168impl<'a, R: AsyncRead + Unpin> Framer<'a, R> {
1169    fn new(reader: &'a mut R) -> Self {
1170        Self {
1171            reader,
1172            pending: Vec::new(),
1173            chunk: vec![0u8; 16 * 1024],
1174            last_read: tokio::time::Instant::now(),
1175        }
1176    }
1177
1178    async fn fill(&mut self, deadline: tokio::time::Instant) -> Result<(), ReadError> {
1179        match tokio::time::timeout_at(deadline, self.reader.read(&mut self.chunk)).await {
1180            Err(_) => Err(ReadError::TimedOut),
1181            Ok(Ok(0)) => Err(ReadError::Eof),
1182            Ok(Ok(n)) => {
1183                self.pending.extend_from_slice(&self.chunk[..n]);
1184                self.last_read = tokio::time::Instant::now();
1185                Ok(())
1186            }
1187            Ok(Err(e)) => Err(ReadError::Io(e)),
1188        }
1189    }
1190
1191    /// `Ok(true)` for the V2 magic, `Ok(false)` for anything else.
1192    async fn magic(&mut self, deadline: tokio::time::Instant) -> Result<bool, ReadError> {
1193        while self.pending.len() < wire::MAGIC_V2.len() {
1194            self.fill(deadline).await?;
1195        }
1196        let ok = &self.pending[..4] == wire::MAGIC_V2;
1197        self.pending.drain(..4);
1198        Ok(ok)
1199    }
1200
1201    async fn next_command(
1202        &mut self,
1203        deadline: tokio::time::Instant,
1204    ) -> Result<(wire::Command, usize), ReadError> {
1205        loop {
1206            match wire::parse_command(&self.pending) {
1207                Ok(Some((command, used))) => {
1208                    self.pending.drain(..used);
1209                    return Ok((command, used));
1210                }
1211                Ok(None) => self.fill(deadline).await?,
1212                Err(e) => return Err(ReadError::Wire(e)),
1213            }
1214        }
1215    }
1216}