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