1pub mod actions;
5pub mod wire;
6
7use crate::llm::action_helper::call_llm;
8use crate::llm::actions::protocol_trait::ActionResult;
9use crate::logging::emit::Log;
10use crate::protocol::{Event, SpawnContext};
11use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS};
12use crate::server::connection::ConnectionId;
13use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
14use anyhow::Result;
15use serde_json::{json, Map, Value};
16use std::collections::HashMap;
17use std::future::Future;
18use std::net::SocketAddr;
19use std::pin::Pin;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::{Arc, LazyLock};
22use std::task::{Context, Poll};
23use std::time::Duration;
24use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt, WriteHalf};
25use tokio::net::{TcpListener, TcpStream};
26use tokio::sync::{Mutex, Notify};
27
28pub const MAX_REQUEST_HEAD_BYTES: usize = 16 * 1024;
30pub const HEAD_TIMEOUT: Duration = Duration::from_secs(10);
32pub const MAX_KEEPALIVE_SECS: u64 = 3600;
33pub const SUBSCRIBER_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
38
39type Writer = Arc<Mutex<StreamWriter>>;
40
41#[derive(Default)]
44struct Stall {
45 tripped: AtomicBool,
46 notify: Notify,
47}
48
49impl Stall {
50 fn trip(&self) {
51 if !self.tripped.swap(true, Ordering::SeqCst) {
52 self.notify.notify_one();
53 }
54 }
55
56 fn is_tripped(&self) -> bool {
57 self.tripped.load(Ordering::SeqCst)
58 }
59}
60
61fn stalled_error() -> std::io::Error {
62 std::io::Error::new(
63 std::io::ErrorKind::TimedOut,
64 format!(
65 "the peer took no bytes for {}s",
66 SUBSCRIBER_WRITE_TIMEOUT.as_secs()
67 ),
68 )
69}
70
71pub struct StreamWriter {
77 inner: WriteHalf<TcpStream>,
78 stall: Arc<Stall>,
79 deadline: Option<Pin<Box<tokio::time::Sleep>>>,
80}
81
82impl StreamWriter {
83 fn guard<T>(
84 &mut self,
85 cx: &mut Context<'_>,
86 poll: Poll<std::io::Result<T>>,
87 ) -> Poll<std::io::Result<T>> {
88 match poll {
89 Poll::Ready(r) => {
90 self.deadline = None;
91 Poll::Ready(r)
92 }
93 Poll::Pending => {
94 let sleep = self
95 .deadline
96 .get_or_insert_with(|| Box::pin(tokio::time::sleep(SUBSCRIBER_WRITE_TIMEOUT)));
97 if sleep.as_mut().poll(cx).is_ready() {
98 self.deadline = None;
99 self.stall.trip();
100 return Poll::Ready(Err(stalled_error()));
101 }
102 Poll::Pending
103 }
104 }
105 }
106}
107
108impl AsyncWrite for StreamWriter {
109 fn poll_write(
110 self: Pin<&mut Self>,
111 cx: &mut Context<'_>,
112 buf: &[u8],
113 ) -> Poll<std::io::Result<usize>> {
114 let this = self.get_mut();
115 if this.stall.is_tripped() {
116 return Poll::Ready(Err(stalled_error()));
117 }
118 let poll = Pin::new(&mut this.inner).poll_write(cx, buf);
119 this.guard(cx, poll)
120 }
121
122 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
123 let this = self.get_mut();
124 if this.stall.is_tripped() {
125 return Poll::Ready(Err(stalled_error()));
126 }
127 let poll = Pin::new(&mut this.inner).poll_flush(cx);
128 this.guard(cx, poll)
129 }
130
131 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
133 let this = self.get_mut();
134 let poll = Pin::new(&mut this.inner).poll_shutdown(cx);
135 this.guard(cx, poll)
136 }
137}
138
139struct Subscriber {
140 path: String,
141 writer: Writer,
142 stall: Arc<Stall>,
143}
144
145async fn deliver(writer: &Writer, bytes: &[u8]) -> bool {
148 let write = async {
149 let mut w = writer.lock().await;
150 w.write_all(bytes).await?;
151 w.flush().await
152 };
153 matches!(
154 tokio::time::timeout(SUBSCRIBER_WRITE_TIMEOUT, write).await,
155 Ok(Ok(()))
156 )
157}
158
159async fn shut(writer: &Writer) {
161 let _ = tokio::time::timeout(SUBSCRIBER_WRITE_TIMEOUT, async {
162 writer.lock().await.shutdown().await
163 })
164 .await;
165}
166
167static HUBS: LazyLock<std::sync::Mutex<HashMap<SocketAddr, Arc<Mutex<HashMap<u32, Subscriber>>>>>> =
171 LazyLock::new(Default::default);
172
173fn hub(server: SocketAddr) -> Arc<Mutex<HashMap<u32, Subscriber>>> {
174 HUBS.lock()
175 .unwrap_or_else(|e| e.into_inner())
176 .entry(server)
177 .or_default()
178 .clone()
179}
180
181struct HubGuard(SocketAddr);
183impl Drop for HubGuard {
184 fn drop(&mut self) {
185 HUBS.lock()
186 .unwrap_or_else(|e| e.into_inner())
187 .remove(&self.0);
188 }
189}
190
191pub async fn broadcast(
197 state: &crate::state::app_state::AppState,
198 server_id: crate::state::ServerId,
199 path: Option<&str>,
200 bytes: &[u8],
201) -> usize {
202 let Some(addr) = state.get_server(server_id).await.and_then(|s| s.local_addr) else {
203 return 0;
204 };
205 let hub = hub(addr);
206 let targets: Vec<(u32, Writer, Arc<Stall>)> = {
207 let subs = hub.lock().await;
208 subs.iter()
209 .filter(|(_, s)| path.is_none_or(|p| s.path == p))
210 .map(|(id, s)| (*id, s.writer.clone(), s.stall.clone()))
211 .collect()
212 };
213 let results =
214 futures::future::join_all(targets.into_iter().map(|(id, writer, stall)| async move {
215 let ok = deliver(&writer, bytes).await;
216 (id, ok, stall)
217 }))
218 .await;
219 let mut delivered = 0;
220 for (id, ok, stall) in results {
221 if ok {
222 delivered += 1;
223 } else {
224 hub.lock().await.remove(&id);
225 stall.trip();
226 tracing::warn!("SSE stream {id} dropped: a broadcast did not reach it");
227 }
228 }
229 delivered
230}
231
232fn keepalive(ctx: &SpawnContext) -> Result<Duration> {
233 let secs = ctx
234 .startup_params
235 .as_ref()
236 .map(|p| p.get_optional_u64("keepalive_secs"))
237 .transpose()?
238 .flatten()
239 .unwrap_or(actions::KEEPALIVE_SECS);
240 anyhow::ensure!(
241 (1..=MAX_KEEPALIVE_SECS).contains(&secs),
242 "keepalive_secs must be 1..={MAX_KEEPALIVE_SECS}"
243 );
244 Ok(Duration::from_secs(secs))
245}
246
247pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
248 let keepalive = keepalive(&ctx)?;
249 let listener = TcpListener::bind(ctx.legacy_listen_addr()).await?;
250 let addr = listener.local_addr()?;
251 Log::new(Some(&ctx.status_tx)).info(format!("SSE server listening on {addr}"));
252 let state = ctx.state.clone();
253 let server_id = ctx.server_id;
254 let accept = tokio::spawn(async move {
255 let _hub = HubGuard(addr);
256 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
257 loop {
258 let (socket, peer, permit) = match accept_bounded(
259 &listener,
260 &limiter,
261 b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
262 "SSE",
263 Some(&ctx.status_tx),
264 )
265 .await
266 {
267 Ok(v) => v,
268 Err(_) => break,
269 };
270 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
271 let now = crate::utils::clock::Instant::now();
272 ctx.state
273 .add_connection_to_server(
274 server_id,
275 ConnectionState {
276 id,
277 remote_addr: peer,
278 local_addr: addr,
279 bytes_sent: 0,
280 bytes_received: 0,
281 packets_sent: 0,
282 packets_received: 0,
283 last_activity: now,
284 status: ConnectionStatus::Active,
285 status_changed_at: now,
286 protocol_info: ProtocolConnectionInfo::empty(),
287 },
288 )
289 .await;
290 let _ = ctx.status_tx.send("__UPDATE_UI__".into());
291 let child = ctx.clone();
292 ctx.state
293 .spawn_server_task(server_id, async move {
294 let _permit = permit;
295 if let Err(e) = connection(&child, addr, id, socket, peer, keepalive).await {
296 Log::new(Some(&child.status_tx))
297 .debug(format!("SSE connection {id} ended: {e}"));
298 }
299 hub(addr).lock().await.remove(&id.as_u32());
300 child.state.close_connection_on_server(server_id, id).await;
301 let _ = child.status_tx.send("__UPDATE_UI__".into());
302 })
303 .await;
304 }
305 });
306 state.register_server_task(server_id, accept).await;
307 Ok(addr)
308}
309
310struct Request {
311 method: String,
312 path: String,
313 query: Map<String, Value>,
314 headers: HashMap<String, String>,
315}
316
317fn head_end(buf: &[u8]) -> Option<usize> {
318 buf.windows(4).position(|w| w == b"\r\n\r\n").map(|i| i + 4)
319}
320
321fn parse_head(head: &[u8]) -> Option<Request> {
322 let text = std::str::from_utf8(head).ok()?;
323 let mut lines = text.split("\r\n");
324 let mut first = lines.next()?.split(' ');
325 let method = first.next()?.to_string();
326 let target = first.next()?;
327 if !first.next()?.starts_with("HTTP/1.") {
328 return None;
329 }
330 let (path, query) = target.split_once('?').unwrap_or((target, ""));
331 let query = url::form_urlencoded::parse(query.as_bytes())
332 .map(|(k, v)| (k.into_owned(), Value::String(v.into_owned())))
333 .collect();
334 let headers = lines
335 .filter_map(|l| l.split_once(':'))
336 .map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string()))
337 .collect();
338 Some(Request {
339 method,
340 path: path.to_string(),
341 query,
342 headers,
343 })
344}
345
346fn status_text(status: u16) -> &'static str {
347 match status {
348 200 => "OK",
349 201 => "Created",
350 202 => "Accepted",
351 204 => "No Content",
352 301 => "Moved Permanently",
353 302 => "Found",
354 304 => "Not Modified",
355 400 => "Bad Request",
356 401 => "Unauthorized",
357 403 => "Forbidden",
358 404 => "Not Found",
359 405 => "Method Not Allowed",
360 413 => "Content Too Large",
361 429 => "Too Many Requests",
362 500 => "Internal Server Error",
363 503 => "Service Unavailable",
364 _ => "Status",
365 }
366}
367
368fn response(status: u16, content_type: &str, body: &[u8], extra: &str) -> Vec<u8> {
369 let mut out = format!(
370 "HTTP/1.1 {status} {}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n{extra}\r\n",
371 status_text(status),
372 body.len()
373 )
374 .into_bytes();
375 out.extend_from_slice(body);
376 out
377}
378
379fn outcome(ctx: &SpawnContext, id: ConnectionId, path: &str, decision: &str) {
380 let line = format!("SSE connection {id} path={path} decision={decision}");
381 if decision.starts_with("fail_closed") {
382 tracing::warn!("{line}");
383 } else {
384 tracing::info!("{line}");
385 }
386 let _ = ctx.status_tx.send(format!("[INFO] {line}"));
387}
388
389#[derive(Default)]
391struct Answer {
392 accept: bool,
393 retry_ms: Option<u64>,
394 respond: Option<Value>,
395 stream: Vec<u8>,
396 close: bool,
397}
398
399fn collect(results: Vec<ActionResult>, answer: &mut Answer) {
400 for r in results {
401 match r {
402 ActionResult::Output(bytes) => answer.stream.extend_from_slice(&bytes),
403 ActionResult::CloseConnection => answer.close = true,
404 ActionResult::Multiple(items) => collect(items, answer),
405 ActionResult::Custom { name, data } if name == actions::ACCEPT => {
406 answer.accept = true;
407 answer.retry_ms = data["retry_ms"].as_u64();
408 }
409 ActionResult::Custom { name, data } if name == actions::RESPOND => {
410 answer.respond.get_or_insert(data);
411 }
412 _ => {}
413 }
414 }
415}
416
417async fn connection(
418 ctx: &SpawnContext,
419 addr: SocketAddr,
420 id: ConnectionId,
421 socket: TcpStream,
422 peer: SocketAddr,
423 keepalive: Duration,
424) -> Result<()> {
425 let (mut reader, writer) = tokio::io::split(socket);
426 let stall = Arc::new(Stall::default());
427 let writer: Writer = Arc::new(Mutex::new(StreamWriter {
428 inner: writer,
429 stall: stall.clone(),
430 deadline: None,
431 }));
432 let mut buf = Vec::new();
433 let mut chunk = [0u8; 4096];
434 let deadline = tokio::time::Instant::now() + HEAD_TIMEOUT;
435 let head_len = loop {
436 if let Some(n) = head_end(&buf) {
437 break n;
438 }
439 if buf.len() > MAX_REQUEST_HEAD_BYTES {
440 outcome(ctx, id, "", "fail_closed_head_too_large");
441 let mut w = writer.lock().await;
442 w.write_all(&response(431, "text/plain", b"", "")).await?;
443 return Ok(());
444 }
445 let want = (MAX_REQUEST_HEAD_BYTES + 1 - buf.len()).min(chunk.len());
446 let n = match tokio::time::timeout_at(deadline, reader.read(&mut chunk[..want])).await {
447 Ok(r) => r?,
448 Err(_) => anyhow::bail!(
449 "request head not complete within {}s",
450 HEAD_TIMEOUT.as_secs()
451 ),
452 };
453 if n == 0 {
454 return Ok(());
455 }
456 buf.extend_from_slice(&chunk[..n]);
457 };
458 ctx.state
459 .update_connection_stats(
460 ctx.server_id,
461 id,
462 Some(buf.len() as u64),
463 None,
464 Some(1),
465 None,
466 )
467 .await;
468 let Some(req) = parse_head(&buf[..head_len]) else {
469 outcome(ctx, id, "", "fail_closed_bad_request");
470 writer
471 .lock()
472 .await
473 .write_all(&response(400, "text/plain", b"", ""))
474 .await?;
475 return Ok(());
476 };
477 if req.method != "GET" && req.method != "HEAD" {
478 outcome(ctx, id, &req.path, "fail_closed_method");
479 writer
480 .lock()
481 .await
482 .write_all(&response(405, "text/plain", b"", "Allow: GET, HEAD\r\n"))
483 .await?;
484 return Ok(());
485 }
486 let declares_body = req
487 .headers
488 .get("content-length")
489 .is_some_and(|l| l.trim() != "0")
490 || req.headers.contains_key("transfer-encoding");
491 if declares_body {
492 outcome(ctx, id, &req.path, "fail_closed_body_not_accepted");
493 writer
494 .lock()
495 .await
496 .write_all(&response(413, "text/plain", b"", ""))
497 .await?;
498 return Ok(());
499 }
500 let h = |k: &str| req.headers.get(k).cloned().unwrap_or_default();
501 let event = Event::new(
502 &actions::REQUEST_EVENT,
503 json!({
504 "method": req.method, "path": req.path, "query": req.query,
505 "last_event_id": h("last-event-id"), "accept": h("accept"),
506 "user_agent": h("user-agent"), "client_ip": peer.ip().to_string(),
507 }),
508 );
509 let protocol = Arc::new(actions::SseProtocol);
510 let mut answer = Answer::default();
511 match call_llm(
512 &ctx.llm_client,
513 &ctx.state,
514 ctx.server_id,
515 Some(id),
516 &event,
517 protocol.as_ref(),
518 )
519 .await
520 {
521 Ok(r) => collect(r.protocol_results, &mut answer),
522 Err(e) => {
523 outcome(ctx, id, &req.path, "fail_closed_llm_error");
524 let text = crate::utils::wire_failure::wire_failure_text(&e);
525 writer
526 .lock()
527 .await
528 .write_all(&response(
529 503,
530 "text/plain; charset=utf-8",
531 text.as_bytes(),
532 "Retry-After: 5\r\n",
533 ))
534 .await?;
535 return Ok(());
536 }
537 }
538 let mut w = writer.lock().await;
539 if let Some(r) = answer.respond {
540 outcome(ctx, id, &req.path, "model_respond");
541 let status = r["status"].as_u64().unwrap_or(500) as u16;
542 let body = r["body"].as_str().unwrap_or_default();
543 let ct = r["content_type"]
544 .as_str()
545 .unwrap_or("text/plain; charset=utf-8");
546 let mut bytes = response(status, ct, body.as_bytes(), "");
547 if req.method == "HEAD" {
548 bytes.truncate(bytes.len() - body.len());
549 }
550 w.write_all(&bytes).await?;
551 return Ok(());
552 }
553 if !answer.accept && answer.stream.is_empty() {
554 outcome(ctx, id, &req.path, "model_silent");
556 w.write_all(&response(204, "text/plain", b"", "")).await?;
557 return Ok(());
558 }
559 outcome(ctx, id, &req.path, "model_stream");
560 let mut first = b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: keep-alive\r\nX-Accel-Buffering: no\r\n\r\n".to_vec();
561 if req.method == "HEAD" {
562 w.write_all(&first).await?;
563 return Ok(());
564 }
565 if let Some(retry) = answer.retry_ms {
566 first.extend_from_slice(&wire::encode_retry(retry));
567 }
568 first.extend_from_slice(&answer.stream);
569 if !answer.close {
570 hub(addr).lock().await.insert(
573 id.as_u32(),
574 Subscriber {
575 path: req.path.clone(),
576 writer: writer.clone(),
577 stall: stall.clone(),
578 },
579 );
580 let commands = crate::server::peer_support::register_peer_channel(
581 &ctx.state,
582 ctx.server_id,
583 id.as_u32(),
584 )
585 .await;
586 crate::server::peer_support::spawn_peer_command_task(
587 commands,
588 protocol,
589 ctx.state.clone(),
590 ctx.server_id,
591 id.as_u32(),
592 writer.clone(),
593 ctx.status_tx.clone(),
594 );
595 }
596 w.write_all(&first).await?;
597 w.flush().await?;
598 ctx.state
599 .update_connection_stats(
600 ctx.server_id,
601 id,
602 None,
603 Some(first.len() as u64),
604 None,
605 Some(1),
606 )
607 .await;
608 if answer.close {
609 let _ = w.shutdown().await;
610 return Ok(());
611 }
612 drop(w);
613 let _ = ctx.status_tx.send("__UPDATE_UI__".into());
614 let mut tick = tokio::time::interval(keepalive);
615 tick.tick().await;
616 loop {
617 tokio::select! {
618 read = reader.read(&mut chunk) => {
619 match read {
621 Ok(0) | Err(_) => return Ok(()),
622 Ok(_) => {}
623 }
624 }
625 _ = stall.notify.notified() => {
626 outcome(ctx, id, &req.path, "fail_closed_subscriber_stalled");
627 hub(addr).lock().await.remove(&id.as_u32());
628 shut(&writer).await;
629 return Ok(());
630 }
631 _ = tick.tick() => {
632 if !deliver(&writer, b": keepalive\n\n").await {
633 stall.trip();
635 }
636 }
637 }
638 }
639}