fix(gRPC): Cache descriptor pools to avoid re-reflection; add manual “Refresh Schema” to force re-fetch (#317)

This commit is contained in:
Mikhail Mamontov
2025-12-10 00:35:35 +01:00
committed by GitHub
parent 846f4d9551
commit ef1ba9b834
3 changed files with 31 additions and 6 deletions

View File

@@ -156,6 +156,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
request_id: &str, request_id: &str,
environment_id: Option<&str>, environment_id: Option<&str>,
proto_files: Vec<String>, proto_files: Vec<String>,
skip_cache: Option<bool>,
window: WebviewWindow<R>, window: WebviewWindow<R>,
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
grpc_handle: State<'_, Mutex<GrpcHandle>>, grpc_handle: State<'_, Mutex<GrpcHandle>>,
@@ -196,6 +197,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(), &proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
&metadata, &metadata,
workspace.setting_validate_certificates, workspace.setting_validate_certificates,
skip_cache.unwrap_or(false),
) )
.await .await
.map_err(|e| GenericError(e.to_string()))?) .map_err(|e| GenericError(e.to_string()))?)

View File

@@ -7,7 +7,7 @@ use crate::{MethodDefinition, ServiceDefinition, json_schema};
use hyper_rustls::HttpsConnector; use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::Client; use hyper_util::client::legacy::Client;
use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::client::legacy::connect::HttpConnector;
use log::warn; use log::{info, warn};
pub use prost_reflect::DynamicMessage; pub use prost_reflect::DynamicMessage;
use prost_reflect::{DescriptorPool, MethodDescriptor, ServiceDescriptor}; use prost_reflect::{DescriptorPool, MethodDescriptor, ServiceDescriptor};
use serde_json::Deserializer; use serde_json::Deserializer;
@@ -244,6 +244,12 @@ impl GrpcHandle {
} }
impl GrpcHandle { impl GrpcHandle {
/// Remove cached descriptor pool for the given key, if present.
pub fn invalidate_pool(&mut self, id: &str, uri: &str, proto_files: &Vec<PathBuf>) {
let key = make_pool_key(id, uri, proto_files);
self.pools.remove(&key);
}
pub async fn reflect( pub async fn reflect(
&mut self, &mut self,
id: &str, id: &str,
@@ -253,6 +259,13 @@ impl GrpcHandle {
validate_certificates: bool, validate_certificates: bool,
) -> Result<bool, String> { ) -> Result<bool, String> {
let server_reflection = proto_files.is_empty(); let server_reflection = proto_files.is_empty();
let key = make_pool_key(id, uri, proto_files);
// If we already have a pool for this key, reuse it and avoid re-reflection
if self.pools.contains_key(&key) {
return Ok(server_reflection);
}
let pool = if server_reflection { let pool = if server_reflection {
let full_uri = uri_from_str(uri)?; let full_uri = uri_from_str(uri)?;
fill_pool_from_reflection(&full_uri, metadata, validate_certificates).await fill_pool_from_reflection(&full_uri, metadata, validate_certificates).await
@@ -260,7 +273,7 @@ impl GrpcHandle {
fill_pool_from_files(&self.app_handle, proto_files).await fill_pool_from_files(&self.app_handle, proto_files).await
}?; }?;
self.pools.insert(make_pool_key(id, uri, proto_files), pool.clone()); self.pools.insert(key, pool.clone());
Ok(server_reflection) Ok(server_reflection)
} }
@@ -271,9 +284,13 @@ impl GrpcHandle {
proto_files: &Vec<PathBuf>, proto_files: &Vec<PathBuf>,
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
validate_certificates: bool, validate_certificates: bool,
skip_cache: bool,
) -> Result<Vec<ServiceDefinition>, String> { ) -> Result<Vec<ServiceDefinition>, String> {
// Ensure reflection is up-to-date // Ensure we have a pool; reflect only if missing
self.reflect(id, uri, proto_files, metadata, validate_certificates).await?; if skip_cache || self.get_pool(id, uri, proto_files).is_none() {
info!("Reflecting gRPC services for {} at {}", id, uri);
self.reflect(id, uri, proto_files, metadata, validate_certificates).await?;
}
let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool".to_string())?; let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool".to_string())?;
Ok(self.services_from_pool(&pool)) Ok(self.services_from_pool(&pool))
@@ -312,8 +329,10 @@ impl GrpcHandle {
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
validate_certificates: bool, validate_certificates: bool,
) -> Result<GrpcConnection, String> { ) -> Result<GrpcConnection, String> {
let use_reflection = let use_reflection = proto_files.is_empty();
if self.get_pool(id, uri, proto_files).is_none() {
self.reflect(id, uri, proto_files, metadata, validate_certificates).await?; self.reflect(id, uri, proto_files, metadata, validate_certificates).await?;
}
let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool")?.clone(); let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool")?.clone();
let uri = uri_from_str(uri)?; let uri = uri_from_str(uri)?;
let conn = get_transport(validate_certificates); let conn = get_transport(validate_certificates);

View File

@@ -47,10 +47,14 @@ export function useGrpc(
const reflect = useQuery<ReflectResponseService[], string>({ const reflect = useQuery<ReflectResponseService[], string>({
enabled: req != null, enabled: req != null,
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl, protoFiles], queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl, protoFiles],
staleTime: Infinity,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
queryFn: () => { queryFn: () => {
const environmentId = jotaiStore.get(activeEnvironmentIdAtom); const environmentId = jotaiStore.get(activeEnvironmentIdAtom);
return minPromiseMillis<ReflectResponseService[]>( return minPromiseMillis<ReflectResponseService[]>(
invokeCmd('cmd_grpc_reflect', { requestId, protoFiles, environmentId }), invokeCmd('cmd_grpc_reflect', { requestId, protoFiles, environmentId, skipCache: true }),
300, 300,
); );
}, },