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 health_check: Addr<HealthCheck>,
78 pub outcome_aggregator: Addr<TrackOutcome>,
79 pub processor: Addr<EnvelopeProcessor>,
80 pub relay_cache: Addr<RelayCache>,
81 pub global_config: Addr<GlobalConfigManager>,
82 pub upstream_relay: Addr<UpstreamRelay>,
83 pub envelope_buffer: Arc<PartitionedEnvelopeBuffer>,
84 pub project_cache_handle: ProjectCacheHandle,
85 pub autoscaling: Option<Addr<AutoscalingMetrics>>,
86 #[cfg(feature = "processing")]
87 pub objectstore: Option<Addr<Objectstore>>,
88 pub upload: Addr<Upload>,
89 pub global_config_handle: GlobalConfigHandle,
90}
91
92pub fn create_runtime(name: &'static str, threads: usize) -> relay_system::Runtime {
94 relay_system::Runtime::builder(name)
95 .worker_threads(threads)
96 .max_blocking_threads(150)
107 .thread_keep_alive(Duration::from_secs(1))
111 .build()
112}
113
114fn create_processor_pool(config: &ConfigSnapshot) -> Result<EnvelopeProcessorServicePool> {
115 let thread_count = match config.cpu_concurrency() {
118 conc @ 0..=2 => conc.max(1),
119 conc @ 3..=4 => conc - 1,
120 conc => conc - 2,
121 };
122 relay_log::info!("starting {thread_count} envelope processing workers");
123
124 let pool = crate::utils::ThreadPoolBuilder::new("processor", tokio::runtime::Handle::current())
125 .num_threads(thread_count)
126 .max_concurrency(config.pool_concurrency())
127 .thread_kind(ThreadKind::Worker)
128 .build()?;
129
130 Ok(pool)
131}
132
133#[cfg(feature = "processing")]
134fn create_store_pool(config: &ConfigSnapshot) -> Result<StoreServicePool> {
135 let thread_count = config.cpu_concurrency().div_ceil(8);
141 relay_log::info!("starting {thread_count} store workers");
142
143 let pool = crate::utils::ThreadPoolBuilder::new("store", tokio::runtime::Handle::current())
144 .num_threads(thread_count)
145 .max_concurrency(config.pool_concurrency())
146 .build()?;
147
148 Ok(pool)
149}
150
151#[derive(Debug)]
152struct StateInner {
153 config: Arc<Config>,
154 memory_checker: MemoryChecker,
155 registry: Registry,
156}
157
158#[derive(Clone, Debug)]
160pub struct ServiceState {
161 inner: Arc<StateInner>,
162}
163
164impl ServiceState {
165 pub async fn start(
167 handle: &relay_system::Handle,
168 services: &dyn ServiceSpawn,
169 config: Arc<Config>,
170 ) -> Result<Self> {
171 let upstream_relay = services.start(UpstreamRelayService::new(config.clone()));
172 let current_config = config.current();
173
174 #[cfg(feature = "processing")]
175 let redis_clients = current_config
176 .redis()
177 .filter(|_| current_config.processing_enabled())
178 .map(create_redis_clients)
179 .transpose()
180 .context(ServiceError::Redis)?;
181
182 #[cfg(feature = "processing")]
187 if let Some(redis_clients) = &redis_clients {
188 initialize_redis_scripts_for_client(redis_clients)
189 .await
190 .context(ServiceError::Redis)?;
191 }
192
193 let memory_stat = MemoryStat::new(current_config.memory_stat_refresh_frequency_ms());
196
197 let (processor, processor_rx) = match current_config.relay_mode() {
200 RelayMode::Proxy => channel(ProxyProcessorService::name()),
201 RelayMode::Managed => channel(EnvelopeProcessorService::name()),
202 };
203
204 let (aggregator, aggregator_rx) = channel(RouterService::name());
205
206 let outcome_aggregator = match current_config.emit_outcomes() {
207 EmitOutcomes::None => services.start(NullOutcomeProducerService::new()),
208 _ => match current_config.relay_mode() {
209 RelayMode::Proxy => services.start(ClientReportOutcomeProducerService::new(
210 ¤t_config,
211 processor.clone(),
212 )),
213 RelayMode::Managed => services.start(OutcomeProducerService::new(
214 Arc::clone(&config),
215 aggregator.clone(),
216 )),
217 },
218 };
219
220 let (global_config, global_config_rx) =
221 GlobalConfigService::new(config.clone(), upstream_relay.clone());
222 let global_config_handle = global_config.handle();
223 let global_config = services.start(global_config);
227
228 let project_source = ProjectSource::start_in(
229 services,
230 Arc::clone(&config),
231 upstream_relay.clone(),
232 #[cfg(feature = "processing")]
233 redis_clients.clone(),
234 )
235 .await;
236 let project_cache_handle =
237 ProjectCacheService::new(Arc::clone(&config), project_source).start_in(services);
238
239 let metric_outcomes = MetricOutcomes::new(outcome_aggregator.clone());
240
241 #[cfg(feature = "processing")]
242 let store_pool = create_store_pool(¤t_config)?;
243 #[cfg(feature = "processing")]
244 let store = current_config
245 .processing_enabled()
246 .then(|| {
247 StoreService::create(
248 store_pool.clone(),
249 config.clone(),
250 global_config_handle.clone(),
251 metric_outcomes.clone(),
252 )
253 .map(|s| services.start(s))
254 })
255 .transpose()?;
256
257 #[cfg(feature = "processing")]
258 let objectstore = ObjectstoreService::new(current_config.objectstore(), store.clone())?
259 .map(|s| {
260 let concurrent = ConcurrentService::new(s)
261 .with_backlog_limit(current_config.objectstore().max_backlog)
262 .with_concurrency_limit(current_config.objectstore().max_concurrent_requests);
263 services.start(concurrent)
264 });
265
266 let envelope_buffer = PartitionedEnvelopeBuffer::create(
267 current_config.spool_partitions(),
268 config.clone(),
269 memory_stat.clone(),
270 global_config_rx.clone(),
271 project_cache_handle.clone(),
272 processor.clone(),
273 outcome_aggregator.clone(),
274 services,
275 );
276
277 let (processor_pool, aggregator_handle, autoscaling) = match current_config.relay_mode() {
278 RelayMode::Proxy => {
279 services.start_with(
280 ProxyProcessorService::new(
281 config.clone(),
282 project_cache_handle.clone(),
283 ProxyAddrs {
284 outcome_aggregator: outcome_aggregator.clone(),
285 upstream_relay: upstream_relay.clone(),
286 },
287 ),
288 processor_rx,
289 );
290 (None, None, None)
291 }
292 RelayMode::Managed => {
293 let processor_pool = create_processor_pool(¤t_config)?;
294
295 let router = RouterService::new(
296 handle.clone(),
297 current_config.default_aggregator_config().clone(),
298 current_config.secondary_aggregator_configs().clone(),
299 Some(processor.clone().recipient()),
300 project_cache_handle.clone(),
301 );
302 let router_handle = router.handle();
303 services.start_with(router, aggregator_rx);
304
305 let cogs = CogsService::new(¤t_config);
306 let cogs = Cogs::new(CogsServiceRecorder::new(
307 ¤t_config,
308 services.start(cogs),
309 ));
310
311 services.start_with(
312 EnvelopeProcessorService::new(
313 processor_pool.clone(),
314 config.clone(),
315 global_config_handle.clone(),
316 project_cache_handle.clone(),
317 cogs,
318 #[cfg(feature = "processing")]
319 redis_clients.clone(),
320 processor::Addrs {
321 outcome_aggregator: outcome_aggregator.clone(),
322 upstream_relay: upstream_relay.clone(),
323 #[cfg(feature = "processing")]
324 objectstore: objectstore.clone(),
325 #[cfg(feature = "processing")]
326 store_forwarder: store,
327 aggregator: aggregator.clone(),
328 },
329 metric_outcomes.clone(),
330 ),
331 processor_rx,
332 );
333
334 let autoscaling = services.start(AutoscalingMetricService::new(
335 memory_stat.clone(),
336 envelope_buffer.clone(),
337 handle.clone(),
338 processor_pool.clone(),
339 ));
340
341 (Some(processor_pool), Some(router_handle), Some(autoscaling))
342 }
343 };
344
345 let health_check = services.start(HealthCheckService::new(
346 config.clone(),
347 MemoryChecker::new(memory_stat.clone(), config.clone()),
348 aggregator_handle,
349 upstream_relay.clone(),
350 envelope_buffer.clone(),
351 ));
352
353 services.start(RelayStats::new(
354 config.clone(),
355 handle.clone(),
356 upstream_relay.clone(),
357 #[cfg(feature = "processing")]
358 redis_clients.clone(),
359 processor_pool,
360 #[cfg(feature = "processing")]
361 store_pool,
362 ));
363
364 let relay_cache = services.start(RelayCacheService::new(
365 config.clone(),
366 upstream_relay.clone(),
367 ));
368
369 let upload = services.start(upload::create_service(
370 &config,
371 &upstream_relay,
372 #[cfg(feature = "processing")]
373 &objectstore,
374 ));
375
376 let _ = services.start(ConfigReloadService::new(config.clone()));
377
378 let registry = Registry {
379 processor,
380 health_check,
381 outcome_aggregator,
382 relay_cache,
383 global_config,
384 project_cache_handle,
385 upstream_relay,
386 envelope_buffer,
387 autoscaling,
388 #[cfg(feature = "processing")]
389 objectstore,
390 upload,
391 global_config_handle,
392 };
393
394 let state = StateInner {
395 config: config.clone(),
396 memory_checker: MemoryChecker::new(memory_stat, config.clone()),
397 registry,
398 };
399
400 Ok(ServiceState {
401 inner: Arc::new(state),
402 })
403 }
404
405 pub fn config(&self) -> ConfigSnapshot {
407 self.inner.config.current()
408 }
409
410 pub fn memory_checker(&self) -> &MemoryChecker {
414 &self.inner.memory_checker
415 }
416
417 pub fn autoscaling(&self) -> Option<&Addr<AutoscalingMetrics>> {
418 self.inner.registry.autoscaling.as_ref()
419 }
420
421 pub fn envelope_buffer(&self, project_key_pair: ProjectKeyPair) -> &ObservableEnvelopeBuffer {
423 self.inner.registry.envelope_buffer.buffer(project_key_pair)
424 }
425
426 pub fn project_cache_handle(&self) -> &ProjectCacheHandle {
428 &self.inner.registry.project_cache_handle
429 }
430
431 pub fn relay_cache(&self) -> &Addr<RelayCache> {
433 &self.inner.registry.relay_cache
434 }
435
436 pub fn health_check(&self) -> &Addr<HealthCheck> {
438 &self.inner.registry.health_check
439 }
440
441 pub fn upstream_relay(&self) -> &Addr<UpstreamRelay> {
443 &self.inner.registry.upstream_relay
444 }
445
446 pub fn processor(&self) -> &Addr<EnvelopeProcessor> {
448 &self.inner.registry.processor
449 }
450
451 pub fn global_config(&self) -> &Addr<GlobalConfigManager> {
453 &self.inner.registry.global_config
454 }
455
456 pub fn outcome_aggregator(&self) -> &Addr<TrackOutcome> {
458 &self.inner.registry.outcome_aggregator
459 }
460
461 #[cfg(feature = "processing")]
462 pub fn objectstore(&self) -> Option<&Addr<Objectstore>> {
464 self.inner.registry.objectstore.as_ref()
465 }
466
467 pub fn upload(&self) -> &Addr<Upload> {
469 &self.inner.registry.upload
470 }
471
472 pub fn global_config_handle(&self) -> &GlobalConfigHandle {
473 &self.inner.registry.global_config_handle
474 }
475}
476
477#[cfg(feature = "processing")]
485pub fn create_redis_clients(configs: RedisConfigsRef<'_>) -> Result<RedisClients, RedisError> {
486 const PROJECT_CONFIG_REDIS_CLIENT: &str = "projectconfig";
487 const QUOTA_REDIS_CLIENT: &str = "quotas";
488 const UNIFIED_REDIS_CLIENT: &str = "unified";
489
490 match configs {
491 RedisConfigsRef::Unified(unified) => {
492 let client = create_async_redis_client(UNIFIED_REDIS_CLIENT, &unified)?;
493
494 Ok(RedisClients {
495 project_configs: client.clone(),
496 quotas: client,
497 })
498 }
499 RedisConfigsRef::Individual {
500 project_configs,
501 quotas,
502 } => {
503 let project_configs =
504 create_async_redis_client(PROJECT_CONFIG_REDIS_CLIENT, &project_configs)?;
505 let quotas = create_async_redis_client(QUOTA_REDIS_CLIENT, "as)?;
506
507 Ok(RedisClients {
508 project_configs,
509 quotas,
510 })
511 }
512 }
513}
514
515#[cfg(feature = "processing")]
516fn create_async_redis_client(
517 name: &'static str,
518 config: &RedisConfigRef<'_>,
519) -> Result<AsyncRedisClient, RedisError> {
520 match config {
521 RedisConfigRef::Cluster {
522 cluster_nodes,
523 options,
524 } => AsyncRedisClient::cluster(name, cluster_nodes.iter().map(|s| s.as_str()), options),
525 RedisConfigRef::Single { server, options } => {
526 AsyncRedisClient::single(name, server, options)
527 }
528 }
529}
530
531#[cfg(feature = "processing")]
532async fn initialize_redis_scripts_for_client(
533 redis_clients: &RedisClients,
534) -> Result<(), RedisError> {
535 let scripts = RedisScripts::all();
536
537 let RedisClients {
538 project_configs,
539 quotas,
540 } = redis_clients;
541
542 initialize_redis_scripts(project_configs, &scripts).await?;
543 initialize_redis_scripts(quotas, &scripts).await?;
544
545 Ok(())
546}
547
548#[cfg(feature = "processing")]
549async fn initialize_redis_scripts(
550 client: &AsyncRedisClient,
551 scripts: &[&Script],
552) -> Result<(), RedisError> {
553 let mut connection = client.get_connection().await?;
554
555 for script in scripts {
556 script
559 .prepare_invoke()
560 .load_async(&mut connection)
561 .await
562 .map_err(RedisError::Redis)?;
563 }
564
565 Ok(())
566}
567
568impl FromRequestParts<Self> for ServiceState {
569 type Rejection = Infallible;
570
571 async fn from_request_parts(_: &mut Parts, state: &Self) -> Result<Self, Self::Rejection> {
572 Ok(state.clone())
573 }
574}