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#[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}
153
154#[derive(Clone, Debug)]
156pub struct ServiceState {
157 inner: Arc<StateInner>,
158}
159
160impl ServiceState {
161 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 #[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 let memory_stat = MemoryStat::new(current_config.memory_stat_refresh_frequency_ms());
192
193 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 ¤t_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 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(¤t_config);
236 let cogs = Cogs::new(CogsServiceRecorder::new(
237 ¤t_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(¤t_config)?;
245 #[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(¤t_config)?;
298 #[cfg(feature = "processing")]
300 let rate_limiter =
301 processor::create_rate_limiter(¤t_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 pub fn config(&self) -> ConfigSnapshot {
431 self.inner.config.current()
432 }
433
434 pub fn cogs(&self) -> &Cogs {
436 &self.inner.registry.cogs
437 }
438
439 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 pub fn envelope_buffer_is_empty(&self) -> bool {
452 self.inner.registry.envelope_buffer.is_empty()
453 }
454
455 pub fn envelope_buffer(&self, project_key_pair: ProjectKeyPair) -> &ObservableEnvelopeBuffer {
457 self.inner.registry.envelope_buffer.buffer(project_key_pair)
458 }
459
460 pub fn project_cache_handle(&self) -> &ProjectCacheHandle {
462 &self.inner.registry.project_cache_handle
463 }
464
465 pub fn relay_cache(&self) -> &Addr<RelayCache> {
467 &self.inner.registry.relay_cache
468 }
469
470 pub fn health_check(&self) -> &Addr<HealthCheck> {
472 &self.inner.registry.health_check
473 }
474
475 pub fn upstream_relay(&self) -> &Addr<UpstreamRelay> {
477 &self.inner.registry.upstream_relay
478 }
479
480 pub fn processor(&self) -> &Addr<EnvelopeProcessor> {
482 &self.inner.registry.processor
483 }
484
485 pub fn global_config(&self) -> &Addr<GlobalConfigManager> {
487 &self.inner.registry.global_config
488 }
489
490 pub fn outcome_aggregator(&self) -> &Addr<TrackOutcome> {
492 &self.inner.registry.outcome_aggregator
493 }
494
495 #[cfg(feature = "processing")]
496 pub fn objectstore(&self) -> Option<&Addr<Objectstore>> {
498 self.inner.registry.objectstore.as_ref()
499 }
500
501 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#[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, "as)?;
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 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}