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, 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<StoreServicePool> {
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 _asserter: AssertEmpty<TrackOutcome>,
153}
154
155#[derive(Clone, Debug)]
157pub struct ServiceState {
158 inner: Arc<StateInner>,
159}
160
161impl ServiceState {
162 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 #[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 let memory_stat = MemoryStat::new(current_config.memory_stat_refresh_frequency_ms());
193
194 let (envelope_processor, envelope_processor_rx) = match current_config.relay_mode() {
197 RelayMode::Proxy => channel(ProxyProcessorService::name()),
198 RelayMode::Managed => channel("envelope_processor"),
199 };
200
201 let (aggregator, aggregator_rx) = channel(RouterService::name());
202
203 let (outcome_metrics_router, outcome_metrics_router_rx) = channel("outcome_metrics_router");
205 let (dummy_outcomes, _asserter) = Addr::never(); 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 ¤t_config,
212 envelope_processor.clone(),
213 )),
214 RelayMode::Managed => services.start(OutcomeProducerService::new(
215 Arc::clone(&config),
216 outcome_metrics_router,
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 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(¤t_config);
241 let cogs = Cogs::new(CogsServiceRecorder::new(
242 ¤t_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(¤t_config)?;
250
251 #[cfg(feature = "processing")]
252 let mut maybe_kafka_client = None;
253
254 #[cfg(feature = "processing")]
255 let store = current_config
256 .processing_enabled()
257 .then(|| {
258 let kafka_client = create_kafka_client(¤t_config)?;
259 maybe_kafka_client = Some(kafka_client.clone());
260 StoreService::create(
261 store_pool.clone(),
262 kafka_client.clone(),
263 config.clone(),
264 global_config_handle.clone(),
265 metric_outcomes.clone(),
266 )
267 .map(|s| services.start(s))
268 })
269 .transpose()?;
270
271 #[cfg(feature = "processing")]
272 let objectstore = objectstore::create_service(current_config.objectstore(), store.clone())?
273 .map(|s| services.start(s));
274
275 let envelope_buffer = PartitionedEnvelopeBuffer::create(
276 current_config.spool_partitions(),
277 config.clone(),
278 memory_stat.clone(),
279 global_config_rx.clone(),
280 project_cache_handle.clone(),
281 envelope_processor.clone(),
282 outcome_aggregator.clone(),
283 services,
284 );
285
286 let (processor_pool, aggregator_handle, autoscaling) = match current_config.relay_mode() {
287 RelayMode::Proxy => {
288 services.start_with(
289 ProxyProcessorService::new(
290 config.clone(),
291 project_cache_handle.clone(),
292 ProxyAddrs {
293 outcome_aggregator: outcome_aggregator.clone(),
294 upstream_relay: upstream_relay.clone(),
295 },
296 ),
297 envelope_processor_rx,
298 );
299 (None, None, None)
300 }
301 RelayMode::Managed => {
302 let processor_pool = create_processor_pool(¤t_config)?;
303 #[cfg(feature = "processing")]
305 let rate_limiter =
306 processor::create_rate_limiter(¤t_config, redis_clients.clone());
307
308 let bucket_processor = services.start(processor::bucket::create_service(
309 processor_pool.clone(),
310 config.clone(),
311 global_config_handle.clone(),
312 project_cache_handle.clone(),
313 cogs.clone(),
314 #[cfg(feature = "processing")]
315 rate_limiter.clone(),
316 processor::bucket::Addrs {
317 outcome_aggregator: outcome_aggregator.clone(),
318 upstream_relay: upstream_relay.clone(),
319 #[cfg(feature = "processing")]
320 store_forwarder: store.clone(),
321 },
322 ));
323
324 let router = RouterService::new(
325 handle.clone(),
326 current_config.default_aggregator_config().clone(),
327 current_config.secondary_aggregator_configs().clone(),
328 Some(bucket_processor.recipient()),
329 project_cache_handle.clone(),
330 );
331 let router_handle = router.handle();
332 services.start_with(router, aggregator_rx);
333
334 {
359 #[cfg(feature = "processing")]
360 let outcome_store = {
361 if current_config.processing_enabled() {
362 let (addr, rx) = channel("outcome_store");
363 let service = StoreService::create(
364 store_pool.clone(),
365 maybe_kafka_client
366 .clone()
367 .expect("kafka client created by store"),
368 config.clone(),
369 global_config_handle.clone(),
370 MetricOutcomes::new(dummy_outcomes.clone()),
371 )?;
372 services.start_with(service, rx);
373 Some(addr)
374 } else {
375 None
376 }
377 };
378
379 let (outcome_bucket_processor, outcome_bucket_rx) =
380 channel("outcome_bucket_processor");
381 let outcome_bucket_service = processor::bucket::create_service(
382 processor_pool.clone(),
383 config.clone(),
384 global_config_handle.clone(),
385 project_cache_handle.clone(),
386 cogs.clone(),
387 #[cfg(feature = "processing")]
388 rate_limiter.clone(),
389 processor::bucket::Addrs {
390 outcome_aggregator: dummy_outcomes,
391 upstream_relay: upstream_relay.clone(),
392 #[cfg(feature = "processing")]
393 store_forwarder: outcome_store,
394 },
395 );
396 services.start_with(outcome_bucket_service, outcome_bucket_rx);
397
398 let outcome_metrics_router = RouterService::prefixed(
401 handle.clone(),
402 "outcomes_",
403 current_config.default_aggregator_config().clone(),
404 current_config.secondary_aggregator_configs().clone(),
405 Some(outcome_bucket_processor.recipient()),
406 project_cache_handle.clone(),
407 );
408 services.start_with(outcome_metrics_router, outcome_metrics_router_rx);
409 }
410
411 services.start_with(
412 processor::envelope::create_service(
413 processor_pool.clone(),
414 config.clone(),
415 global_config_handle.clone(),
416 project_cache_handle.clone(),
417 cogs.clone(),
418 #[cfg(feature = "processing")]
419 rate_limiter,
420 processor::envelope::Addrs {
421 outcome_aggregator: outcome_aggregator.clone(),
422 upstream_relay: upstream_relay.clone(),
423 #[cfg(feature = "processing")]
424 objectstore: objectstore.clone(),
425 #[cfg(feature = "processing")]
426 store_forwarder: store,
427 aggregator: aggregator.clone(),
428 },
429 metric_outcomes.clone(),
430 ),
431 envelope_processor_rx,
432 );
433
434 let autoscaling = services.start(AutoscalingMetricService::new(
435 memory_stat.clone(),
436 envelope_buffer.clone(),
437 handle.clone(),
438 processor_pool.clone(),
439 ));
440
441 (Some(processor_pool), Some(router_handle), Some(autoscaling))
442 }
443 };
444
445 let health_check = services.start(HealthCheckService::new(
446 config.clone(),
447 MemoryChecker::new(memory_stat.clone(), config.clone()),
448 aggregator_handle,
449 upstream_relay.clone(),
450 envelope_buffer.clone(),
451 ));
452
453 services.start(RelayStats::new(
454 config.clone(),
455 handle.clone(),
456 upstream_relay.clone(),
457 #[cfg(feature = "processing")]
458 redis_clients.clone(),
459 processor_pool,
460 #[cfg(feature = "processing")]
461 store_pool,
462 ));
463
464 let relay_cache = services.start(RelayCacheService::new(
465 config.clone(),
466 upstream_relay.clone(),
467 ));
468
469 let upload = services.start(upload::create_service(
470 &config,
471 &upstream_relay,
472 #[cfg(feature = "processing")]
473 &objectstore,
474 ));
475
476 let _ = services.start(ConfigReloadService::new(
477 config.clone(),
478 #[cfg(feature = "processing")]
479 maybe_kafka_client,
480 ));
481
482 let registry = Registry {
483 cogs,
484 processor: envelope_processor,
485 health_check,
486 outcome_aggregator,
487 relay_cache,
488 global_config,
489 project_cache_handle,
490 upstream_relay,
491 envelope_buffer,
492 autoscaling,
493 #[cfg(feature = "processing")]
494 objectstore,
495 upload,
496 global_config_handle,
497 };
498
499 let state = StateInner {
500 config: config.clone(),
501 memory_checker: MemoryChecker::new(memory_stat, config.clone()),
502 registry,
503 _asserter,
504 };
505
506 Ok(ServiceState {
507 inner: Arc::new(state),
508 })
509 }
510
511 pub fn config(&self) -> ConfigSnapshot {
513 self.inner.config.current()
514 }
515
516 pub fn cogs(&self) -> &Cogs {
518 &self.inner.registry.cogs
519 }
520
521 pub fn memory_checker(&self) -> &MemoryChecker {
525 &self.inner.memory_checker
526 }
527
528 pub fn autoscaling(&self) -> Option<&Addr<AutoscalingMetrics>> {
529 self.inner.registry.autoscaling.as_ref()
530 }
531
532 pub fn envelope_buffer_is_empty(&self) -> bool {
534 self.inner.registry.envelope_buffer.is_empty()
535 }
536
537 pub fn envelope_buffer(&self, project_key_pair: ProjectKeyPair) -> &ObservableEnvelopeBuffer {
539 self.inner.registry.envelope_buffer.buffer(project_key_pair)
540 }
541
542 pub fn project_cache_handle(&self) -> &ProjectCacheHandle {
544 &self.inner.registry.project_cache_handle
545 }
546
547 pub fn relay_cache(&self) -> &Addr<RelayCache> {
549 &self.inner.registry.relay_cache
550 }
551
552 pub fn health_check(&self) -> &Addr<HealthCheck> {
554 &self.inner.registry.health_check
555 }
556
557 pub fn upstream_relay(&self) -> &Addr<UpstreamRelay> {
559 &self.inner.registry.upstream_relay
560 }
561
562 pub fn processor(&self) -> &Addr<EnvelopeProcessor> {
564 &self.inner.registry.processor
565 }
566
567 pub fn global_config(&self) -> &Addr<GlobalConfigManager> {
569 &self.inner.registry.global_config
570 }
571
572 pub fn outcome_aggregator(&self) -> &Addr<TrackOutcome> {
574 &self.inner.registry.outcome_aggregator
575 }
576
577 #[cfg(feature = "processing")]
578 pub fn objectstore(&self) -> Option<&Addr<Objectstore>> {
580 self.inner.registry.objectstore.as_ref()
581 }
582
583 pub fn upload(&self) -> &Addr<Upload> {
585 &self.inner.registry.upload
586 }
587
588 pub fn global_config_handle(&self) -> &GlobalConfigHandle {
589 &self.inner.registry.global_config_handle
590 }
591}
592
593#[cfg(feature = "processing")]
601pub fn create_redis_clients(configs: RedisConfigsRef<'_>) -> Result<RedisClients, RedisError> {
602 const PROJECT_CONFIG_REDIS_CLIENT: &str = "projectconfig";
603 const QUOTA_REDIS_CLIENT: &str = "quotas";
604 const UNIFIED_REDIS_CLIENT: &str = "unified";
605
606 match configs {
607 RedisConfigsRef::Unified(unified) => {
608 let client = create_async_redis_client(UNIFIED_REDIS_CLIENT, &unified)?;
609
610 Ok(RedisClients {
611 project_configs: client.clone(),
612 quotas: client,
613 })
614 }
615 RedisConfigsRef::Individual {
616 project_configs,
617 quotas,
618 } => {
619 let project_configs =
620 create_async_redis_client(PROJECT_CONFIG_REDIS_CLIENT, &project_configs)?;
621 let quotas = create_async_redis_client(QUOTA_REDIS_CLIENT, "as)?;
622
623 Ok(RedisClients {
624 project_configs,
625 quotas,
626 })
627 }
628 }
629}
630
631#[cfg(feature = "processing")]
632fn create_async_redis_client(
633 name: &'static str,
634 config: &RedisConfigRef<'_>,
635) -> Result<AsyncRedisClient, RedisError> {
636 match config {
637 RedisConfigRef::Cluster {
638 cluster_nodes,
639 options,
640 } => AsyncRedisClient::cluster(name, cluster_nodes.iter().map(|s| s.as_str()), options),
641 RedisConfigRef::Single { server, options } => {
642 AsyncRedisClient::single(name, server, options)
643 }
644 }
645}
646
647#[cfg(feature = "processing")]
648async fn initialize_redis_scripts_for_client(
649 redis_clients: &RedisClients,
650) -> Result<(), RedisError> {
651 let scripts = RedisScripts::all();
652
653 let RedisClients {
654 project_configs,
655 quotas,
656 } = redis_clients;
657
658 initialize_redis_scripts(project_configs, &scripts).await?;
659 initialize_redis_scripts(quotas, &scripts).await?;
660
661 Ok(())
662}
663
664#[cfg(feature = "processing")]
665async fn initialize_redis_scripts(
666 client: &AsyncRedisClient,
667 scripts: &[&Script],
668) -> Result<(), RedisError> {
669 let mut connection = client.get_connection().await?;
670
671 for script in scripts {
672 script
675 .prepare_invoke()
676 .load_async(&mut connection)
677 .await
678 .map_err(RedisError::Redis)?;
679 }
680
681 Ok(())
682}
683
684impl FromRequestParts<Self> for ServiceState {
685 type Rejection = Infallible;
686
687 async fn from_request_parts(_: &mut Parts, state: &Self) -> Result<Self, Self::Rejection> {
688 Ok(state.clone())
689 }
690}