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 warn!(
91 %peer,
92 "dropped OCLA gRPC connection because no gRPC service is attached"
93 );
94 }
95 Err(error) => {
96 warn!(%error, "OCLA gRPC listener stopped accepting connections");
97 break;
98 }
99 }
100 }
101 });
102
103 let Ok(mut stored_task) = grpc_task().lock() else {
104 task.abort();
105 GRPC_RUNNING.store(false, Ordering::Release);
106 return Err(OclaError::InvalidRequest(
107 "OCLA gRPC task state is unavailable".into(),
108 ));
109 };
110 if GRPC_RUNNING.load(Ordering::Acquire)
111 || stored_task
112 .as_ref()
113 .is_some_and(|existing| !existing.is_finished())
114 {
115 task.abort();
116 return Err(OclaError::InvalidRequest(
117 "OCLA gRPC server is already running".into(),
118 ));
119 }
120 if let Some(existing) = stored_task.take() {
121 existing.abort();
122 }
123 *stored_task = Some(task);
124 GRPC_RUNNING.store(true, Ordering::Release);
125 Ok(())
126}
127
128pub async fn stop_grpc_server() {
130 let task = if let Ok(mut stored_task) = grpc_task().lock() {
131 stored_task.take()
132 } else {
133 warn!("OCLA gRPC task state is unavailable during shutdown");
134 None
135 };
136 if let Some(task) = task {
137 task.abort();
138 let _ = task.await;
139 }
140 GRPC_RUNNING.store(false, Ordering::Release);
141}
142
143pub fn is_grpc_listener_running() -> bool {
148 GRPC_RUNNING.load(Ordering::Acquire)
149}
150
151pub fn is_grpc_service_ready() -> bool {
155 false
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn grpc_config_defaults_to_disabled_loopback_listener() {
164 let config = GrpcConfig::default();
165
166 assert!(!config.enabled);
167 assert_eq!(config.listen, DEFAULT_GRPC_LISTEN);
168 }
169
170 #[test]
171 fn grpc_listen_address_parses() {
172 let config = GrpcConfig {
173 listen: "127.0.0.1:60051".into(),
174 ..GrpcConfig::default()
175 };
176
177 assert_eq!(config.listen.parse::<SocketAddr>().unwrap().port(), 60051);
178 }
179
180 #[tokio::test]
181 #[serial_test::serial]
182 async fn grpc_server_binds_and_accepts() {
183 let config = GrpcConfig {
184 enabled: true,
185 listen: "127.0.0.1:0".into(),
186 };
187
188 assert!(start_grpc_server(&config).await.is_ok());
189 assert!(is_grpc_listener_running());
190 assert!(!is_grpc_service_ready());
191 stop_grpc_server().await;
192 assert!(!is_grpc_listener_running());
193 }
194
195 #[tokio::test]
196 #[serial_test::serial]
197 async fn disabled_config_does_not_bind() {
198 stop_grpc_server().await;
199 assert!(start_grpc_server(&GrpcConfig::default()).await.is_ok());
200 assert!(!is_grpc_listener_running());
201 assert!(!is_grpc_service_ready());
202 }
203
204 #[tokio::test]
205 #[serial_test::serial]
206 async fn stop_sets_unavailable() {
207 let config = GrpcConfig {
208 enabled: true,
209 listen: "127.0.0.1:0".into(),
210 };
211
212 start_grpc_server(&config).await.unwrap();
213 assert!(is_grpc_listener_running());
214 assert!(!is_grpc_service_ready());
215 stop_grpc_server().await;
216 assert!(!is_grpc_listener_running());
217 }
218}