Skip to main content

relay_server/
service.rs

1use std::convert::Infallible;
2use std::sync::Arc;
3use std::time::Duration;
4
5use crate::metrics::MetricOutcomes;
6use crate::services::autoscaling::{AutoscalingMetricService, AutoscalingMetrics};
7use crate::services::buffer::{
8    ObservableEnvelopeBuffer, PartitionedEnvelopeBuffer, ProjectKeyPair,
9};
10use crate::services::cogs::{CogsService, CogsServiceRecorder};
11use crate::services::config_reload::ConfigReloadService;
12use crate::services::global_config::{
13    GlobalConfigHandle, GlobalConfigManager, GlobalConfigService,
14};
15use crate::services::health_check::{HealthCheck, HealthCheckService};
16use crate::services::metrics::RouterService;
17#[cfg(feature = "processing")]
18use crate::services::objectstore::Objectstore;
19#[cfg(feature = "processing")]
20use crate::services::objectstore::ObjectstoreService;
21use crate::services::outcome::{
22    ClientReportOutcomeProducerService, NullOutcomeProducerService, OutcomeProducerService,
23    TrackOutcome,
24};
25use crate::services::processor::{
26    self, EnvelopeProcessor, EnvelopeProcessorService, EnvelopeProcessorServicePool,
27};
28use crate::services::projects::cache::{ProjectCacheHandle, ProjectCacheService};
29use crate::services::projects::source::ProjectSource;
30use crate::services::proxy_processor::{ProxyAddrs, ProxyProcessorService};
31use crate::services::relays::{RelayCache, RelayCacheService};
32use crate::services::stats::RelayStats;
33#[cfg(feature = "processing")]
34use crate::services::store::{StoreService, StoreServicePool};
35use crate::services::upload::{self, Upload};
36use crate::services::upstream::{UpstreamRelay, UpstreamRelayService};
37use crate::utils::{MemoryChecker, MemoryStat, ThreadKind};
38#[cfg(feature = "processing")]
39use anyhow::Context;
40use anyhow::Result;
41use axum::extract::FromRequestParts;
42use axum::http::request::Parts;
43use relay_cogs::Cogs;
44use relay_config::{Config, ConfigSnapshot, EmitOutcomes, RelayMode};
45#[cfg(feature = "processing")]
46use relay_config::{RedisConfigRef, RedisConfigsRef};
47#[cfg(feature = "processing")]
48use relay_redis::AsyncRedisClient;
49#[cfg(feature = "processing")]
50use relay_redis::redis::Script;
51#[cfg(feature = "processing")]
52use relay_redis::{RedisClients, RedisError, RedisScripts};
53#[cfg(feature = "processing")]
54use relay_system::ConcurrentService;
55use relay_system::{Addr, Service, ServiceSpawn, ServiceSpawnExt as _, channel};
56
57/// Indicates the type of failure of the server.
58#[derive(Debug, thiserror::Error)]
59pub enum ServiceError {
60    /// GeoIp construction failed.
61    #[error("could not load the Geoip Db")]
62    GeoIp,
63
64    /// Initializing the Kafka producer failed.
65    #[cfg(feature = "processing")]
66    #[error("could not initialize kafka producer: {0}")]
67    Kafka(String),
68
69    /// Initializing the Redis client failed.
70    #[cfg(feature = "processing")]
71    #[error("could not initialize redis client during startup")]
72    Redis,
73}
74
75#[derive(Clone, Debug)]
76pub struct Registry {
77    pub cogs: Cogs,
78    pub health_check: Addr<HealthCheck>,
79    pub outcome_aggregator: Addr<TrackOutcome>,
80    pub processor: Addr<EnvelopeProcessor>,
81    pub relay_cache: Addr<RelayCache>,
82    pub global_config: Addr<GlobalConfigManager>,
83    pub upstream_relay: Addr<UpstreamRelay>,
84    pub envelope_buffer: Arc<PartitionedEnvelopeBuffer>,
85    pub project_cache_handle: ProjectCacheHandle,
86    pub autoscaling: Option<Addr<AutoscalingMetrics>>,
87    #[cfg(feature = "processing")]
88    pub objectstore: Option<Addr<Objectstore>>,
89    pub upload: Addr<Upload>,
90    pub global_config_handle: GlobalConfigHandle,
91}
92
93/// Constructs a Tokio [`relay_system::Runtime`] configured for running [services](relay_system::Service).
94pub fn create_runtime(name: &'static str, threads: usize) -> relay_system::Runtime {
95    relay_system::Runtime::builder(name)
96        .worker_threads(threads)
97        // Relay uses `spawn_blocking` only for Redis connections within the project
98        // cache, those should never exceed 100 concurrent connections
99        // (limited by connection pool).
100        //
101        // Relay also does not use other blocking operations from Tokio which require
102        // this pool, no usage of `tokio::fs` and `tokio::io::{Stdin, Stdout, Stderr}`.
103        //
104        // We limit the maximum amount of threads here, we've seen that Tokio
105        // expands this pool very very aggressively and basically never shrinks it
106        // which leads to a massive resource waste.
107        .max_blocking_threads(150)
108        // We also lower down the default (10s) keep alive timeout for blocking
109        // threads to encourage the runtime to not keep too many idle blocking threads
110        // around.
111        .thread_keep_alive(Duration::from_secs(1))
112        .build()
113}
114
115fn create_processor_pool(config: &ConfigSnapshot) -> Result<EnvelopeProcessorServicePool> {
116    // Adjust thread count for small cpu counts to not have too many idle cores
117    // and distribute workload better.
118    let thread_count = match config.cpu_concurrency() {
119        conc @ 0..=2 => conc.max(1),
120        conc @ 3..=4 => conc - 1,
121        conc => conc - 2,
122    };
123    relay_log::info!("starting {thread_count} envelope processing workers");
124
125    let pool = crate::utils::ThreadPoolBuilder::new("processor", tokio::runtime::Handle::current())
126        .num_threads(thread_count)
127        .max_concurrency(config.pool_concurrency())
128        .thread_kind(ThreadKind::Worker)
129        .build()?;
130
131    Ok(pool)
132}
133
134#[cfg(feature = "processing")]
135fn create_store_pool(config: &ConfigSnapshot) -> Result<StoreServicePool> {
136    // Spawn a store worker for every 12 threads in the processor pool.
137    // This ratio was found empirically and may need adjustments in the future.
138    //
139    // Ideally in the future the store will be single threaded again, after we move
140    // all the heavy processing (de- and re-serialization) into the processor.
141    let thread_count = config.cpu_concurrency().div_ceil(8);
142    relay_log::info!("starting {thread_count} store workers");
143
144    let pool = crate::utils::ThreadPoolBuilder::new("store", tokio::runtime::Handle::current())
145        .num_threads(thread_count)
146        .max_concurrency(config.pool_concurrency())
147        .build()?;
148
149    Ok(pool)
150}
151
152#[derive(Debug)]
153struct StateInner {
154    config: Arc<Config>,
155    memory_checker: MemoryChecker,
156    registry: Registry,
157}
158
159/// Server state.
160#[derive(Clone, Debug)]
161pub struct ServiceState {
162    inner: Arc<StateInner>,
163}
164
165impl ServiceState {
166    /// Starts all services and returns addresses to all of them.
167    pub async fn start(
168        handle: &relay_system::Handle,
169        services: &dyn ServiceSpawn,
170        config: Arc<Config>,
171    ) -> Result<Self> {
172        let upstream_relay = services.start(UpstreamRelayService::new(config.clone()));
173        let current_config = config.current();
174
175        #[cfg(feature = "processing")]
176        let redis_clients = current_config
177            .redis()
178            .filter(|_| current_config.processing_enabled())
179            .map(create_redis_clients)
180            .transpose()
181            .context(ServiceError::Redis)?;
182
183        // If we have Redis configured, we want to initialize all the scripts by loading them in
184        // the scripts cache if not present. Our custom ConnectionLike implementation relies on this
185        // initialization to work properly since it assumes that scripts are loaded across all Redis
186        // instances.
187        #[cfg(feature = "processing")]
188        if let Some(redis_clients) = &redis_clients {
189            initialize_redis_scripts_for_client(redis_clients)
190                .await
191                .context(ServiceError::Redis)?;
192        }
193
194        // We create an instance of `MemoryStat` which can be supplied composed with any arbitrary
195        // configuration object down the line.
196        let memory_stat = MemoryStat::new(current_config.memory_stat_refresh_frequency_ms());
197
198        // Create an address for the `EnvelopeProcessor`, which can be injected into the
199        // other services.
200        let (processor, processor_rx) = match current_config.relay_mode() {
201            RelayMode::Proxy => channel(ProxyProcessorService::name()),
202            RelayMode::Managed => channel(EnvelopeProcessorService::name()),
203        };
204
205        let (aggregator, aggregator_rx) = channel(RouterService::name());
206
207        let outcome_aggregator = match current_config.emit_outcomes() {
208            EmitOutcomes::None => services.start(NullOutcomeProducerService::new()),
209            _ => match current_config.relay_mode() {
210                RelayMode::Proxy => services.start(ClientReportOutcomeProducerService::new(
211                    &current_config,
212                    processor.clone(),
213                )),
214                RelayMode::Managed => services.start(OutcomeProducerService::new(
215                    Arc::clone(&config),
216                    aggregator.clone(),
217                )),
218            },
219        };
220
221        let (global_config, global_config_rx) =
222            GlobalConfigService::new(config.clone(), upstream_relay.clone());
223        let global_config_handle = global_config.handle();
224        // The global config service must start before dependant services are
225        // started. Messages like subscription requests to the global config
226        // service fail if the service is not running.
227        let global_config = services.start(global_config);
228
229        let project_source = ProjectSource::start_in(
230            services,
231            Arc::clone(&config),
232            upstream_relay.clone(),
233            #[cfg(feature = "processing")]
234            redis_clients.clone(),
235        )
236        .await;
237        let project_cache_handle =
238            ProjectCacheService::new(Arc::clone(&config), project_source).start_in(services);
239
240        let cogs = CogsService::new(&current_config);
241        let cogs = Cogs::new(CogsServiceRecorder::new(
242            &current_config,
243            services.start(cogs),
244        ));
245
246        let metric_outcomes = MetricOutcomes::new(outcome_aggregator.clone());
247
248        #[cfg(feature = "processing")]
249        let store_pool = create_store_pool(&current_config)?;
250        // Taken before the service is started, which consumes it. `ConfigReloadService` pushes
251        // a reloaded Kafka config to it.
252        #[cfg(feature = "processing")]
253        let mut kafka_client = None;
254        #[cfg(feature = "processing")]
255        let store = current_config
256            .processing_enabled()
257            .then(|| {
258                StoreService::create(
259                    store_pool.clone(),
260                    config.clone(),
261                    global_config_handle.clone(),
262                    metric_outcomes.clone(),
263                )
264                .map(|s| {
265                    kafka_client = Some(s.kafka_client());
266                    services.start(s)
267                })
268            })
269            .transpose()?;
270
271        #[cfg(feature = "processing")]
272        let objectstore = ObjectstoreService::new(current_config.objectstore(), store.clone())?
273            .map(|s| {
274                let concurrent = ConcurrentService::new(s)
275                    .with_backlog_limit(current_config.objectstore().max_backlog)
276                    .with_concurrency_limit(current_config.objectstore().max_concurrent_requests);
277                services.start(concurrent)
278            });
279
280        let envelope_buffer = PartitionedEnvelopeBuffer::create(
281            current_config.spool_partitions(),
282            config.clone(),
283            memory_stat.clone(),
284            global_config_rx.clone(),
285            project_cache_handle.clone(),
286            processor.clone(),
287            outcome_aggregator.clone(),
288            services,
289        );
290
291        let (processor_pool, aggregator_handle, autoscaling) = match current_config.relay_mode() {
292            RelayMode::Proxy => {
293                services.start_with(
294                    ProxyProcessorService::new(
295                        config.clone(),
296                        project_cache_handle.clone(),
297                        ProxyAddrs {
298                            outcome_aggregator: outcome_aggregator.clone(),
299                            upstream_relay: upstream_relay.clone(),
300                        },
301                    ),
302                    processor_rx,
303                );
304                (None, None, None)
305            }
306            RelayMode::Managed => {
307                let processor_pool = create_processor_pool(&current_config)?;
308
309                let router = RouterService::new(
310                    handle.clone(),
311                    current_config.default_aggregator_config().clone(),
312                    current_config.secondary_aggregator_configs().clone(),
313                    Some(processor.clone().recipient()),
314                    project_cache_handle.clone(),
315                );
316                let router_handle = router.handle();
317                services.start_with(router, aggregator_rx);
318
319                services.start_with(
320                    EnvelopeProcessorService::new(
321                        processor_pool.clone(),
322                        config.clone(),
323                        global_config_handle.clone(),
324                        project_cache_handle.clone(),
325                        cogs.clone(),
326                        #[cfg(feature = "processing")]
327                        redis_clients.clone(),
328                        processor::Addrs {
329                            outcome_aggregator: outcome_aggregator.clone(),
330                            upstream_relay: upstream_relay.clone(),
331                            #[cfg(feature = "processing")]
332                            objectstore: objectstore.clone(),
333                            #[cfg(feature = "processing")]
334                            store_forwarder: store,
335                            aggregator: aggregator.clone(),
336                        },
337                        metric_outcomes.clone(),
338                    ),
339                    processor_rx,
340                );
341
342                let autoscaling = services.start(AutoscalingMetricService::new(
343                    memory_stat.clone(),
344                    envelope_buffer.clone(),
345                    handle.clone(),
346                    processor_pool.clone(),
347                ));
348
349                (Some(processor_pool), Some(router_handle), Some(autoscaling))
350            }
351        };
352
353        let health_check = services.start(HealthCheckService::new(
354            config.clone(),
355            MemoryChecker::new(memory_stat.clone(), config.clone()),
356            aggregator_handle,
357            upstream_relay.clone(),
358            envelope_buffer.clone(),
359        ));
360
361        services.start(RelayStats::new(
362            config.clone(),
363            handle.clone(),
364            upstream_relay.clone(),
365            #[cfg(feature = "processing")]
366            redis_clients.clone(),
367            processor_pool,
368            #[cfg(feature = "processing")]
369            store_pool,
370        ));
371
372        let relay_cache = services.start(RelayCacheService::new(
373            config.clone(),
374            upstream_relay.clone(),
375        ));
376
377        let upload = services.start(upload::create_service(
378            &config,
379            &upstream_relay,
380            #[cfg(feature = "processing")]
381            &objectstore,
382        ));
383
384        let _ = services.start(ConfigReloadService::new(
385            config.clone(),
386            #[cfg(feature = "processing")]
387            kafka_client,
388        ));
389
390        let registry = Registry {
391            cogs,
392            processor,
393            health_check,
394            outcome_aggregator,
395            relay_cache,
396            global_config,
397            project_cache_handle,
398            upstream_relay,
399            envelope_buffer,
400            autoscaling,
401            #[cfg(feature = "processing")]
402            objectstore,
403            upload,
404            global_config_handle,
405        };
406
407        let state = StateInner {
408            config: config.clone(),
409            memory_checker: MemoryChecker::new(memory_stat, config.clone()),
410            registry,
411        };
412
413        Ok(ServiceState {
414            inner: Arc::new(state),
415        })
416    }
417
418    /// Returns a snapshot of the Relay configuration.
419    pub fn config(&self) -> ConfigSnapshot {
420        self.inner.config.current()
421    }
422
423    /// Returns a reference to the [`Cogs`] tracking.
424    pub fn cogs(&self) -> &Cogs {
425        &self.inner.registry.cogs
426    }
427
428    /// Returns a reference to the [`MemoryChecker`] which is a [`Config`] aware wrapper on the
429    /// [`MemoryStat`] which gives utility methods to determine whether memory usage is above
430    /// thresholds set in the [`Config`].
431    pub fn memory_checker(&self) -> &MemoryChecker {
432        &self.inner.memory_checker
433    }
434
435    pub fn autoscaling(&self) -> Option<&Addr<AutoscalingMetrics>> {
436        self.inner.registry.autoscaling.as_ref()
437    }
438
439    /// Returns `true` if all envelope buffer partitions are empty, in memory and on disk.
440    pub fn envelope_buffer_is_empty(&self) -> bool {
441        self.inner.registry.envelope_buffer.is_empty()
442    }
443
444    /// Returns the V2 envelope buffer, if present.
445    pub fn envelope_buffer(&self, project_key_pair: ProjectKeyPair) -> &ObservableEnvelopeBuffer {
446        self.inner.registry.envelope_buffer.buffer(project_key_pair)
447    }
448
449    /// Returns a [`ProjectCacheHandle`].
450    pub fn project_cache_handle(&self) -> &ProjectCacheHandle {
451        &self.inner.registry.project_cache_handle
452    }
453
454    /// Returns the address of the [`RelayCache`] service.
455    pub fn relay_cache(&self) -> &Addr<RelayCache> {
456        &self.inner.registry.relay_cache
457    }
458
459    /// Returns the address of the [`HealthCheck`] service.
460    pub fn health_check(&self) -> &Addr<HealthCheck> {
461        &self.inner.registry.health_check
462    }
463
464    /// Returns the address of the [`UpstreamRelay`] service.
465    pub fn upstream_relay(&self) -> &Addr<UpstreamRelay> {
466        &self.inner.registry.upstream_relay
467    }
468
469    /// Returns the address of the [`EnvelopeProcessor`] service.
470    pub fn processor(&self) -> &Addr<EnvelopeProcessor> {
471        &self.inner.registry.processor
472    }
473
474    /// Returns the address of the [`GlobalConfigService`] service.
475    pub fn global_config(&self) -> &Addr<GlobalConfigManager> {
476        &self.inner.registry.global_config
477    }
478
479    /// Returns the address of the [`TrackOutcome`] service.
480    pub fn outcome_aggregator(&self) -> &Addr<TrackOutcome> {
481        &self.inner.registry.outcome_aggregator
482    }
483
484    #[cfg(feature = "processing")]
485    /// Returns the address of the [`Objectstore`] service.
486    pub fn objectstore(&self) -> Option<&Addr<Objectstore>> {
487        self.inner.registry.objectstore.as_ref()
488    }
489
490    /// Returns the address of the [`Upload`] service.
491    pub fn upload(&self) -> &Addr<Upload> {
492        &self.inner.registry.upload
493    }
494
495    pub fn global_config_handle(&self) -> &GlobalConfigHandle {
496        &self.inner.registry.global_config_handle
497    }
498}
499
500/// Creates Redis clients from the given `configs`.
501///
502/// If `configs` is [`Unified`](RedisConfigsRef::Unified), one client is created and then cloned
503/// for project configs, cardinality, and quotas, meaning that they really use the same client.
504///
505/// If it is [`Individual`](RedisConfigsRef::Individual), an actual separate client
506/// is created for each use case.
507#[cfg(feature = "processing")]
508pub fn create_redis_clients(configs: RedisConfigsRef<'_>) -> Result<RedisClients, RedisError> {
509    const PROJECT_CONFIG_REDIS_CLIENT: &str = "projectconfig";
510    const QUOTA_REDIS_CLIENT: &str = "quotas";
511    const UNIFIED_REDIS_CLIENT: &str = "unified";
512
513    match configs {
514        RedisConfigsRef::Unified(unified) => {
515            let client = create_async_redis_client(UNIFIED_REDIS_CLIENT, &unified)?;
516
517            Ok(RedisClients {
518                project_configs: client.clone(),
519                quotas: client,
520            })
521        }
522        RedisConfigsRef::Individual {
523            project_configs,
524            quotas,
525        } => {
526            let project_configs =
527                create_async_redis_client(PROJECT_CONFIG_REDIS_CLIENT, &project_configs)?;
528            let quotas = create_async_redis_client(QUOTA_REDIS_CLIENT, &quotas)?;
529
530            Ok(RedisClients {
531                project_configs,
532                quotas,
533            })
534        }
535    }
536}
537
538#[cfg(feature = "processing")]
539fn create_async_redis_client(
540    name: &'static str,
541    config: &RedisConfigRef<'_>,
542) -> Result<AsyncRedisClient, RedisError> {
543    match config {
544        RedisConfigRef::Cluster {
545            cluster_nodes,
546            options,
547        } => AsyncRedisClient::cluster(name, cluster_nodes.iter().map(|s| s.as_str()), options),
548        RedisConfigRef::Single { server, options } => {
549            AsyncRedisClient::single(name, server, options)
550        }
551    }
552}
553
554#[cfg(feature = "processing")]
555async fn initialize_redis_scripts_for_client(
556    redis_clients: &RedisClients,
557) -> Result<(), RedisError> {
558    let scripts = RedisScripts::all();
559
560    let RedisClients {
561        project_configs,
562        quotas,
563    } = redis_clients;
564
565    initialize_redis_scripts(project_configs, &scripts).await?;
566    initialize_redis_scripts(quotas, &scripts).await?;
567
568    Ok(())
569}
570
571#[cfg(feature = "processing")]
572async fn initialize_redis_scripts(
573    client: &AsyncRedisClient,
574    scripts: &[&Script],
575) -> Result<(), RedisError> {
576    let mut connection = client.get_connection().await?;
577
578    for script in scripts {
579        // We load on all instances without checking if the script is already in cache because of a
580        // limitation in the connection implementation.
581        script
582            .prepare_invoke()
583            .load_async(&mut connection)
584            .await
585            .map_err(RedisError::Redis)?;
586    }
587
588    Ok(())
589}
590
591impl FromRequestParts<Self> for ServiceState {
592    type Rejection = Infallible;
593
594    async fn from_request_parts(_: &mut Parts, state: &Self) -> Result<Self, Self::Rejection> {
595        Ok(state.clone())
596    }
597}