lean_ctx/core/ocla/
grpc_bridge.rs1use std::net::SocketAddr;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::{Mutex, OnceLock};
10
11use serde::{Deserialize, Serialize};
12use tokio::net::TcpListener;
13use tokio::task::JoinHandle;
14use tracing::{debug, warn};
15
16use super::types::{OclaError, OclaResult};
17
18const DEFAULT_GRPC_LISTEN: &str = "127.0.0.1:50051";
19static GRPC_RUNNING: AtomicBool = AtomicBool::new(false);
20static GRPC_TASK: OnceLock<Mutex<Option<JoinHandle<()>>>> = OnceLock::new();
21
22fn grpc_task() -> &'static Mutex<Option<JoinHandle<()>>> {
23 GRPC_TASK.get_or_init(|| Mutex::new(None))
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(default)]
29pub struct GrpcConfig {
30 pub enabled: bool,
32 pub listen: String,
34}
35
36impl Default for GrpcConfig {
37 fn default() -> Self {
38 Self {
39 enabled: false,
40 listen: DEFAULT_GRPC_LISTEN.to_owned(),
41 }
42 }
43}
44
45struct RunningGuard;
46
47impl Drop for RunningGuard {
48 fn drop(&mut self) {
49 GRPC_RUNNING.store(false, Ordering::Release);
50 }
51}
52
53pub async fn start_grpc_server(config: &GrpcConfig) -> OclaResult<()> {
59 if !config.enabled {
60 return Ok(());
61 }
62
63 let address = config.listen.parse::<SocketAddr>().map_err(|error| {
64 OclaError::InvalidRequest(format!("invalid OCLA gRPC listen address: {error}"))
65 })?;
66 if !address.ip().is_loopback() {
67 return Err(OclaError::InvalidRequest(
68 "OCLA gRPC listener must use a loopback address".into(),
69 ));
70 }
71 if GRPC_RUNNING.load(Ordering::Acquire) {
72 return Err(OclaError::InvalidRequest(
73 "OCLA gRPC server is already running".into(),
74 ));
75 }
76 tokio::runtime::Handle::try_current().map_err(|_| {
77 OclaError::InvalidRequest("OCLA gRPC startup requires a Tokio runtime".into())
78 })?;
79
80 let listener = TcpListener::bind(address).await.map_err(|error| {
81 OclaError::InvalidRequest(format!("failed to bind OCLA gRPC listener: {error}"))
82 })?;
83
84 let task = tokio::spawn(async move {
85 let _running = RunningGuard;
86 debug!(%address, "OCLA gRPC listener task started");
87 loop {
88 match listener.accept().await {
89 Ok((stream, peer)) => {
90 debug!(%peer, "accepted OCLA gRPC connection");
91 drop(stream);
92 }
93 Err(error) => {
94 warn!(%error, "OCLA gRPC listener stopped accepting connections");
95 break;
96 }
97 }
98 }
99 });
100
101 let Ok(mut stored_task) = grpc_task().lock() else {
102 task.abort();
103 GRPC_RUNNING.store(false, Ordering::Release);
104 return Err(OclaError::InvalidRequest(
105 "OCLA gRPC task state is unavailable".into(),
106 ));
107 };
108 if GRPC_RUNNING.load(Ordering::Acquire)
109 || stored_task
110 .as_ref()
111 .is_some_and(|existing| !existing.is_finished())
112 {
113 task.abort();
114 return Err(OclaError::InvalidRequest(
115 "OCLA gRPC server is already running".into(),
116 ));
117 }
118 if let Some(existing) = stored_task.take() {
119 existing.abort();
120 }
121 *stored_task = Some(task);
122 GRPC_RUNNING.store(true, Ordering::Release);
123 Ok(())
124}
125
126pub async fn stop_grpc_server() {
128 let task = if let Ok(mut stored_task) = grpc_task().lock() {
129 stored_task.take()
130 } else {
131 warn!("OCLA gRPC task state is unavailable during shutdown");
132 None
133 };
134 if let Some(task) = task {
135 task.abort();
136 let _ = task.await;
137 }
138 GRPC_RUNNING.store(false, Ordering::Release);
139}
140
141pub fn is_grpc_available() -> bool {
143 GRPC_RUNNING.load(Ordering::Acquire)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn grpc_config_defaults_to_disabled_loopback_listener() {
152 let config = GrpcConfig::default();
153
154 assert!(!config.enabled);
155 assert_eq!(config.listen, DEFAULT_GRPC_LISTEN);
156 }
157
158 #[test]
159 fn grpc_listen_address_parses() {
160 let config = GrpcConfig {
161 listen: "127.0.0.1:60051".into(),
162 ..GrpcConfig::default()
163 };
164
165 assert_eq!(config.listen.parse::<SocketAddr>().unwrap().port(), 60051);
166 }
167
168 #[tokio::test]
169 #[serial_test::serial]
170 async fn grpc_server_binds_and_accepts() {
171 let config = GrpcConfig {
172 enabled: true,
173 listen: "127.0.0.1:0".into(),
174 };
175
176 assert!(start_grpc_server(&config).await.is_ok());
177 assert!(is_grpc_available());
178 stop_grpc_server().await;
179 assert!(!is_grpc_available());
180 }
181
182 #[tokio::test]
183 #[serial_test::serial]
184 async fn disabled_config_does_not_bind() {
185 stop_grpc_server().await;
186 assert!(start_grpc_server(&GrpcConfig::default()).await.is_ok());
187 assert!(!is_grpc_available());
188 }
189
190 #[tokio::test]
191 #[serial_test::serial]
192 async fn stop_sets_unavailable() {
193 let config = GrpcConfig {
194 enabled: true,
195 listen: "127.0.0.1:0".into(),
196 };
197
198 start_grpc_server(&config).await.unwrap();
199 assert!(is_grpc_available());
200 stop_grpc_server().await;
201 assert!(!is_grpc_available());
202 }
203}