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::{self, create_kafka_client};
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, AssertEmpty, Service, ServiceSpawn, ServiceSpawnExt as _, channel};
50use relay_threading::ServicePool;
51
52#[derive(Debug, thiserror::Error)]
54pub enum ServiceError {
55 #[error("could not load the Geoip Db")]
57 GeoIp,
58
59 #[cfg(feature = "processing")]
61 #[error("could not initialize kafka producer: {0}")]
62 Kafka(String),
63
64 #[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
88pub fn create_runtime(name: &'static str, threads: usize) -> relay_system::Runtime {
90 relay_system::Runtime::builder(name)
91 .worker_threads(threads)
92 .max_blocking_threads(150)
103 .thread_keep_alive(Duration::from_secs(1))
107 .build()
108}
109
110fn create_processor_pool(config: &ConfigSnapshot) -> Result<ServicePool> {
111 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<ServicePool> {
131 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 _daemons: Vec<Addr<()>>, _asserter: AssertEmpty<TrackOutcome>,
154}
155
156#[derive(Clone, Debug)]
158pub struct ServiceState {
159 inner: Arc<StateInner>,
160}
161
162impl ServiceState {
163 pub async fn start(
165 handle: &relay_system::Handle,
166 services: &dyn ServiceSpawn,
167 config: Arc<Config>,
168 ) -> Result<Self> {
169 let upstream_relay = services.start(UpstreamRelayService::new(config.clone()));
170 let current_config = config.current();
171
172 #[cfg(feature = "processing")]
173 let redis_clients = current_config
174 .redis()
175 .filter(|_| current_config.processing_enabled())
176 .map(create_redis_clients)
177 .transpose()
178 .context(ServiceError::Redis)?;
179
180 #[cfg(feature = "processing")]
185 if let Some(redis_clients) = &redis_clients {
186 initialize_redis_scripts_for_client(redis_clients)
187 .await
188 .context(ServiceError::Redis)?;
189 }
190
191 let memory_stat = MemoryStat::new(current_config.memory_stat_refresh_frequency_ms());
194
195 let (envelope_processor, envelope_processor_rx) = match current_config.relay_mode() {
198 RelayMode::Proxy => channel(ProxyProcessorService::name()),
199 RelayMode::Managed => channel("envelope_processor"),
200 };
201
202 let (aggregator, aggregator_rx) = channel(RouterService::name());
203
204 let (outcome_metrics_router, outcome_metrics_router_rx) = channel("outcome_metrics_router");
206 let (dummy_outcomes, _asserter) = Addr::never(); let outcome_aggregator = match current_config.emit_outcomes() {
209 EmitOutcomes::None => services.start(NullOutcomeProducerService::new()),
210 _ => match current_config.relay_mode() {
211 RelayMode::Proxy => services.start(ClientReportOutcomeProducerService::new(
212 ¤t_config,
213 envelope_processor.clone(),
214 )),
215 RelayMode::Managed => services.start(OutcomeProducerService::new(
216 Arc::clone(&config),
217 outcome_metrics_router,
218 )),
219 },
220 };
221
222 let (global_config, global_config_rx) =
223 GlobalConfigService::new(config.clone(), upstream_relay.clone());
224 let global_config_handle = global_config.handle();
225 let global_config = services.start(global_config);
229
230 let project_source = ProjectSource::start_in(
231 services,
232 Arc::clone(&config),
233 upstream_relay.clone(),
234 #[cfg(feature = "processing")]
235 redis_clients.clone(),
236 )
237 .await;
238 let project_cache_handle =
239 ProjectCacheService::new(Arc::clone(&config), project_source).start_in(services);
240
241 let cogs = CogsService::new(¤t_config);
242 let cogs = Cogs::new(CogsServiceRecorder::new(
243 ¤t_config,
244 services.start(cogs),
245 ));
246
247 let metric_outcomes = MetricOutcomes::new(outcome_aggregator.clone());
248
249 #[cfg(feature = "processing")]
250 let store_pool = create_store_pool(¤t_config)?;
251
252 #[cfg(feature = "processing")]
253 let mut maybe_kafka_client = None;
254
255 #[cfg(feature = "processing")]
256 let store = current_config
257 .processing_enabled()
258 .then(|| {
259 let kafka_client = create_kafka_client(¤t_config)?;
260 maybe_kafka_client = Some(kafka_client.clone());
261 let service = store::create_service(
262 store_pool.clone(),
263 kafka_client.clone(),
264 config.clone(),
265 global_config_handle.clone(),
266 metric_outcomes.clone(),
267 );
268 Ok::<_, anyhow::Error>(services.start(service))
269 })
270 .transpose()?;
271
272 #[cfg(feature = "processing")]
273 let objectstore = objectstore::create_service(current_config.objectstore(), store.clone())?
274 .map(|s| services.start(s));
275
276 let envelope_buffer = PartitionedEnvelopeBuffer::create(
277 current_config.spool_partitions(),
278 config.clone(),
279 memory_stat.clone(),
280 global_config_rx.clone(),
281 project_cache_handle.clone(),
282 envelope_processor.clone(),
283 outcome_aggregator.clone(),
284 services,
285 );
286
287 let (processor_pool, aggregator_handle, autoscaling) = match current_config.relay_mode() {
288 RelayMode::Proxy => {
289 services.start_with(
290 ProxyProcessorService::new(
291 config.clone(),
292 project_cache_handle.clone(),
293 ProxyAddrs {
294 outcome_aggregator: outcome_aggregator.clone(),
295 upstream_relay: upstream_relay.clone(),
296 },
297 ),
298 envelope_processor_rx,
299 );
300 (None, None, None)
301 }
302 RelayMode::Managed => {
303 let processor_pool = create_processor_pool(¤t_config)?;
304 #[cfg(feature = "processing")]
306 let rate_limiter =
307 processor::create_rate_limiter(¤t_config, redis_clients.clone());
308
309 let bucket_processor = services.start(processor::bucket::create_service(
310 processor_pool.clone(),
311 config.clone(),
312 global_config_handle.clone(),
313 project_cache_handle.clone(),
314 cogs.clone(),
315 #[cfg(feature = "processing")]
316 rate_limiter.clone(),
317 processor::bucket::Addrs {
318 outcome_aggregator: outcome_aggregator.clone(),
319 upstream_relay: upstream_relay.clone(),
320 #[cfg(feature = "processing")]
321 store_forwarder: store.clone(),
322 },
323 ));
324
325 let router = RouterService::new(
326 handle.clone(),
327 current_config.default_aggregator_config().clone(),
328 current_config.secondary_aggregator_configs().clone(),
329 Some(bucket_processor.recipient()),
330 project_cache_handle.clone(),
331 );
332 let router_handle = router.handle();
333 services.start_with(router, aggregator_rx);
334
335 {
360 #[cfg(feature = "processing")]
361 let outcome_store = {
362 if current_config.processing_enabled() {
363 let (addr, rx) = channel("outcome_store");
364 let service = store::create_service(
365 store_pool.clone(),
366 maybe_kafka_client
367 .clone()
368 .expect("kafka client created by store"),
369 config.clone(),
370 global_config_handle.clone(),
371 MetricOutcomes::new(dummy_outcomes.clone()),
372 );
373 services.start_with(service, rx);
374 Some(addr)
375 } else {
376 None
377 }
378 };
379
380 let (outcome_bucket_processor, outcome_bucket_rx) =
381 channel("outcome_bucket_processor");
382 let outcome_bucket_service = processor::bucket::create_service(
383 processor_pool.clone(),
384 config.clone(),
385 global_config_handle.clone(),
386 project_cache_handle.clone(),
387 cogs.clone(),
388 #[cfg(feature = "processing")]
389 rate_limiter.clone(),
390 processor::bucket::Addrs {
391 outcome_aggregator: dummy_outcomes,
392 upstream_relay: upstream_relay.clone(),
393 #[cfg(feature = "processing")]
394 store_forwarder: outcome_store,
395 },
396 );
397 services.start_with(outcome_bucket_service, outcome_bucket_rx);
398
399 let outcome_metrics_router = RouterService::prefixed(
402 handle.clone(),
403 "outcomes_",
404 current_config.default_aggregator_config().clone(),
405 current_config.secondary_aggregator_configs().clone(),
406 Some(outcome_bucket_processor.recipient()),
407 project_cache_handle.clone(),
408 );
409 services.start_with(outcome_metrics_router, outcome_metrics_router_rx);
410 }
411
412 services.start_with(
413 processor::envelope::create_service(
414 processor_pool.clone(),
415 config.clone(),
416 global_config_handle.clone(),
417 project_cache_handle.clone(),
418 cogs.clone(),
419 #[cfg(feature = "processing")]
420 rate_limiter,
421 processor::envelope::Addrs {
422 outcome_aggregator: outcome_aggregator.clone(),
423 upstream_relay: upstream_relay.clone(),
424 #[cfg(feature = "processing")]
425 objectstore: objectstore.clone(),
426 #[cfg(feature = "processing")]
427 store_forwarder: store,
428 aggregator: aggregator.clone(),
429 },
430 metric_outcomes.clone(),
431 ),
432 envelope_processor_rx,
433 );
434
435 let autoscaling = services.start(AutoscalingMetricService::new(
436 memory_stat.clone(),
437 envelope_buffer.clone(),
438 handle.clone(),
439 processor_pool.clone(),
440 ));
441
442 (Some(processor_pool), Some(router_handle), Some(autoscaling))
443 }
444 };
445
446 let health_check = services.start(HealthCheckService::new(
447 config.clone(),
448 MemoryChecker::new(memory_stat.clone(), config.clone()),
449 aggregator_handle,
450 upstream_relay.clone(),
451 envelope_buffer.clone(),
452 ));
453
454 let stats = services.start(RelayStats::new(
455 config.clone(),
456 handle.clone(),
457 upstream_relay.clone(),
458 #[cfg(feature = "processing")]
459 redis_clients.clone(),
460 processor_pool,
461 #[cfg(feature = "processing")]
462 store_pool,
463 ));
464
465 let relay_cache = services.start(RelayCacheService::new(
466 config.clone(),
467 upstream_relay.clone(),
468 ));
469
470 let upload = services.start(upload::create_service(
471 &config,
472 &upstream_relay,
473 #[cfg(feature = "processing")]
474 &objectstore,
475 ));
476
477 let config_reload = services.start(ConfigReloadService::new(
478 config.clone(),
479 #[cfg(feature = "processing")]
480 maybe_kafka_client,
481 ));
482
483 let registry = Registry {
484 cogs,
485 processor: envelope_processor,
486 health_check,
487 outcome_aggregator,
488 relay_cache,
489 global_config,
490 project_cache_handle,
491 upstream_relay,
492 envelope_buffer,
493 autoscaling,
494 #[cfg(feature = "processing")]
495 objectstore,
496 upload,
497 global_config_handle,
498 };
499
500 let state = StateInner {
501 config: config.clone(),
502 memory_checker: MemoryChecker::new(memory_stat, config.clone()),
503 registry,
504 _daemons: vec![stats, config_reload],
505 _asserter,
506 };
507
508 Ok(ServiceState {
509 inner: Arc::new(state),
510 })
511 }
512
513 pub fn config(&self) -> ConfigSnapshot {
515 self.inner.config.current()
516 }
517
518 pub fn cogs(&self) -> &Cogs {
520 &self.inner.registry.cogs
521 }
522
523 pub fn memory_checker(&self) -> &MemoryChecker {
527 &self.inner.memory_checker
528 }
529
530 pub fn autoscaling(&self) -> Option<&Addr<AutoscalingMetrics>> {
531 self.inner.registry.autoscaling.as_ref()
532 }
533
534 pub fn envelope_buffer_is_empty(&self) -> bool {
536 self.inner.registry.envelope_buffer.is_empty()
537 }
538
539 pub fn envelope_buffer(&self, project_key_pair: ProjectKeyPair) -> &ObservableEnvelopeBuffer {
541 self.inner.registry.envelope_buffer.buffer(project_key_pair)
542 }
543
544 pub fn project_cache_handle(&self) -> &ProjectCacheHandle {
546 &self.inner.registry.project_cache_handle
547 }
548
549 pub fn relay_cache(&self) -> &Addr<RelayCache> {
551 &self.inner.registry.relay_cache
552 }
553
554 pub fn health_check(&self) -> &Addr<HealthCheck> {
556 &self.inner.registry.health_check
557 }
558
559 pub fn upstream_relay(&self) -> &Addr<UpstreamRelay> {
561 &self.inner.registry.upstream_relay
562 }
563
564 pub fn processor(&self) -> &Addr<EnvelopeProcessor> {
566 &self.inner.registry.processor
567 }
568
569 pub fn global_config(&self) -> &Addr<GlobalConfigManager> {
571 &self.inner.registry.global_config
572 }
573
574 pub fn outcome_aggregator(&self) -> &Addr<TrackOutcome> {
576 &self.inner.registry.outcome_aggregator
577 }
578
579 #[cfg(feature = "processing")]
580 pub fn objectstore(&self) -> Option<&Addr<Objectstore>> {
582 self.inner.registry.objectstore.as_ref()
583 }
584
585 pub fn upload(&self) -> &Addr<Upload> {
587 &self.inner.registry.upload
588 }
589
590 pub fn global_config_handle(&self) -> &GlobalConfigHandle {
591 &self.inner.registry.global_config_handle
592 }
593}
594
595#[cfg(feature = "processing")]
603pub fn create_redis_clients(configs: RedisConfigsRef<'_>) -> Result<RedisClients, RedisError> {
604 const PROJECT_CONFIG_REDIS_CLIENT: &str = "projectconfig";
605 const QUOTA_REDIS_CLIENT: &str = "quotas";
606 const UNIFIED_REDIS_CLIENT: &str = "unified";
607
608 match configs {
609 RedisConfigsRef::Unified(unified) => {
610 let client = create_async_redis_client(UNIFIED_REDIS_CLIENT, &unified)?;
611
612 Ok(RedisClients {
613 project_configs: client.clone(),
614 quotas: client,
615 })
616 }
617 RedisConfigsRef::Individual {
618 project_configs,
619 quotas,
620 } => {
621 let project_configs =
622 create_async_redis_client(PROJECT_CONFIG_REDIS_CLIENT, &project_configs)?;
623 let quotas = create_async_redis_client(QUOTA_REDIS_CLIENT, "as)?;
624
625 Ok(RedisClients {
626 project_configs,
627 quotas,
628 })
629 }
630 }
631}
632
633#[cfg(feature = "processing")]
634fn create_async_redis_client(
635 name: &'static str,
636 config: &RedisConfigRef<'_>,
637) -> Result<AsyncRedisClient, RedisError> {
638 match config {
639 RedisConfigRef::Cluster {
640 cluster_nodes,
641 options,
642 } => AsyncRedisClient::cluster(name, cluster_nodes.iter().map(|s| s.as_str()), options),
643 RedisConfigRef::Single { server, options } => {
644 AsyncRedisClient::single(name, server, options)
645 }
646 }
647}
648
649#[cfg(feature = "processing")]
650async fn initialize_redis_scripts_for_client(
651 redis_clients: &RedisClients,
652) -> Result<(), RedisError> {
653 let scripts = RedisScripts::all();
654
655 let RedisClients {
656 project_configs,
657 quotas,
658 } = redis_clients;
659
660 initialize_redis_scripts(project_configs, &scripts).await?;
661 initialize_redis_scripts(quotas, &scripts).await?;
662
663 Ok(())
664}
665
666#[cfg(feature = "processing")]
667async fn initialize_redis_scripts(
668 client: &AsyncRedisClient,
669 scripts: &[&Script],
670) -> Result<(), RedisError> {
671 let mut connection = client.get_connection().await?;
672
673 for script in scripts {
674 script
677 .prepare_invoke()
678 .load_async(&mut connection)
679 .await
680 .map_err(RedisError::Redis)?;
681 }
682
683 Ok(())
684}
685
686impl FromRequestParts<Self> for ServiceState {
687 type Rejection = Infallible;
688
689 async fn from_request_parts(_: &mut Parts, state: &Self) -> Result<Self, Self::Rejection> {
690 Ok(state.clone())
691 }
692}