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