1use std::borrow::Cow;
13use std::fmt;
14use std::sync::Arc;
15use std::time::Duration;
16
17use relay_config::Config;
18use relay_config::RelayMode;
19use relay_dynamic_config::GlobalConfig;
20use relay_statsd::metric;
21use relay_system::{Addr, AsyncResponse, Controller, FromMessage, Interface, Service};
22use reqwest::Method;
23use serde::{Deserialize, Serialize};
24use tokio::sync::{mpsc, watch};
25use tokio::time::Instant;
26
27use crate::services::upstream::{
28 RequestPriority, SendQuery, UpstreamQuery, UpstreamRelay, UpstreamRequestError,
29};
30use crate::statsd::{RelayCounters, RelayTimers};
31use crate::utils::SleepHandle;
32
33type UpstreamQueryResult =
36 Result<Result<GetGlobalConfigResponse, UpstreamRequestError>, relay_system::SendError>;
37
38#[derive(Debug, Deserialize, Serialize)]
40#[serde(rename_all = "camelCase")]
41struct GetGlobalConfigResponse {
42 global: Option<GlobalConfig>,
43 global_status: Option<StatusResponse>,
46}
47
48#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
50#[serde(rename_all = "lowercase")]
51pub enum StatusResponse {
52 Ready,
53 Pending,
54}
55
56impl StatusResponse {
57 pub fn is_ready(self) -> bool {
58 matches!(self, Self::Ready)
59 }
60}
61
62#[derive(Debug, Deserialize, Serialize)]
64#[serde(rename_all = "camelCase")]
65struct GetGlobalConfig {
66 global: bool,
67 public_keys: Vec<()>,
69}
70
71impl GetGlobalConfig {
72 fn new() -> GetGlobalConfig {
73 GetGlobalConfig {
74 global: true,
75 public_keys: vec![],
76 }
77 }
78}
79
80impl UpstreamQuery for GetGlobalConfig {
81 type Response = GetGlobalConfigResponse;
82
83 fn method(&self) -> reqwest::Method {
84 Method::POST
85 }
86
87 fn path(&self) -> std::borrow::Cow<'static, str> {
88 Cow::Borrowed("/api/0/relays/projectconfigs/?version=3")
89 }
90
91 fn retry() -> bool {
92 false
93 }
94
95 fn priority() -> super::upstream::RequestPriority {
96 RequestPriority::High
97 }
98
99 fn route(&self) -> &'static str {
100 "global_config"
101 }
102}
103
104pub struct Get;
106
107pub enum GlobalConfigManager {
113 Get(relay_system::Sender<Status>),
115}
116
117impl Interface for GlobalConfigManager {}
118
119impl FromMessage<Get> for GlobalConfigManager {
120 type Response = AsyncResponse<Status>;
121
122 fn from_message(_: Get, sender: relay_system::Sender<Status>) -> Self {
123 Self::Get(sender)
124 }
125}
126
127#[derive(Debug, Clone, Default)]
129pub enum Status {
130 Ready(Arc<GlobalConfig>),
136 #[default]
140 Pending,
141}
142
143impl Status {
144 pub fn is_ready(&self) -> bool {
146 matches!(self, Self::Ready(_))
147 }
148}
149
150#[derive(Clone)]
151pub struct GlobalConfigHandle {
152 watch: watch::Receiver<Status>,
153}
154
155impl GlobalConfigHandle {
156 #[cfg(test)]
158 pub fn fixed(config: GlobalConfig) -> Self {
159 let (_, watch) = watch::channel(Status::Ready(Arc::new(config)));
160 Self { watch }
161 }
162
163 pub fn is_ready(&self) -> bool {
165 self.watch.borrow().is_ready()
166 }
167
168 pub fn current(&self) -> Option<Arc<GlobalConfig>> {
173 match &*self.watch.borrow() {
174 Status::Ready(config) => Some(Arc::clone(config)),
175 Status::Pending => None,
176 }
177 }
178}
179
180impl fmt::Debug for GlobalConfigHandle {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.debug_tuple("GlobalConfigHandle")
183 .field(&*self.watch.borrow())
184 .finish()
185 }
186}
187
188#[derive(Debug)]
190pub struct GlobalConfigService {
191 config: Arc<Config>,
192 global_config_watch: watch::Sender<Status>,
194 internal_tx: mpsc::Sender<UpstreamQueryResult>,
196 internal_rx: mpsc::Receiver<UpstreamQueryResult>,
198 upstream: Addr<UpstreamRelay>,
200 fetch_handle: SleepHandle,
202 last_fetched: Instant,
204 upstream_failure_interval: Duration,
206 shutdown: bool,
208}
209
210impl GlobalConfigService {
211 pub fn new(
213 config: Arc<Config>,
214 upstream: Addr<UpstreamRelay>,
215 ) -> (Self, watch::Receiver<Status>) {
216 let (internal_tx, internal_rx) = mpsc::channel(1);
217 let (global_config_watch, rx) = watch::channel(Status::Pending);
218
219 (
220 Self {
221 config,
222 global_config_watch,
223 internal_tx,
224 internal_rx,
225 upstream,
226 fetch_handle: SleepHandle::idle(),
227 last_fetched: Instant::now(),
228 upstream_failure_interval: Duration::from_secs(35),
229 shutdown: false,
230 },
231 rx,
232 )
233 }
234
235 pub fn handle(&self) -> GlobalConfigHandle {
238 GlobalConfigHandle {
239 watch: self.global_config_watch.subscribe(),
240 }
241 }
242
243 fn handle_message(&mut self, message: GlobalConfigManager) {
245 match message {
246 GlobalConfigManager::Get(sender) => {
247 sender.send(self.global_config_watch.borrow().clone());
248 }
249 }
250 }
251
252 fn schedule_fetch(&mut self) {
254 if !self.shutdown && self.fetch_handle.is_idle() {
255 self.fetch_handle
256 .set(self.config.current().global_config_fetch_interval());
257 }
258 }
259
260 fn request_global_config(&mut self) {
265 self.fetch_handle.reset();
267
268 let upstream_relay = self.upstream.clone();
269 let internal_tx = self.internal_tx.clone();
270
271 relay_system::spawn!(async move {
272 metric!(timer(RelayTimers::GlobalConfigRequestDuration), {
273 let query = GetGlobalConfig::new();
274 let res = upstream_relay.send(SendQuery(query)).await;
275 internal_tx.send(res).await.ok();
278 });
279 });
280 }
281
282 fn handle_result(&mut self, result: UpstreamQueryResult) {
290 match result {
291 Ok(Ok(response)) => {
292 let mut success = false;
293 let is_ready = response.global_status.is_none_or(|stat| stat.is_ready());
296
297 match response.global {
298 Some(global_config) if is_ready => {
299 if !self.global_config_watch.borrow().is_ready() {
301 relay_log::info!("received global config from upstream");
302 }
303
304 self.global_config_watch
305 .send_replace(Status::Ready(Arc::new(global_config)));
306 success = true;
307 self.last_fetched = Instant::now();
308 }
309 Some(_) => relay_log::info!("global config from upstream is not yet ready"),
310 None => relay_log::error!("global config missing in upstream response"),
311 }
312 metric!(
313 counter(RelayCounters::GlobalConfigFetched) += 1,
314 success = if success { "true" } else { "false" },
315 );
316 }
317 Ok(Err(e)) => {
318 if self.last_fetched.elapsed() >= self.upstream_failure_interval {
319 relay_log::error!(
320 error = &e as &dyn std::error::Error,
321 "failed to fetch global config from upstream"
322 );
323 }
324 }
325 Err(e) => relay_log::error!(
326 error = &e as &dyn std::error::Error,
327 "failed to send request to upstream"
328 ),
329 }
330
331 self.schedule_fetch();
333 }
334
335 fn handle_shutdown(&mut self) {
336 self.shutdown = true;
337 self.fetch_handle.reset();
338 }
339}
340
341impl Service for GlobalConfigService {
342 type Interface = GlobalConfigManager;
343
344 async fn run(mut self, mut rx: relay_system::Receiver<Self::Interface>) {
345 let mut shutdown_handle = Controller::shutdown_handle();
346
347 relay_log::info!("global config service starting");
348 if self.config.current().relay_mode() == RelayMode::Managed {
349 relay_log::info!("requesting global config from upstream");
350 self.request_global_config();
351 } else {
352 match GlobalConfig::load(self.config.path()) {
353 Ok(Some(from_file)) => {
354 relay_log::info!("serving static global config loaded from file");
355 self.global_config_watch
356 .send_replace(Status::Ready(Arc::new(from_file)));
357 }
358 Ok(None) => {
359 relay_log::info!(
360 "serving default global configs due to lacking static global config file"
361 );
362 self.global_config_watch
363 .send_replace(Status::Ready(Arc::default()));
364 }
365 Err(e) => {
366 relay_log::error!("failed to load global config from file: {}", e);
367 relay_log::info!(
368 "serving default global configs due to failure to load global config from file"
369 );
370 self.global_config_watch
371 .send_replace(Status::Ready(Arc::default()));
372 }
373 }
374 };
375
376 loop {
377 tokio::select! {
378 biased;
379
380 () = &mut self.fetch_handle => self.request_global_config(),
381 Some(result) = self.internal_rx.recv() => self.handle_result(result),
382 Some(message) = rx.recv() => self.handle_message(message),
383 _ = shutdown_handle.notified() => self.handle_shutdown(),
384
385 else => break,
386 }
387 }
388 relay_log::info!("global config service stopped");
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use std::sync::Arc;
395 use std::time::Duration;
396
397 use relay_config::{Config, Credentials, RelayMode};
398 use relay_system::{Controller, Service, ShutdownMode};
399 use relay_test::mock_service;
400
401 use crate::services::global_config::{Get, GlobalConfigService};
402
403 #[tokio::test]
406 async fn shutdown_service() {
407 relay_test::setup();
408 tokio::time::pause();
409
410 let (upstream, _) = mock_service("upstream", 0, |state, _| {
411 *state += 1;
412
413 if *state > 1 {
414 panic!("should not receive requests after shutdown");
415 }
416 });
417
418 Controller::start(Duration::from_secs(1));
419 let mut config = Config::default();
420 config
421 .replace_credentials(Some(Credentials::generate()))
422 .unwrap();
423 let fetch_interval = config.current().global_config_fetch_interval();
424
425 let service = GlobalConfigService::new(Arc::new(config), upstream)
426 .0
427 .start_detached();
428
429 assert!(service.send(Get).await.is_ok());
430
431 Controller::shutdown(ShutdownMode::Immediate);
432 tokio::time::sleep(fetch_interval * 2).await;
433
434 assert!(service.send(Get).await.is_ok());
435 }
436
437 #[tokio::test]
438 #[should_panic]
439 async fn managed_relay_makes_upstream_request() {
440 relay_test::setup();
441 tokio::time::pause();
442
443 let (upstream, handle) = mock_service("upstream", (), |(), _| {
444 panic!();
445 });
446
447 let mut config = Config::from_json_value(serde_json::json!({
448 "relay": {
449 "mode": RelayMode::Managed
450 }
451 }))
452 .unwrap();
453 config
454 .replace_credentials(Some(Credentials::generate()))
455 .unwrap();
456
457 let fetch_interval = config.current().global_config_fetch_interval();
458 let service = GlobalConfigService::new(Arc::new(config), upstream)
459 .0
460 .start_detached();
461 service.send(Get).await.unwrap();
462
463 tokio::time::sleep(fetch_interval * 2).await;
464 handle.await.unwrap();
465 }
466
467 #[tokio::test]
468 async fn proxy_relay_does_not_make_upstream_request() {
469 relay_test::setup();
470 tokio::time::pause();
471
472 let (upstream, _) = mock_service("upstream", (), |(), _| {
473 panic!("upstream should not be called outside of managed mode");
474 });
475
476 let config = Config::from_json_value(serde_json::json!({
477 "relay": {
478 "mode": RelayMode::Proxy
479 }
480 }))
481 .unwrap();
482
483 let fetch_interval = config.current().global_config_fetch_interval();
484
485 let service = GlobalConfigService::new(Arc::new(config), upstream)
486 .0
487 .start_detached();
488 service.send(Get).await.unwrap();
489
490 tokio::time::sleep(fetch_interval * 2).await;
491 }
492}