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