netget/server/msgpack_rpc/
mod.rs1pub mod actions;
4pub mod wire;
5
6use crate::llm::action_helper::call_llm;
7use crate::llm::actions::protocol_trait::ActionResult;
8use crate::logging::emit::Log;
9use crate::protocol::{Event, SpawnContext};
10use crate::server::connection::ConnectionId;
11use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
12use anyhow::{Context, Result};
13use serde_json::{json, Value};
14use std::{net::SocketAddr, time::Duration};
15use tokio::{
16 io::AsyncWriteExt,
17 net::{TcpListener, TcpStream},
18};
19use wire::Rpc;
20
21pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
22pub const MAX_IDLE_TIMEOUT_SECS: u64 = 86400;
23
24fn idle(ctx: &SpawnContext) -> Result<Duration> {
25 let secs = ctx
26 .startup_params
27 .as_ref()
28 .map(|p| p.get_optional_u64("idle_timeout_secs"))
29 .transpose()?
30 .flatten()
31 .unwrap_or(IDLE_TIMEOUT.as_secs());
32 anyhow::ensure!(
33 (1..=MAX_IDLE_TIMEOUT_SECS).contains(&secs),
34 "idle_timeout_secs must be between 1 and {MAX_IDLE_TIMEOUT_SECS}"
35 );
36 Ok(Duration::from_secs(secs))
37}
38
39pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
40 let idle = idle(&ctx)?;
41 let listener = TcpListener::bind(ctx.legacy_listen_addr()).await?;
42 let addr = listener.local_addr()?;
43 Log::new(Some(&ctx.status_tx)).info(format!("MessagePack-RPC listening on {addr}"));
44 let state = ctx.state.clone();
45 let server_id = ctx.server_id;
46 let accept = tokio::spawn(async move {
47 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(
48 crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS,
49 );
50 loop {
51 let (socket, peer, permit) = match crate::server::accept_bounded::accept_bounded(
52 &listener,
53 &limiter,
54 b"",
55 "MessagePack-RPC",
56 Some(&ctx.status_tx),
57 )
58 .await
59 {
60 Ok(v) => v,
61 Err(_) => break,
62 };
63 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
64 let now = crate::utils::clock::Instant::now();
65 ctx.state
66 .add_connection_to_server(
67 server_id,
68 ConnectionState {
69 id,
70 remote_addr: peer,
71 local_addr: addr,
72 bytes_sent: 0,
73 bytes_received: 0,
74 packets_sent: 0,
75 packets_received: 0,
76 last_activity: now,
77 status: ConnectionStatus::Active,
78 status_changed_at: now,
79 protocol_info: ProtocolConnectionInfo::empty(),
80 },
81 )
82 .await;
83 let child = ctx.clone();
84 ctx.state
85 .spawn_server_task(server_id, async move {
86 let _permit = permit;
87 if let Err(e) = session(&child, id, socket, peer, idle).await {
88 Log::new(Some(&child.status_tx))
89 .warn(format!("MessagePack-RPC connection {id} ended: {e}"));
90 }
91 child
92 .state
93 .update_connection_status(server_id, id, ConnectionStatus::Closed)
94 .await;
95 let _ = child.status_tx.send("__UPDATE_UI__".into());
96 })
97 .await;
98 }
99 });
100 state.register_server_task(server_id, accept).await;
101 Ok(addr)
102}
103
104fn outcome(ctx: &SpawnContext, id: ConnectionId, operation: &str, decision: &str) {
105 let summary =
106 format!("MessagePack-RPC connection {id} operation={operation} decision={decision}");
107 let log = Log::new(Some(&ctx.status_tx));
108 if decision.starts_with("fail_closed_") || decision == "model_silent" {
109 log.error(summary);
110 } else {
111 log.info(summary);
112 }
113}
114
115async fn answer(
117 ctx: &SpawnContext,
118 id: ConnectionId,
119 rpc: Rpc,
120 peer: SocketAddr,
121) -> Result<Vec<u8>> {
122 let (event, msgid, op) = match &rpc {
123 Rpc::Request {
124 msgid,
125 method,
126 params,
127 } => (
128 Event::new(
129 &actions::REQUEST_EVENT,
130 json!({"method": method, "params": params, "msgid": msgid, "remote_addr": peer.to_string()}),
131 ),
132 Some(*msgid),
133 "msgpack_request",
134 ),
135 Rpc::Notification { method, params } => (
136 Event::new(
137 &actions::NOTIFICATION_EVENT,
138 json!({"method": method, "params": params, "remote_addr": peer.to_string()}),
139 ),
140 None,
141 "msgpack_notification",
142 ),
143 Rpc::Response { .. } => anyhow::bail!("unsolicited response from the client"),
146 };
147 let failed = |error: Option<&anyhow::Error>| -> Result<Vec<u8>> {
148 match msgid {
149 Some(m) => {
150 let text = match error {
151 Some(e) => crate::utils::wire_failure::prefixed_wire_failure_text(e),
152 None => crate::utils::WireFailure::Unavailable.prefixed_text(),
153 };
154 wire::response(m, &json!(text), &Value::Null)
155 }
156 None => Ok(Vec::new()),
157 }
158 };
159 let result = match call_llm(
160 &ctx.llm_client,
161 &ctx.state,
162 ctx.server_id,
163 Some(id),
164 &event,
165 &actions::MsgpackRpcProtocol,
166 )
167 .await
168 {
169 Ok(r) => r,
170 Err(e) => {
171 outcome(ctx, id, op, "fail_closed_llm_error");
172 return failed(Some(&e));
173 }
174 };
175 if !result.failures.is_empty() {
176 outcome(ctx, id, op, "fail_closed_invalid_reply");
177 return failed(None);
178 }
179 let mut answers = Vec::new();
180 let mut notes = Vec::new();
181 let mut pending = result.protocol_results;
182 while let Some(r) = pending.pop() {
183 match r {
184 ActionResult::Custom { name, data } if name == "msgpack_notify" => notes.push(data),
185 ActionResult::Custom { name, data } if name.starts_with("msgpack_") => {
186 answers.push((name, data))
187 }
188 ActionResult::Multiple(items) => pending.extend(items),
189 _ => {}
190 }
191 }
192 notes.reverse();
193 let mut out = Vec::new();
194 match (msgid, answers.as_slice()) {
195 (Some(m), [(name, data)]) if name == "msgpack_result" => {
196 outcome(ctx, id, op, "model_answer");
197 out.extend(wire::response(m, &Value::Null, &data["result"])?);
198 }
199 (Some(m), [(name, data)]) if name == "msgpack_error" => {
200 outcome(ctx, id, op, "model_reject");
201 out.extend(wire::response(m, &data["error"], &Value::Null)?);
202 }
203 (Some(_), []) => {
204 outcome(ctx, id, op, "model_silent");
205 return failed(None);
206 }
207 (Some(_), _) => {
208 outcome(ctx, id, op, "fail_closed_invalid_reply");
209 return failed(None);
210 }
211 (None, []) => outcome(
212 ctx,
213 id,
214 op,
215 if notes.is_empty() {
216 "model_silent"
217 } else {
218 "model_answer"
219 },
220 ),
221 (None, [(name, _)]) if name == "msgpack_ignore" => outcome(ctx, id, op, "model_answer"),
222 (None, _) => {
223 outcome(ctx, id, op, "fail_closed_invalid_reply");
224 return Ok(Vec::new());
225 }
226 }
227 for n in notes {
228 out.extend(wire::notification(
229 n["method"].as_str().unwrap_or_default(),
230 &n["params"],
231 )?);
232 }
233 Ok(out)
234}
235
236async fn session(
237 ctx: &SpawnContext,
238 id: ConnectionId,
239 socket: TcpStream,
240 peer: SocketAddr,
241 idle: Duration,
242) -> Result<()> {
243 let (reader, mut writer) = tokio::io::split(socket);
244 let mut stream = wire::Stream::new(reader);
245 while let Some(value) = stream.next(idle).await? {
246 ctx.state
247 .update_connection_stats(ctx.server_id, id, None, None, Some(1), None)
248 .await;
249 let rpc = wire::parse(value)?;
250 let out = answer(ctx, id, rpc, peer).await?;
251 if !out.is_empty() {
252 tokio::time::timeout(wire::IO_TIMEOUT, writer.write_all(&out))
253 .await
254 .context("MessagePack-RPC write deadline")??;
255 ctx.state
256 .update_connection_stats(
257 ctx.server_id,
258 id,
259 None,
260 Some(out.len() as u64),
261 None,
262 Some(1),
263 )
264 .await;
265 }
266 }
267 Ok(())
268}