Skip to main content

relay_server/services/
global_config.rs

1//! This module implements the Global Config service.
2//!
3//! The global config service is a Relay service to manage [`GlobalConfig`]s,
4//! from fetching to forwarding. Once the service is started, it requests
5//! recurrently the configs from upstream in a timely manner to provide it to
6//! the rest of Relay.
7//!
8//! There are two ways to interact with this service: requesting a single global
9//! config update or subscribing for updates; see [`GlobalConfigManager`] for
10//! more details.
11
12use 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
33/// The result of sending a global config query to upstream.
34/// It can fail both in sending it, and in the response.
35type UpstreamQueryResult =
36    Result<Result<GetGlobalConfigResponse, UpstreamRequestError>, relay_system::SendError>;
37
38/// The response of a fetch of a global config from upstream.
39#[derive(Debug, Deserialize, Serialize)]
40#[serde(rename_all = "camelCase")]
41struct GetGlobalConfigResponse {
42    global: Option<GlobalConfig>,
43    // Instead of using [`Status`], we use StatusResponse as a separate field in order to not
44    // make breaking changes to the api.
45    global_status: Option<StatusResponse>,
46}
47
48/// A mirror of [`Status`] without the associated data for use in serialization.
49#[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/// The request to fetch a global config from upstream.
63#[derive(Debug, Deserialize, Serialize)]
64#[serde(rename_all = "camelCase")]
65struct GetGlobalConfig {
66    global: bool,
67    // Dummy variable - upstream expects a list of public keys.
68    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
104/// The message for requesting the most recent global config from [`GlobalConfigService`].
105pub struct Get;
106
107/// An interface to get [`GlobalConfig`]s through [`GlobalConfigService`].
108///
109/// For a one-off update, [`GlobalConfigService`] responds to
110/// [`GlobalConfigManager::Get`] messages with the latest instance of the
111/// [`GlobalConfig`].
112pub enum GlobalConfigManager {
113    /// Returns the most recent global config.
114    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/// Describes the current fetching status of the [`GlobalConfig`] from the upstream.
128#[derive(Debug, Clone, Default)]
129pub enum Status {
130    /// Global config ready to be used by other services.
131    ///
132    /// This variant implies different things in different circumstances. In managed mode, it means
133    /// that we have received a config from upstream. In other modes the config is either
134    /// from a file or the default global config.
135    Ready(Arc<GlobalConfig>),
136    /// The global config is requested from the upstream but it has not arrived yet.
137    ///
138    /// This variant should never be sent after the first `Ready` has occurred.
139    #[default]
140    Pending,
141}
142
143impl Status {
144    /// Returns `true` if the global config is ready to be read.
145    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    /// Creates a new global config handle with a fixed global config.
157    #[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    /// Returns `true` if the global config was loaded from the upstream.
164    pub fn is_ready(&self) -> bool {
165        self.watch.borrow().is_ready()
166    }
167
168    /// Returns the currently loaded or a default global config.
169    ///
170    /// When no global config has been received from upstream yet,
171    /// this will return None.
172    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/// Service implementing the [`GlobalConfigManager`] interface.
189#[derive(Debug)]
190pub struct GlobalConfigService {
191    config: Arc<Config>,
192    /// Sender of the [`watch`] channel for the subscribers of the service.
193    global_config_watch: watch::Sender<Status>,
194    /// Sender of the internal channel to forward global configs from upstream.
195    internal_tx: mpsc::Sender<UpstreamQueryResult>,
196    /// Receiver of the internal channel to forward global configs from upstream.
197    internal_rx: mpsc::Receiver<UpstreamQueryResult>,
198    /// Upstream service to request global configs from.
199    upstream: Addr<UpstreamRelay>,
200    /// Handle to avoid multiple outgoing requests.
201    fetch_handle: SleepHandle,
202    /// Last instant the global config was successfully fetched in.
203    last_fetched: Instant,
204    /// Interval of upstream fetching failures before reporting such errors.
205    upstream_failure_interval: Duration,
206    /// Disables the upstream fetch loop.
207    shutdown: bool,
208}
209
210impl GlobalConfigService {
211    /// Creates a new [`GlobalConfigService`].
212    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    /// Creates a [`GlobalConfigHandle`] which can be used to retrieve the current state
236    /// of the global config at any time.
237    pub fn handle(&self) -> GlobalConfigHandle {
238        GlobalConfigHandle {
239            watch: self.global_config_watch.subscribe(),
240        }
241    }
242
243    /// Handles messages from external services.
244    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    /// Schedules the next global config request.
253    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    /// Requests a new global config from upstream.
261    ///
262    /// We check if we have credentials before sending,
263    /// otherwise we would log an [`UpstreamRequestError::NoCredentials`] error.
264    fn request_global_config(&mut self) {
265        // Disable upstream requests timer until we receive result of query.
266        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 forwarding should only fail when the internal
276                // receiver is closed.
277                internal_tx.send(res).await.ok();
278            });
279        });
280    }
281
282    /// Handles the response of an attempt to fetch the global config from
283    /// upstream.
284    ///
285    /// This function checks two levels of results:
286    /// 1. Whether the request to the upstream was successful.
287    /// 2. If the request was successful, it then checks whether the returned
288    ///    global config is valid and contains the expected data.
289    fn handle_result(&mut self, result: UpstreamQueryResult) {
290        match result {
291            Ok(Ok(response)) => {
292                let mut success = false;
293                // Older relays won't send a global status, in that case, we will pretend like the
294                // default global config is an up to date one, because that was the old behaviour.
295                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                        // Log the first time we receive a global config from upstream.
300                        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        // Enable upstream requests timer for global configs.
332        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    /// Tests that the service can still handle requests after sending a
404    /// shutdown signal.
405    #[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}