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#[derive(Debug, thiserror::Error)]
59pub enum ServiceError {
60 #[error("could not load the Geoip Db")]
62 GeoIp,
63
64 #[cfg(feature = "processing")]
66 #[error("could not initialize kafka producer: {0}")]
67 Kafka(String),
68
69 #[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 cogs: Cogs,
78 pub health_check: Addr<HealthCheck>,
79 pub outcome_aggregator: Addr<TrackOutcome>,
80 pub processor: Addr<EnvelopeProcessor>,
81 pub relay_cache: Addr<RelayCache>,
82 pub global_config: Addr<GlobalConfigManager>,
83 pub upstream_relay: Addr<UpstreamRelay>,
84 pub envelope_buffer: Arc<PartitionedEnvelopeBuffer>,
85 pub project_cache_handle: ProjectCacheHandle,
86 pub autoscaling: Option<Addr<AutoscalingMetrics>>,
87 #[cfg(feature = "processing")]
88 pub objectstore: Option<Addr<Objectstore>>,
89 pub upload: Addr<Upload>,
90 pub global_config_handle: GlobalConfigHandle,
91}
92
93pub fn create_runtime(name: &'static str, threads: usize) -> relay_system::Runtime {
95 relay_system::Runtime::builder(name)
96 .worker_threads(threads)
97 .max_blocking_threads(150)
108 .thread_keep_alive(Duration::from_secs(1))
112 .build()
113}
114
115fn create_processor_pool(config: &ConfigSnapshot) -> Result<EnvelopeProcessorServicePool> {
116 let thread_count = match config.cpu_concurrency() {
119 conc @ 0..=2 => conc.max(1),
120 conc @ 3..=4 => conc - 1,
121 conc => conc - 2,
122 };
123 relay_log::info!("starting {thread_count} envelope processing workers");
124
125 let pool = crate::utils::ThreadPoolBuilder::new("processor", tokio::runtime::Handle::current())
126 .num_threads(thread_count)
127 .max_concurrency(config.pool_concurrency())
128 .thread_kind(ThreadKind::Worker)
129 .build()?;
130
131 Ok(pool)
132}
133
134#[cfg(feature = "processing")]
135fn create_store_pool(config: &ConfigSnapshot) -> Result<StoreServicePool> {
136 let thread_count = config.cpu_concurrency().div_ceil(8);
142 relay_log::info!("starting {thread_count} store workers");
143
144 let pool = crate::utils::ThreadPoolBuilder::new("store", tokio::runtime::Handle::current())
145 .num_threads(thread_count)
146 .max_concurrency(config.pool_concurrency())
147 .build()?;
148
149 Ok(pool)
150}
151
152#[derive(Debug)]
153struct StateInner {
154 config: Arc<Config>,
155 memory_checker: MemoryChecker,
156 registry: Registry,
157}
158
159#[derive(Clone, Debug)]
161pub struct ServiceState {
162 inner: Arc<StateInner>,
163}
164
165impl ServiceState {
166 pub async fn start(
168 handle: &relay_system::Handle,
169 services: &dyn ServiceSpawn,
170 config: Arc<Config>,
171 ) -> Result<Self> {
172 let upstream_relay = services.start(UpstreamRelayService::new(config.clone()));
173 let current_config = config.current();
174
175 #[cfg(feature = "processing")]
176 let redis_clients = current_config
177 .redis()
178 .filter(|_| current_config.processing_enabled())
179 .map(create_redis_clients)
180 .transpose()
181 .context(ServiceError::Redis)?;
182
183 #[cfg(feature = "processing")]
188 if let Some(redis_clients) = &redis_clients {
189 initialize_redis_scripts_for_client(redis_clients)
190 .await
191 .context(ServiceError::Redis)?;
192 }
193
194 let memory_stat = MemoryStat::new(current_config.memory_stat_refresh_frequency_ms());
197
198 let (processor, processor_rx) = match current_config.relay_mode() {
201 RelayMode::Proxy => channel(ProxyProcessorService::name()),
202 RelayMode::Managed => channel(EnvelopeProcessorService::name()),
203 };
204
205 let (aggregator, aggregator_rx) = channel(RouterService::name());
206
207 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 processor.clone(),
213 )),
214 RelayMode::Managed => services.start(OutcomeProducerService::new(
215 Arc::clone(&config),
216 aggregator.clone(),
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 #[cfg(feature = "processing")]
253 let mut kafka_client = None;
254 #[cfg(feature = "processing")]
255 let store = current_config
256 .processing_enabled()
257 .then(|| {
258 StoreService::create(
259 store_pool.clone(),
260 config.clone(),
261 global_config_handle.clone(),
262 metric_outcomes.clone(),
263 )
264 .map(|s| {
265 kafka_client = Some(s.kafka_client());
266 services.start(s)
267 })
268 })
269 .transpose()?;
270
271 #[cfg(feature = "processing")]
272 let objectstore = ObjectstoreService::new(current_config.objectstore(), store.clone())?
273 .map(|s| {
274 let concurrent = ConcurrentService::new(s)
275 .with_backlog_limit(current_config.objectstore().max_backlog)
276 .with_concurrency_limit(current_config.objectstore().max_concurrent_requests);
277 services.start(concurrent)
278 });
279
280 let envelope_buffer = PartitionedEnvelopeBuffer::create(
281 current_config.spool_partitions(),
282 config.clone(),
283 memory_stat.clone(),
284 global_config_rx.clone(),
285 project_cache_handle.clone(),
286 processor.clone(),
287 outcome_aggregator.clone(),
288 services,
289 );
290
291 let (processor_pool, aggregator_handle, autoscaling) = match current_config.relay_mode() {
292 RelayMode::Proxy => {
293 services.start_with(
294 ProxyProcessorService::new(
295 config.clone(),
296 project_cache_handle.clone(),
297 ProxyAddrs {
298 outcome_aggregator: outcome_aggregator.clone(),
299 upstream_relay: upstream_relay.clone(),
300 },
301 ),
302 processor_rx,
303 );
304 (None, None, None)
305 }
306 RelayMode::Managed => {
307 let processor_pool = create_processor_pool(¤t_config)?;
308
309 let router = RouterService::new(
310 handle.clone(),
311 current_config.default_aggregator_config().clone(),
312 current_config.secondary_aggregator_configs().clone(),
313 Some(processor.clone().recipient()),
314 project_cache_handle.clone(),
315 );
316 let router_handle = router.handle();
317 services.start_with(router, aggregator_rx);
318
319 services.start_with(
320 EnvelopeProcessorService::new(
321 processor_pool.clone(),
322 config.clone(),
323 global_config_handle.clone(),
324 project_cache_handle.clone(),
325 cogs.clone(),
326 #[cfg(feature = "processing")]
327 redis_clients.clone(),
328 processor::Addrs {
329 outcome_aggregator: outcome_aggregator.clone(),
330 upstream_relay: upstream_relay.clone(),
331 #[cfg(feature = "processing")]
332 objectstore: objectstore.clone(),
333 #[cfg(feature = "processing")]
334 store_forwarder: store,
335 aggregator: aggregator.clone(),
336 },
337 metric_outcomes.clone(),
338 ),
339 processor_rx,
340 );
341
342 let autoscaling = services.start(AutoscalingMetricService::new(
343 memory_stat.clone(),
344 envelope_buffer.clone(),
345 handle.clone(),
346 processor_pool.clone(),
347 ));
348
349 (Some(processor_pool), Some(router_handle), Some(autoscaling))
350 }
351 };
352
353 let health_check = services.start(HealthCheckService::new(
354 config.clone(),
355 MemoryChecker::new(memory_stat.clone(), config.clone()),
356 aggregator_handle,
357 upstream_relay.clone(),
358 envelope_buffer.clone(),
359 ));
360
361 services.start(RelayStats::new(
362 config.clone(),
363 handle.clone(),
364 upstream_relay.clone(),
365 #[cfg(feature = "processing")]
366 redis_clients.clone(),
367 processor_pool,
368 #[cfg(feature = "processing")]
369 store_pool,
370 ));
371
372 let relay_cache = services.start(RelayCacheService::new(
373 config.clone(),
374 upstream_relay.clone(),
375 ));
376
377 let upload = services.start(upload::create_service(
378 &config,
379 &upstream_relay,
380 #[cfg(feature = "processing")]
381 &objectstore,
382 ));
383
384 let _ = services.start(ConfigReloadService::new(
385 config.clone(),
386 #[cfg(feature = "processing")]
387 kafka_client,
388 ));
389
390 let registry = Registry {
391 cogs,
392 processor,
393 health_check,
394 outcome_aggregator,
395 relay_cache,
396 global_config,
397 project_cache_handle,
398 upstream_relay,
399 envelope_buffer,
400 autoscaling,
401 #[cfg(feature = "processing")]
402 objectstore,
403 upload,
404 global_config_handle,
405 };
406
407 let state = StateInner {
408 config: config.clone(),
409 memory_checker: MemoryChecker::new(memory_stat, config.clone()),
410 registry,
411 };
412
413 Ok(ServiceState {
414 inner: Arc::new(state),
415 })
416 }
417
418 pub fn config(&self) -> ConfigSnapshot {
420 self.inner.config.current()
421 }
422
423 pub fn cogs(&self) -> &Cogs {
425 &self.inner.registry.cogs
426 }
427
428 pub fn memory_checker(&self) -> &MemoryChecker {
432 &self.inner.memory_checker
433 }
434
435 pub fn autoscaling(&self) -> Option<&Addr<AutoscalingMetrics>> {
436 self.inner.registry.autoscaling.as_ref()
437 }
438
439 pub fn envelope_buffer_is_empty(&self) -> bool {
441 self.inner.registry.envelope_buffer.is_empty()
442 }
443
444 pub fn envelope_buffer(&self, project_key_pair: ProjectKeyPair) -> &ObservableEnvelopeBuffer {
446 self.inner.registry.envelope_buffer.buffer(project_key_pair)
447 }
448
449 pub fn project_cache_handle(&self) -> &ProjectCacheHandle {
451 &self.inner.registry.project_cache_handle
452 }
453
454 pub fn relay_cache(&self) -> &Addr<RelayCache> {
456 &self.inner.registry.relay_cache
457 }
458
459 pub fn health_check(&self) -> &Addr<HealthCheck> {
461 &self.inner.registry.health_check
462 }
463
464 pub fn upstream_relay(&self) -> &Addr<UpstreamRelay> {
466 &self.inner.registry.upstream_relay
467 }
468
469 pub fn processor(&self) -> &Addr<EnvelopeProcessor> {
471 &self.inner.registry.processor
472 }
473
474 pub fn global_config(&self) -> &Addr<GlobalConfigManager> {
476 &self.inner.registry.global_config
477 }
478
479 pub fn outcome_aggregator(&self) -> &Addr<TrackOutcome> {
481 &self.inner.registry.outcome_aggregator
482 }
483
484 #[cfg(feature = "processing")]
485 pub fn objectstore(&self) -> Option<&Addr<Objectstore>> {
487 self.inner.registry.objectstore.as_ref()
488 }
489
490 pub fn upload(&self) -> &Addr<Upload> {
492 &self.inner.registry.upload
493 }
494
495 pub fn global_config_handle(&self) -> &GlobalConfigHandle {
496 &self.inner.registry.global_config_handle
497 }
498}
499
500#[cfg(feature = "processing")]
508pub fn create_redis_clients(configs: RedisConfigsRef<'_>) -> Result<RedisClients, RedisError> {
509 const PROJECT_CONFIG_REDIS_CLIENT: &str = "projectconfig";
510 const QUOTA_REDIS_CLIENT: &str = "quotas";
511 const UNIFIED_REDIS_CLIENT: &str = "unified";
512
513 match configs {
514 RedisConfigsRef::Unified(unified) => {
515 let client = create_async_redis_client(UNIFIED_REDIS_CLIENT, &unified)?;
516
517 Ok(RedisClients {
518 project_configs: client.clone(),
519 quotas: client,
520 })
521 }
522 RedisConfigsRef::Individual {
523 project_configs,
524 quotas,
525 } => {
526 let project_configs =
527 create_async_redis_client(PROJECT_CONFIG_REDIS_CLIENT, &project_configs)?;
528 let quotas = create_async_redis_client(QUOTA_REDIS_CLIENT, "as)?;
529
530 Ok(RedisClients {
531 project_configs,
532 quotas,
533 })
534 }
535 }
536}
537
538#[cfg(feature = "processing")]
539fn create_async_redis_client(
540 name: &'static str,
541 config: &RedisConfigRef<'_>,
542) -> Result<AsyncRedisClient, RedisError> {
543 match config {
544 RedisConfigRef::Cluster {
545 cluster_nodes,
546 options,
547 } => AsyncRedisClient::cluster(name, cluster_nodes.iter().map(|s| s.as_str()), options),
548 RedisConfigRef::Single { server, options } => {
549 AsyncRedisClient::single(name, server, options)
550 }
551 }
552}
553
554#[cfg(feature = "processing")]
555async fn initialize_redis_scripts_for_client(
556 redis_clients: &RedisClients,
557) -> Result<(), RedisError> {
558 let scripts = RedisScripts::all();
559
560 let RedisClients {
561 project_configs,
562 quotas,
563 } = redis_clients;
564
565 initialize_redis_scripts(project_configs, &scripts).await?;
566 initialize_redis_scripts(quotas, &scripts).await?;
567
568 Ok(())
569}
570
571#[cfg(feature = "processing")]
572async fn initialize_redis_scripts(
573 client: &AsyncRedisClient,
574 scripts: &[&Script],
575) -> Result<(), RedisError> {
576 let mut connection = client.get_connection().await?;
577
578 for script in scripts {
579 script
582 .prepare_invoke()
583 .load_async(&mut connection)
584 .await
585 .map_err(RedisError::Redis)?;
586 }
587
588 Ok(())
589}
590
591impl FromRequestParts<Self> for ServiceState {
592 type Rejection = Infallible;
593
594 async fn from_request_parts(_: &mut Parts, state: &Self) -> Result<Self, Self::Rejection> {
595 Ok(state.clone())
596 }
597}