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