1pub mod actions;
7
8pub use actions::EtcdProtocol;
10
11use anyhow::Result;
12use std::net::SocketAddr;
13use std::sync::Arc;
14use tokio::sync::{mpsc, Mutex};
15use tracing::{debug, error, trace};
16
17#[cfg(not(target_arch = "wasm32"))]
18use crate::llm::action_helper::call_llm;
19#[cfg(not(target_arch = "wasm32"))]
20use crate::llm::ollama_client::OllamaClient;
21#[cfg(not(target_arch = "wasm32"))]
22use crate::protocol::Event;
23#[cfg(not(target_arch = "wasm32"))]
24use crate::server::etcd::actions::ETCD_RANGE_REQUEST_EVENT;
25#[cfg(not(target_arch = "wasm32"))]
26use crate::state::app_state::AppState;
27#[cfg(not(target_arch = "wasm32"))]
28use bytes::Bytes;
29#[cfg(not(target_arch = "wasm32"))]
30use http_body_util::{BodyExt, Limited};
31#[cfg(not(target_arch = "wasm32"))]
32use hyper::{body::Incoming, header::HeaderValue, Request, Response, StatusCode};
33#[cfg(not(target_arch = "wasm32"))]
34use prost::Message;
35
36#[cfg(not(target_arch = "wasm32"))]
38mod etcdserverpb {
39 include!(concat!(env!("OUT_DIR"), "/etcdserverpb.rs"));
40}
41#[cfg(not(target_arch = "wasm32"))]
42mod mvccpb {
43 include!(concat!(env!("OUT_DIR"), "/mvccpb.rs"));
44}
45
46#[cfg(not(target_arch = "wasm32"))]
47use crate::logging::emit::Log;
48#[cfg(not(target_arch = "wasm32"))]
49use etcdserverpb::{
50 CompactionRequest, CompactionResponse, DeleteRangeRequest, DeleteRangeResponse, PutRequest,
51 PutResponse, RangeRequest, RangeResponse, ResponseHeader, TxnRequest, TxnResponse,
52};
53#[cfg(not(target_arch = "wasm32"))]
54use mvccpb::KeyValue;
55
56#[cfg(not(target_arch = "wasm32"))]
64struct EtcdMeta {
65 revision: i64,
67 cluster_id: u64,
69 member_id: u64,
71}
72
73#[cfg(not(target_arch = "wasm32"))]
74impl EtcdMeta {
75 fn new(cluster_id: u64, member_id: u64) -> Self {
76 Self {
77 revision: 0,
78 cluster_id,
79 member_id,
80 }
81 }
82
83 fn get_response_header(&self) -> ResponseHeader {
84 ResponseHeader {
85 cluster_id: self.cluster_id,
86 member_id: self.member_id,
87 revision: self.revision,
88 raft_term: 1, }
90 }
91
92 fn increment_revision(&mut self) {
93 self.revision += 1;
94 }
95}
96
97#[cfg(not(target_arch = "wasm32"))]
99const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
106
107const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
118
119const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
124
125const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
133 Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
134
135pub const MAX_REQUEST_BYTES: usize = 1_572_864;
136
137#[cfg(not(target_arch = "wasm32"))]
139const GRPC_UNKNOWN: i32 = 2;
140#[cfg(not(target_arch = "wasm32"))]
141const GRPC_NOT_FOUND: i32 = 5;
142#[cfg(not(target_arch = "wasm32"))]
143const GRPC_INVALID_ARGUMENT: i32 = 3;
144#[cfg(not(target_arch = "wasm32"))]
145const GRPC_RESOURCE_EXHAUSTED: i32 = 8;
146#[cfg(not(target_arch = "wasm32"))]
147const GRPC_UNIMPLEMENTED: i32 = 12;
148#[cfg(not(target_arch = "wasm32"))]
149const GRPC_INTERNAL: i32 = 13;
150#[cfg(not(target_arch = "wasm32"))]
151const GRPC_UNAVAILABLE: i32 = 14;
152
153#[cfg(not(target_arch = "wasm32"))]
171pub fn grpc_status_for_llm_failure(err: &anyhow::Error) -> i32 {
172 if crate::llm::is_overload_error(err) {
173 GRPC_UNAVAILABLE
174 } else {
175 GRPC_INTERNAL
176 }
177}
178
179#[cfg(not(target_arch = "wasm32"))]
182fn llm_failure(
183 status_tx: &mpsc::UnboundedSender<String>,
184 method: &str,
185 e: anyhow::Error,
186) -> GrpcFailure {
187 let status = grpc_status_for_llm_failure(&e);
188 let name = if status == GRPC_UNAVAILABLE {
189 "UNAVAILABLE"
190 } else {
191 "INTERNAL"
192 };
193 Log::new(Some(status_tx)).warn(format!(
194 "etcd {} could not be answered: the handler failed ({}). Replying grpc-status {} \
195 ({}); no key-value data is being invented.",
196 method, e, status, name
197 ));
198 GrpcFailure::new(
199 status,
200 crate::utils::WireFailure::classify(&e).prefixed_text(),
201 )
202}
203
204#[cfg(not(target_arch = "wasm32"))]
206struct GrpcFailure {
207 status: i32,
208 message: String,
209}
210
211#[cfg(not(target_arch = "wasm32"))]
212impl GrpcFailure {
213 fn new(status: i32, message: impl Into<String>) -> Self {
214 Self {
215 status,
216 message: message.into(),
217 }
218 }
219
220 fn from_action_code(code: &str, message: String) -> Self {
224 let status = match code.to_ascii_uppercase().as_str() {
225 "KEY_NOT_FOUND" | "NOT_FOUND" => GRPC_NOT_FOUND,
226 "INVALID_ARGUMENT" | "BAD_REQUEST" => GRPC_INVALID_ARGUMENT,
227 "UNIMPLEMENTED" | "NOT_IMPLEMENTED" => GRPC_UNIMPLEMENTED,
228 "RESOURCE_EXHAUSTED" => GRPC_RESOURCE_EXHAUSTED,
229 "INTERNAL" => GRPC_INTERNAL,
230 _ => GRPC_UNKNOWN,
231 };
232 Self::new(status, message)
233 }
234}
235
236#[cfg(not(target_arch = "wasm32"))]
237impl From<anyhow::Error> for GrpcFailure {
238 fn from(e: anyhow::Error) -> Self {
239 let status = grpc_status_for_llm_failure(&e);
246 Self::new(status, crate::utils::WireFailure::classify(&e).text())
247 }
248}
249
250#[cfg(not(target_arch = "wasm32"))]
251impl From<prost::DecodeError> for GrpcFailure {
252 fn from(e: prost::DecodeError) -> Self {
253 debug!("etcd request message did not decode: {e}");
256 Self::new(GRPC_INVALID_ARGUMENT, "could not decode request message")
257 }
258}
259
260#[cfg(not(target_arch = "wasm32"))]
261impl From<prost::EncodeError> for GrpcFailure {
262 fn from(e: prost::EncodeError) -> Self {
263 debug!("etcd response encoding failed: {e}");
264 Self::new(GRPC_INTERNAL, "could not encode response")
265 }
266}
267
268#[cfg(not(target_arch = "wasm32"))]
274fn take_etcd_error(
275 results: &[crate::llm::actions::protocol_trait::ActionResult],
276) -> Option<GrpcFailure> {
277 for result in results {
278 if let crate::llm::actions::protocol_trait::ActionResult::Custom { name, data } = result {
279 if name == "etcd_error" {
280 let code = data
281 .get("code")
282 .and_then(|v| v.as_str())
283 .unwrap_or("UNKNOWN");
284 let message = data
285 .get("message")
286 .and_then(|v| v.as_str())
287 .unwrap_or("etcdserver: request failed")
288 .to_string();
289 return Some(GrpcFailure::from_action_code(code, message));
290 }
291 }
292 }
293 None
294}
295
296#[cfg(not(target_arch = "wasm32"))]
309fn header_safe(message: &str) -> String {
310 let mut out = String::with_capacity(message.len().min(512));
311 let mut last_was_space = false;
312 for c in message.chars() {
313 let c = if (' '..='~').contains(&c) { c } else { ' ' };
314 if c == ' ' {
315 if last_was_space {
316 continue;
317 }
318 last_was_space = true;
319 } else {
320 last_was_space = false;
321 }
322 if out.chars().count() >= 512 {
323 break;
324 }
325 out.push(c);
326 }
327 let trimmed = out.trim();
328 if trimmed.is_empty() {
329 "internal error".to_string()
330 } else {
331 trimmed.to_string()
332 }
333}
334
335type GrpcBody = http_body_util::combinators::BoxBody<Bytes, std::convert::Infallible>;
360
361#[cfg(not(target_arch = "wasm32"))]
366fn grpc_body_with_trailers(body: Bytes, status: i32, message: &str) -> GrpcBody {
367 use http_body_util::StreamBody;
368
369 let mut trailers = hyper::HeaderMap::new();
370 trailers.insert(
371 "grpc-status",
372 HeaderValue::from_str(&status.to_string())
373 .unwrap_or_else(|_| HeaderValue::from_static("13")),
374 );
375 trailers.insert(
376 "grpc-message",
377 HeaderValue::from_str(message).unwrap_or_else(|_| HeaderValue::from_static("")),
378 );
379
380 let frames: Vec<std::result::Result<hyper::body::Frame<Bytes>, std::convert::Infallible>> = vec![
381 Ok(hyper::body::Frame::data(body)),
382 Ok(hyper::body::Frame::trailers(trailers)),
383 ];
384
385 BodyExt::boxed(StreamBody::new(futures::stream::iter(frames)))
386}
387
388#[cfg(not(target_arch = "wasm32"))]
390fn empty_grpc_body() -> GrpcBody {
391 BodyExt::boxed(http_body_util::Empty::<Bytes>::new())
392}
393
394#[cfg(not(target_arch = "wasm32"))]
395fn grpc_status_reply(status: i32, message: &str) -> Response<GrpcBody> {
396 let message = &header_safe(message);
397 let mut res = Response::new(empty_grpc_body());
402 *res.status_mut() = StatusCode::OK;
403 let headers = res.headers_mut();
404 headers.insert(
405 "content-type",
406 HeaderValue::from_static("application/grpc+proto"),
407 );
408 headers.insert(
409 "grpc-status",
410 HeaderValue::from_str(&status.to_string())
411 .unwrap_or_else(|_| HeaderValue::from_static("13")),
412 );
413 headers.insert(
414 "grpc-message",
415 HeaderValue::from_str(message)
416 .unwrap_or_else(|_| HeaderValue::from_static("internal error")),
417 );
418 res
419}
420
421pub struct EtcdServer;
423
424#[cfg(not(target_arch = "wasm32"))]
425impl EtcdServer {
426 pub async fn spawn_with_llm_actions(
428 listen_addr: SocketAddr,
429 llm_client: OllamaClient,
430 app_state: Arc<AppState>,
431 status_tx: mpsc::UnboundedSender<String>,
432 server_id: crate::state::ServerId,
433 startup_params: Option<crate::protocol::StartupParams>,
434 ) -> Result<SocketAddr> {
435 let cluster_name = startup_params
437 .as_ref()
438 .map(|p| p.get_optional_string("cluster_name"))
439 .transpose()?
440 .flatten()
441 .unwrap_or_else(|| "netget-cluster".to_string());
442
443 let cluster_id = 0x6574636400000001u64; let member_id = 0x6d656d6265720001u64; Log::new(Some(&status_tx)).info(format!(
447 "etcd server starting on {} (cluster: {})",
448 listen_addr, cluster_name
449 ));
450
451 let meta = Arc::new(Mutex::new(EtcdMeta::new(cluster_id, member_id)));
453 let protocol = Arc::new(EtcdProtocol::new());
454
455 let listener = tokio::net::TcpListener::bind(listen_addr).await?;
457 let local_addr = listener.local_addr()?;
458
459 Log::new(Some(&status_tx)).info(format!("etcd server listening on {}", local_addr));
460
461 let task_registrar = app_state.clone();
463 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
464 let accept_handle = tokio::spawn(async move {
465 loop {
466 match crate::server::accept_bounded::accept_bounded(
467 &listener,
468 &limiter,
469 CONNECTION_CAP_REFUSAL,
470 "etcd",
471 Some(&status_tx),
472 )
473 .await
474 {
475 Ok((stream, peer_addr, permit)) => {
476 Log::new(Some(&status_tx))
477 .debug(format!("etcd connection from {}", peer_addr));
478
479 let connection_id = crate::server::connection::ConnectionId::new(
485 app_state.get_next_unified_id().await,
486 );
487 {
488 use crate::state::server::{
489 ConnectionState as ServerConnectionState, ConnectionStatus,
490 ProtocolConnectionInfo,
491 };
492 let now = crate::utils::clock::Instant::now();
493 app_state
494 .add_connection_to_server(
495 server_id,
496 ServerConnectionState {
497 id: connection_id,
498 remote_addr: peer_addr,
499 local_addr,
500 bytes_sent: 0,
501 bytes_received: 0,
502 packets_sent: 0,
503 packets_received: 0,
504 last_activity: now,
505 status: ConnectionStatus::Active,
506 status_changed_at: now,
507 protocol_info: ProtocolConnectionInfo::empty(),
508 },
509 )
510 .await;
511 }
512 let _ = status_tx.send("__UPDATE_UI__".to_string());
513
514 let llm_clone = llm_client.clone();
515 let state_clone = app_state.clone();
516 let status_clone = status_tx.clone();
517 let meta_clone = meta.clone();
518 let protocol_clone = protocol.clone();
519 let conn_owner = app_state.clone();
520
521 let task_owner = app_state.clone();
523 task_owner
524 .spawn_server_task(server_id, async move {
525 let _permit = permit;
528 if let Err(e) = Self::handle_connection(
529 stream,
530 peer_addr,
531 local_addr,
532 connection_id,
533 llm_clone,
534 state_clone,
535 status_clone,
536 server_id,
537 meta_clone,
538 protocol_clone,
539 )
540 .await
541 {
542 error!("etcd connection error: {}", e);
543 }
544 conn_owner
549 .close_connection_on_server(server_id, connection_id)
550 .await;
551 })
552 .await;
553 }
554 Err(e) => {
555 Log::new(Some(&status_tx))
559 .error(format!("etcd accept failed, listener stopped: {}", e));
560 break;
561 }
562 }
563 }
564 });
565
566 task_registrar
567 .register_server_task(server_id, accept_handle)
568 .await;
569
570 Ok(local_addr)
571 }
572
573 #[allow(clippy::too_many_arguments)]
574 async fn handle_connection(
575 stream: tokio::net::TcpStream,
576 peer_addr: SocketAddr,
577 local_addr: SocketAddr,
578 connection_id: crate::server::connection::ConnectionId,
579 llm_client: OllamaClient,
580 app_state: Arc<AppState>,
581 status_tx: mpsc::UnboundedSender<String>,
582 server_id: crate::state::ServerId,
583 meta: Arc<Mutex<EtcdMeta>>,
584 protocol: Arc<EtcdProtocol>,
585 ) -> Result<()> {
586 use hyper::service::service_fn;
587 use hyper_util::rt::TokioIo;
588
589 match tokio::time::timeout(FIRST_BYTE_READ_TIMEOUT, stream.peek(&mut [0u8; 1])).await {
599 Ok(Ok(0)) | Ok(Err(_)) => return Ok(()),
600 Ok(Ok(_)) => {}
601 Err(_) => {
602 Log::new(Some(&status_tx)).debug(format!(
603 "etcd peer {} sent nothing for {}s; closing before the HTTP/2 preface",
604 peer_addr,
605 FIRST_BYTE_READ_TIMEOUT.as_secs()
606 ));
607 return Ok(());
608 }
609 }
610
611 let io = TokioIo::new(stream);
612
613 let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
618 let activity_for_service = Arc::clone(&activity);
619
620 let service = service_fn(move |req: Request<Incoming>| {
621 let llm = llm_client.clone();
622 let state = app_state.clone();
623 let status = status_tx.clone();
624 let meta_ref = meta.clone();
625 let proto = protocol.clone();
626 let activity = Arc::clone(&activity_for_service);
627
628 async move {
629 let _busy = activity.busy();
630 let response = Self::handle_grpc_request(
631 req,
632 peer_addr,
633 local_addr,
634 llm,
635 state.clone(),
636 status,
637 server_id,
638 meta_ref,
639 proto,
640 )
641 .await;
642
643 if let Ok(res) = &response {
648 use hyper::body::Body as _;
649 state
650 .update_connection_stats(
651 server_id,
652 connection_id,
653 None,
654 Some(res.body().size_hint().lower()),
655 None,
656 Some(1),
657 )
658 .await;
659 }
660 response
661 }
662 });
663
664 let conn = hyper::server::conn::http2::Builder::new(hyper_util::rt::TokioExecutor::new())
665 .serve_connection(io, service);
666 tokio::pin!(conn);
667
668 tokio::select! {
669 result = &mut conn => result?,
670 _ = crate::server::accept_bounded::watch_idle(
671 Arc::clone(&activity),
672 IDLE_BETWEEN_REQUESTS_TIMEOUT,
673 ) => {
674 debug!(
675 "etcd connection {} idle for {}s; closing",
676 connection_id,
677 IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
678 );
679 }
680 }
681
682 Ok(())
683 }
684
685 async fn handle_grpc_request(
686 req: Request<Incoming>,
687 _peer_addr: SocketAddr,
688 _local_addr: SocketAddr,
689 llm_client: OllamaClient,
690 app_state: Arc<AppState>,
691 status_tx: mpsc::UnboundedSender<String>,
692 server_id: crate::state::ServerId,
693 meta: Arc<Mutex<EtcdMeta>>,
694 protocol: Arc<EtcdProtocol>,
695 ) -> Result<Response<GrpcBody>> {
696 Ok(
702 match Self::route_grpc_request(
703 req, llm_client, app_state, status_tx, server_id, meta, protocol,
704 )
705 .await
706 {
707 Ok(response) => response,
708 Err(GrpcFailure { status, message }) => grpc_status_reply(status, &message),
709 },
710 )
711 }
712
713 async fn route_grpc_request(
714 req: Request<Incoming>,
715 llm_client: OllamaClient,
716 app_state: Arc<AppState>,
717 status_tx: mpsc::UnboundedSender<String>,
718 server_id: crate::state::ServerId,
719 meta: Arc<Mutex<EtcdMeta>>,
720 protocol: Arc<EtcdProtocol>,
721 ) -> std::result::Result<Response<GrpcBody>, GrpcFailure> {
722 let path = req.uri().path().to_string();
724 let method = req.method().as_str().to_string();
725
726 Log::new(Some(&status_tx)).debug(format!("etcd gRPC {} {}", method, path));
727
728 let body = Limited::new(req.into_body(), MAX_REQUEST_BYTES);
732 let whole_body = body.collect().await.map_err(|e| {
733 GrpcFailure::new(
734 GRPC_RESOURCE_EXHAUSTED,
735 format!(
736 "request body rejected (limit {} bytes): {}",
737 MAX_REQUEST_BYTES, e
738 ),
739 )
740 })?;
741 let whole_body = whole_body.to_bytes();
742
743 if whole_body.len() < 5 {
745 return Err(GrpcFailure::new(
746 GRPC_INTERNAL,
747 format!("gRPC frame too short: {} bytes", whole_body.len()),
748 ));
749 }
750
751 if whole_body[0] != 0 {
752 return Err(GrpcFailure::new(
755 GRPC_UNIMPLEMENTED,
756 "compressed gRPC messages are not supported".to_string(),
757 ));
758 }
759
760 let declared =
764 u32::from_be_bytes([whole_body[1], whole_body[2], whole_body[3], whole_body[4]])
765 as usize;
766 let available = whole_body.len() - 5;
767 if declared > available {
768 return Err(GrpcFailure::new(
769 GRPC_INTERNAL,
770 format!(
771 "gRPC frame declares {} bytes but only {} follow the header",
772 declared, available
773 ),
774 ));
775 }
776 let msg_bytes = &whole_body[5..5 + declared];
777
778 let response_bytes = match path.as_str() {
780 "/etcdserverpb.KV/Range" => {
781 Self::handle_range(
782 msg_bytes, llm_client, app_state, status_tx, server_id, meta, protocol,
783 )
784 .await?
785 }
786 "/etcdserverpb.KV/Put" => {
787 Self::handle_put(
788 msg_bytes, llm_client, app_state, status_tx, server_id, meta, protocol,
789 )
790 .await?
791 }
792 "/etcdserverpb.KV/DeleteRange" => {
793 Self::handle_delete_range(
794 msg_bytes, llm_client, app_state, status_tx, server_id, meta, protocol,
795 )
796 .await?
797 }
798 "/etcdserverpb.KV/Txn" => {
799 Self::handle_txn(
800 msg_bytes, llm_client, app_state, status_tx, server_id, meta, protocol,
801 )
802 .await?
803 }
804 "/etcdserverpb.KV/Compact" => {
805 Self::handle_compact(
806 msg_bytes, llm_client, app_state, status_tx, server_id, meta, protocol,
807 )
808 .await?
809 }
810 other => {
814 return Err(GrpcFailure::new(
815 GRPC_UNIMPLEMENTED,
816 format!(
817 "etcd server implements only etcdserverpb.KV; no method {}",
818 other
819 ),
820 ));
821 }
822 };
823
824 let mut response_with_frame = Vec::with_capacity(5 + response_bytes.len());
826 response_with_frame.push(0); response_with_frame.extend_from_slice(&(response_bytes.len() as u32).to_be_bytes());
828 response_with_frame.extend_from_slice(&response_bytes);
829
830 let mut res = Response::new(grpc_body_with_trailers(
833 Bytes::from(response_with_frame),
834 0,
835 "",
836 ));
837 *res.status_mut() = StatusCode::OK;
838 res.headers_mut().insert(
839 "content-type",
840 HeaderValue::from_static("application/grpc+proto"),
841 );
842
843 Ok(res)
844 }
845
846 async fn handle_range(
847 msg_bytes: &[u8],
848 llm_client: OllamaClient,
849 app_state: Arc<AppState>,
850 status_tx: mpsc::UnboundedSender<String>,
851 server_id: crate::state::ServerId,
852 meta: Arc<Mutex<EtcdMeta>>,
853 protocol: Arc<EtcdProtocol>,
854 ) -> std::result::Result<Vec<u8>, GrpcFailure> {
855 let request = RangeRequest::decode(msg_bytes)?;
856
857 let key_str = String::from_utf8_lossy(&request.key);
858 Log::new(Some(&status_tx)).debug(format!("etcd Range request: key={}", key_str));
859
860 trace!("etcd Range request: {:?}", request);
861
862 let event = Event::new(
864 &ETCD_RANGE_REQUEST_EVENT,
865 serde_json::json!({
866 "key": key_str,
867 "range_end": if request.range_end.is_empty() { None } else { Some(String::from_utf8_lossy(&request.range_end).to_string()) },
868 "limit": request.limit,
869 }),
870 );
871
872 let execution_result = call_llm(
874 &llm_client,
875 &app_state,
876 server_id,
877 None,
878 &event,
879 protocol.as_ref(),
880 )
881 .await
882 .map_err(|e| llm_failure(&status_tx, "Range", e))?;
883
884 if let Some(failure) = take_etcd_error(&execution_result.protocol_results) {
885 return Err(failure);
886 }
887
888 let meta_lock = meta.lock().await;
890 let mut kvs = vec![];
891 let mut more = false;
892 let mut count = 0;
893 let mut answered = false;
894
895 for protocol_result in &execution_result.protocol_results {
896 if let crate::llm::actions::protocol_trait::ActionResult::Custom { name, data } =
897 protocol_result
898 {
899 if name == "etcd_range_response" {
900 answered = true;
901 if let Some(kvs_array) = data.get("kvs").and_then(|v| v.as_array()) {
903 for kv_json in kvs_array {
904 let key = kv_json.get("key").and_then(|v| v.as_str()).unwrap_or("");
905 let value = kv_json.get("value").and_then(|v| v.as_str()).unwrap_or("");
906 let create_revision = kv_json
907 .get("create_revision")
908 .and_then(|v| v.as_i64())
909 .unwrap_or(0);
910 let mod_revision = kv_json
911 .get("mod_revision")
912 .and_then(|v| v.as_i64())
913 .unwrap_or(0);
914 let version =
915 kv_json.get("version").and_then(|v| v.as_i64()).unwrap_or(0);
916 let lease = kv_json.get("lease").and_then(|v| v.as_i64()).unwrap_or(0);
917
918 kvs.push(KeyValue {
919 key: key.as_bytes().to_vec(),
920 create_revision,
921 mod_revision,
922 version,
923 lease,
924 value: value.as_bytes().to_vec(),
925 });
926 }
927 }
928 more = data.get("more").and_then(|v| v.as_bool()).unwrap_or(false);
929 count = data
930 .get("count")
931 .and_then(|v| v.as_i64())
932 .unwrap_or(kvs.len() as i64);
933 }
934 }
935 }
936
937 if !answered {
948 drop(meta_lock);
949 Log::new(Some(&status_tx)).warn(
950 "etcd Range could not be answered: the handler produced no etcd_range_response \
951 (decision=fail_closed_no_action). Replying grpc-status 13 (INTERNAL); \
952 \"key not found\" is not being invented."
953 .to_string(),
954 );
955 return Err(GrpcFailure::new(
956 GRPC_INTERNAL,
957 crate::utils::WireFailure::Unavailable.prefixed_text(),
958 ));
959 }
960
961 let response = RangeResponse {
962 header: Some(meta_lock.get_response_header()),
963 kvs,
964 more,
965 count,
966 };
967
968 let mut buf = Vec::new();
969 response.encode(&mut buf)?;
970 Ok(buf)
971 }
972
973 async fn handle_put(
974 msg_bytes: &[u8],
975 llm_client: OllamaClient,
976 app_state: Arc<AppState>,
977 status_tx: mpsc::UnboundedSender<String>,
978 server_id: crate::state::ServerId,
979 meta: Arc<Mutex<EtcdMeta>>,
980 protocol: Arc<EtcdProtocol>,
981 ) -> std::result::Result<Vec<u8>, GrpcFailure> {
982 let request = PutRequest::decode(msg_bytes)?;
983
984 let key_str = String::from_utf8_lossy(&request.key);
985 let value_str = String::from_utf8_lossy(&request.value);
986 Log::new(Some(&status_tx)).debug(format!(
987 "etcd Put request: key={}, value={}",
988 key_str, value_str
989 ));
990
991 use crate::protocol::Event;
993 use crate::server::etcd::actions::ETCD_PUT_REQUEST_EVENT;
994
995 let event = Event::new(
996 &ETCD_PUT_REQUEST_EVENT,
997 serde_json::json!({
998 "key": key_str,
999 "value": value_str,
1000 "lease": request.lease,
1001 }),
1002 );
1003
1004 Log::new(Some(&status_tx)).debug("etcd calling LLM for Put request");
1005
1006 let execution_result = crate::llm::action_helper::call_llm(
1007 &llm_client,
1008 &app_state,
1009 server_id,
1010 None,
1011 &event,
1012 protocol.as_ref(),
1013 )
1014 .await
1015 .map_err(|e| llm_failure(&status_tx, "Put", e))?;
1016
1017 for message in &execution_result.messages {
1019 Log::new(Some(&status_tx)).info(format!("{}", message));
1020 }
1021
1022 Log::new(Some(&status_tx)).debug(format!(
1023 "etcd got {} protocol results",
1024 execution_result.protocol_results.len()
1025 ));
1026
1027 if let Some(failure) = take_etcd_error(&execution_result.protocol_results) {
1028 return Err(failure);
1029 }
1030
1031 let mut meta_lock = meta.lock().await;
1033 let mut revision: i64 = 0;
1034 let mut answered = false;
1035
1036 for protocol_result in &execution_result.protocol_results {
1037 if let crate::llm::actions::protocol_trait::ActionResult::Custom { name, data } =
1038 protocol_result
1039 {
1040 if name == "etcd_put_response" {
1041 answered = true;
1042 revision = data
1044 .get("revision")
1045 .and_then(|v| v.as_i64())
1046 .unwrap_or_else(|| {
1047 meta_lock.increment_revision();
1048 meta_lock.revision
1049 });
1050
1051 if revision > meta_lock.revision {
1053 meta_lock.revision = revision;
1054 }
1055 }
1056 }
1057 }
1058
1059 if !answered {
1068 drop(meta_lock);
1069 Log::new(Some(&status_tx)).warn(
1070 "etcd Put could not be answered: the handler produced no etcd_put_response \
1071 (decision=fail_closed_no_action). Replying grpc-status 13 (INTERNAL); no \
1072 revision is being invented."
1073 .to_string(),
1074 );
1075 return Err(GrpcFailure::new(
1076 GRPC_INTERNAL,
1077 crate::utils::WireFailure::Unavailable.prefixed_text(),
1078 ));
1079 }
1080
1081 let response = PutResponse {
1082 header: Some(meta_lock.get_response_header()),
1083 prev_kv: None,
1084 };
1085
1086 let mut buf = Vec::new();
1087 response.encode(&mut buf)?;
1088 Ok(buf)
1089 }
1090
1091 async fn handle_delete_range(
1092 msg_bytes: &[u8],
1093 llm_client: OllamaClient,
1094 app_state: Arc<AppState>,
1095 status_tx: mpsc::UnboundedSender<String>,
1096 server_id: crate::state::ServerId,
1097 meta: Arc<Mutex<EtcdMeta>>,
1098 protocol: Arc<EtcdProtocol>,
1099 ) -> std::result::Result<Vec<u8>, GrpcFailure> {
1100 let request = DeleteRangeRequest::decode(msg_bytes)?;
1101
1102 let key_str = String::from_utf8_lossy(&request.key);
1103 Log::new(Some(&status_tx)).debug(format!("etcd DeleteRange request: key={}", key_str));
1104
1105 use crate::protocol::Event;
1107 use crate::server::etcd::actions::ETCD_DELETE_REQUEST_EVENT;
1108
1109 let event = Event::new(
1110 &ETCD_DELETE_REQUEST_EVENT,
1111 serde_json::json!({
1112 "key": key_str,
1113 "range_end": if request.range_end.is_empty() {
1114 serde_json::Value::Null
1115 } else {
1116 serde_json::json!(String::from_utf8_lossy(&request.range_end))
1117 },
1118 }),
1119 );
1120
1121 Log::new(Some(&status_tx)).debug("etcd calling LLM for Delete request");
1122
1123 let execution_result = crate::llm::action_helper::call_llm(
1124 &llm_client,
1125 &app_state,
1126 server_id,
1127 None,
1128 &event,
1129 protocol.as_ref(),
1130 )
1131 .await
1132 .map_err(|e| llm_failure(&status_tx, "DeleteRange", e))?;
1133
1134 for message in &execution_result.messages {
1136 Log::new(Some(&status_tx)).info(format!("{}", message));
1137 }
1138
1139 Log::new(Some(&status_tx)).debug(format!(
1140 "etcd got {} protocol results",
1141 execution_result.protocol_results.len()
1142 ));
1143
1144 if let Some(failure) = take_etcd_error(&execution_result.protocol_results) {
1145 return Err(failure);
1146 }
1147
1148 let mut meta_lock = meta.lock().await;
1150 let mut deleted: i64 = 0;
1151 let mut answered = false;
1152
1153 for protocol_result in &execution_result.protocol_results {
1154 if let crate::llm::actions::protocol_trait::ActionResult::Custom { name, data } =
1155 protocol_result
1156 {
1157 if name == "etcd_delete_range_response" {
1158 answered = true;
1159 deleted = data.get("deleted").and_then(|v| v.as_i64()).unwrap_or(0);
1161 }
1162 }
1163 }
1164
1165 if !answered {
1170 drop(meta_lock);
1171 Log::new(Some(&status_tx)).warn(
1172 "etcd DeleteRange could not be answered: the handler produced no \
1173 etcd_delete_range_response (decision=fail_closed_no_action). Replying \
1174 grpc-status 13 (INTERNAL); no deletion is being reported.\
1175 "
1176 .to_string(),
1177 );
1178 return Err(GrpcFailure::new(
1179 GRPC_INTERNAL,
1180 crate::utils::WireFailure::Unavailable.prefixed_text(),
1181 ));
1182 }
1183
1184 meta_lock.increment_revision();
1185
1186 let response = DeleteRangeResponse {
1187 header: Some(meta_lock.get_response_header()),
1188 deleted,
1189 prev_kvs: vec![],
1190 };
1191
1192 let mut buf = Vec::new();
1193 response.encode(&mut buf)?;
1194 Ok(buf)
1195 }
1196
1197 async fn handle_txn(
1205 msg_bytes: &[u8],
1206 llm_client: OllamaClient,
1207 app_state: Arc<AppState>,
1208 status_tx: mpsc::UnboundedSender<String>,
1209 server_id: crate::state::ServerId,
1210 meta: Arc<Mutex<EtcdMeta>>,
1211 protocol: Arc<EtcdProtocol>,
1212 ) -> std::result::Result<Vec<u8>, GrpcFailure> {
1213 use crate::server::etcd::actions::ETCD_TXN_REQUEST_EVENT;
1214
1215 let request = TxnRequest::decode(msg_bytes)?;
1216
1217 Log::new(Some(&status_tx)).debug(format!(
1218 "etcd Txn request: {} compare(s), {} success op(s), {} failure op(s)",
1219 request.compare.len(),
1220 request.success.len(),
1221 request.failure.len()
1222 ));
1223
1224 let compares: Vec<serde_json::Value> = request
1227 .compare
1228 .iter()
1229 .map(|c| {
1230 serde_json::json!({
1231 "key": String::from_utf8_lossy(&c.key),
1232 "range_end": if c.range_end.is_empty() {
1233 serde_json::Value::Null
1234 } else {
1235 serde_json::json!(String::from_utf8_lossy(&c.range_end))
1236 },
1237 "result": match c.result {
1238 0 => "EQUAL",
1239 1 => "GREATER",
1240 2 => "LESS",
1241 3 => "NOT_EQUAL",
1242 _ => "UNKNOWN",
1243 },
1244 "target": match c.target {
1245 0 => "VERSION",
1246 1 => "CREATE",
1247 2 => "MOD",
1248 3 => "VALUE",
1249 4 => "LEASE",
1250 _ => "UNKNOWN",
1251 },
1252 })
1253 })
1254 .collect();
1255
1256 let event = Event::new(
1257 &ETCD_TXN_REQUEST_EVENT,
1258 serde_json::json!({
1259 "compare_count": request.compare.len(),
1260 "success_count": request.success.len(),
1261 "failure_count": request.failure.len(),
1262 "compares": compares,
1263 }),
1264 );
1265
1266 let execution_result = call_llm(
1267 &llm_client,
1268 &app_state,
1269 server_id,
1270 None,
1271 &event,
1272 protocol.as_ref(),
1273 )
1274 .await
1275 .map_err(|e| llm_failure(&status_tx, "Txn", e))?;
1276
1277 for message in &execution_result.messages {
1278 Log::new(Some(&status_tx)).info(format!("{}", message));
1279 }
1280
1281 if let Some(failure) = take_etcd_error(&execution_result.protocol_results) {
1282 return Err(failure);
1283 }
1284
1285 let mut succeeded = false;
1286 for protocol_result in &execution_result.protocol_results {
1287 if let crate::llm::actions::protocol_trait::ActionResult::Custom { name, data } =
1288 protocol_result
1289 {
1290 if name == "etcd_txn_response" {
1291 succeeded = data
1292 .get("succeeded")
1293 .and_then(|v| v.as_bool())
1294 .unwrap_or(false);
1295 }
1296 }
1297 }
1298
1299 let mut meta_lock = meta.lock().await;
1300 meta_lock.increment_revision();
1301
1302 let response = TxnResponse {
1303 header: Some(meta_lock.get_response_header()),
1304 succeeded,
1305 responses: vec![],
1306 };
1307
1308 let mut buf = Vec::new();
1309 response.encode(&mut buf)?;
1310 Ok(buf)
1311 }
1312
1313 async fn handle_compact(
1314 msg_bytes: &[u8],
1315 _llm_client: OllamaClient,
1316 _app_state: Arc<AppState>,
1317 status_tx: mpsc::UnboundedSender<String>,
1318 _server_id: crate::state::ServerId,
1319 meta: Arc<Mutex<EtcdMeta>>,
1320 _protocol: Arc<EtcdProtocol>,
1321 ) -> std::result::Result<Vec<u8>, GrpcFailure> {
1322 let request = CompactionRequest::decode(msg_bytes)?;
1323
1324 Log::new(Some(&status_tx)).debug(format!(
1325 "etcd Compact request: revision={}",
1326 request.revision
1327 ));
1328
1329 let meta_lock = meta.lock().await;
1330
1331 let response = CompactionResponse {
1332 header: Some(meta_lock.get_response_header()),
1333 };
1334
1335 let mut buf = Vec::new();
1336 response.encode(&mut buf)?;
1337 Ok(buf)
1338 }
1339}