grpc_webnext/schema.rs
1//! Descriptor source for `+json` termination.
2//!
3//! The proxy is schema-agnostic for binary `+proto` (it forwards opaque bytes), but
4//! turning `+json` into the binary protobuf an upstream expects needs the message
5//! descriptors. [`Schema`] resolves a [`Transcoder`] for a method path from one of
6//! three sources; the binary path never touches it.
7//!
8//! Reflection is loaded **eagerly and whole**: on startup the proxy enumerates every
9//! service the upstream exposes and builds a single transcoder covering all of them,
10//! then refreshes it on a TTL (and on an operator-forced [`Schema::reload`]). One
11//! consistent snapshot, rather than resolving each method lazily.
12
13use std::sync::Arc;
14use std::time::Duration;
15
16use bytes::Bytes;
17use crate::{TranscodeError, Transcoder};
18use tokio::sync::watch;
19use tonic::transport::Channel;
20use tonic::Status;
21
22use crate::reflect;
23
24/// How long a request will block waiting for the very first reflection load to land
25/// (subsequent loads are swapped in atomically, so requests never wait again).
26const FIRST_LOAD_WAIT: Duration = Duration::from_secs(10);
27/// Upper bound on a single reflection enumeration, so a wedged upstream can't hang the
28/// refresh task forever.
29const LOAD_TIMEOUT: Duration = Duration::from_secs(30);
30/// Retry cadence after a failed load (e.g. upstream not up yet), capped by the TTL.
31const RETRY_INTERVAL: Duration = Duration::from_secs(30);
32
33/// Where the proxy gets the descriptors it needs to transcode `+json`.
34#[derive(Clone, Default)]
35pub enum SchemaSource {
36 /// Binary-only: `+json` is rejected with `Unimplemented` (the default).
37 #[default]
38 None,
39 /// Fetch descriptors from the upstream's gRPC reflection service. The whole schema
40 /// is enumerated eagerly and refreshed on a TTL (see `ProxyConfig::reflection_ttl`).
41 Reflection,
42 /// A precompiled `FileDescriptorSet` (`protoc --descriptor_set_out` / prost
43 /// `file_descriptor_set_path`), for upstreams that don't expose reflection.
44 Bundled(Bytes),
45 /// Reflection primary, with the bundle as a fallback: the bundle serves immediately
46 /// (and if the upstream has no reflection at all), and the live reflection snapshot
47 /// takes over once it loads. Best of both — a baked-in safety net plus live
48 /// descriptors when the upstream supports reflection.
49 ReflectionOrBundled(Bytes),
50}
51
52/// Resolves a [`Transcoder`] for a gRPC method path according to a [`SchemaSource`].
53/// Cheap to clone; the reflection snapshot is shared.
54#[derive(Clone)]
55pub struct Schema {
56 inner: Inner,
57}
58
59#[derive(Clone)]
60enum Inner {
61 None,
62 /// One transcoder covers every method in the bundle.
63 Bundled(Arc<Transcoder>),
64 Reflection(Arc<ReflectionState>),
65 /// Reflection when its snapshot is loaded, else the bundle.
66 ReflectionOrBundled { reflection: Arc<ReflectionState>, bundle: Arc<Transcoder> },
67}
68
69/// The live reflection snapshot plus what's needed to refresh it. The current
70/// transcoder rides a `watch` channel: readers see the latest atomically, and a reader
71/// that arrives before the first load blocks on it.
72struct ReflectionState {
73 channel: Channel,
74 ttl: Duration,
75 tx: watch::Sender<Option<Arc<Transcoder>>>,
76 rx: watch::Receiver<Option<Arc<Transcoder>>>,
77}
78
79impl ReflectionState {
80 /// Enumerate the upstream and atomically swap in the new snapshot.
81 async fn load_and_swap(&self) -> Result<(), Status> {
82 let tc = match tokio::time::timeout(LOAD_TIMEOUT, reflect::load_all(&self.channel)).await {
83 Ok(result) => result?,
84 Err(_) => return Err(Status::deadline_exceeded("upstream reflection load timed out")),
85 };
86 let _ = self.tx.send(Some(Arc::new(tc)));
87 Ok(())
88 }
89
90 /// The current snapshot, or `None` if no load has landed yet (non-blocking).
91 fn current(&self) -> Option<Arc<Transcoder>> {
92 self.rx.borrow().clone()
93 }
94
95 /// The current transcoder, blocking (bounded) for the first load if none has landed.
96 async fn transcoder(&self) -> Result<Arc<Transcoder>, Status> {
97 let mut rx = self.rx.clone();
98 loop {
99 if let Some(tc) = rx.borrow_and_update().clone() {
100 return Ok(tc);
101 }
102 if tokio::time::timeout(FIRST_LOAD_WAIT, rx.changed()).await.is_err() {
103 return Err(Status::unavailable("descriptors not yet loaded from upstream reflection"));
104 }
105 }
106 }
107}
108
109/// Background task: load once, then keep the snapshot fresh on the TTL. A failed load
110/// keeps the previous snapshot (if any) and retries sooner than the TTL.
111async fn refresh_loop(state: Arc<ReflectionState>) {
112 loop {
113 let wait = match state.load_and_swap().await {
114 Ok(()) => {
115 tracing::info!("proxy: loaded +json descriptors from upstream reflection");
116 state.ttl
117 }
118 Err(e) => {
119 tracing::warn!("proxy: reflection load failed ({e}); will retry");
120 RETRY_INTERVAL.min(state.ttl)
121 }
122 };
123 tokio::time::sleep(wait).await;
124 }
125}
126
127impl Schema {
128 /// Build a schema from an already-constructed transcoder — the in-process case, where
129 /// the descriptors live in memory and there is no upstream to reflect against. `None`
130 /// means no `+json` transcoder is configured.
131 pub fn from_transcoder(transcoder: Option<Arc<Transcoder>>) -> Self {
132 Self { inner: transcoder.map(Inner::Bundled).unwrap_or(Inner::None) }
133 }
134
135 /// Build a resolver. The `Bundled` set is parsed eagerly so a bad descriptor set
136 /// fails at startup; `channel` is the upstream connection reused for reflection, and
137 /// `ttl` is the reflection refresh interval. Call [`Schema::start`] afterwards to
138 /// kick off the reflection loader.
139 pub fn build(source: SchemaSource, channel: Channel, ttl: Duration) -> Result<Self, TranscodeError> {
140 let inner = match source {
141 SchemaSource::None => Inner::None,
142 SchemaSource::Bundled(bytes) => {
143 Inner::Bundled(Arc::new(Transcoder::from_file_descriptor_set(&bytes)?))
144 }
145 SchemaSource::Reflection => {
146 let (tx, rx) = watch::channel(None);
147 Inner::Reflection(Arc::new(ReflectionState { channel, ttl, tx, rx }))
148 }
149 SchemaSource::ReflectionOrBundled(bytes) => {
150 let bundle = Arc::new(Transcoder::from_file_descriptor_set(&bytes)?);
151 let (tx, rx) = watch::channel(None);
152 Inner::ReflectionOrBundled {
153 reflection: Arc::new(ReflectionState { channel, ttl, tx, rx }),
154 bundle,
155 }
156 }
157 };
158 Ok(Self { inner })
159 }
160
161 /// Start the background reflection loader (eager initial load + TTL refresh). A
162 /// no-op unless the source involves reflection. Must be called from within a Tokio
163 /// runtime.
164 pub fn start(&self) {
165 let state = match &self.inner {
166 Inner::Reflection(state) => state,
167 Inner::ReflectionOrBundled { reflection, .. } => reflection,
168 _ => return,
169 };
170 tokio::spawn(refresh_loop(state.clone()));
171 }
172
173 /// Force an immediate reflection reload, returning its result. The management hook
174 /// behind `ProxyConfig::admin_reload_path`.
175 pub async fn reload(&self) -> Result<(), Status> {
176 match &self.inner {
177 Inner::Reflection(state) | Inner::ReflectionOrBundled { reflection: state, .. } => {
178 state.load_and_swap().await
179 }
180 Inner::None => Err(Status::failed_precondition("no descriptor source to reload")),
181 Inner::Bundled(_) => {
182 Err(Status::failed_precondition("bundled descriptors are static; nothing to reload"))
183 }
184 }
185 }
186
187 /// Whether this proxy can transcode `+json` at all.
188 pub fn enabled(&self) -> bool {
189 !matches!(self.inner, Inner::None)
190 }
191
192 /// The transcoder for this source, without validating any specific method — for
193 /// REST annotation matching, where the target method isn't known until the URL is
194 /// matched against the router.
195 pub async fn transcoder_any(&self) -> Result<Arc<Transcoder>, Status> {
196 match &self.inner {
197 Inner::None => Err(Status::unimplemented(
198 "no +json transcoder configured (pass a transcoder in-process, or enable upstream reflection / bundle a descriptor set on the proxy)",
199 )),
200 Inner::Bundled(tc) => Ok(tc.clone()),
201 Inner::Reflection(state) => state.transcoder().await,
202 // Prefer the live reflection snapshot; fall back to the bundle immediately
203 // (no wait) while reflection loads or if the upstream has no reflection.
204 Inner::ReflectionOrBundled { reflection, bundle } => {
205 Ok(reflection.current().unwrap_or_else(|| bundle.clone()))
206 }
207 }
208 }
209
210 /// Resolve the transcoder for `method_path` (`/pkg.Service/Method`). An unknown
211 /// method yields `Unimplemented` (uniformly across sources), distinct from a
212 /// transcode failure.
213 pub async fn transcoder(&self, method_path: &str) -> Result<Arc<Transcoder>, Status> {
214 let tc = self.transcoder_any().await?;
215 if tc.has_method(method_path) {
216 Ok(tc)
217 } else {
218 Err(Status::unimplemented(format!("no descriptor for method {method_path}")))
219 }
220 }
221}