1use std::pin::Pin;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::{Duration, SystemTime};
5
6use hashbrown::HashMap;
7use hashbrown::hash_map::Entry;
8use relay_base_schema::project::ProjectKey;
9use relay_config::AggregatorServiceConfig;
10use relay_metrics::Bucket;
11use relay_metrics::aggregator::{self, AggregateMetricsError, AggregatorConfig, Partition};
12use relay_quotas::{RateLimits, Scoping};
13use relay_system::{Controller, FromMessage, Interface, NoResponse, Recipient, Service};
14use tokio::time::{Instant, Sleep};
15
16use crate::services::projects::cache::ProjectCacheHandle;
17use crate::services::projects::project::{ProjectInfo, ProjectState};
18use crate::statsd::{RelayCounters, RelayTimers};
19
20#[derive(Debug)]
31pub enum Aggregator {
32 MergeBuckets(MergeBuckets),
34}
35
36impl Aggregator {
37 pub fn variant(&self) -> &'static str {
39 match self {
40 Aggregator::MergeBuckets(_) => "MergeBuckets",
41 }
42 }
43}
44
45impl Interface for Aggregator {}
46
47impl FromMessage<MergeBuckets> for Aggregator {
48 type Response = NoResponse;
49 fn from_message(message: MergeBuckets, _: ()) -> Self {
50 Self::MergeBuckets(message)
51 }
52}
53
54#[derive(Clone, Debug)]
61pub struct FlushBuckets {
62 pub partition_key: u32,
64 pub buckets: HashMap<ProjectKey, ProjectBuckets>,
66}
67
68#[derive(Debug, Clone)]
70pub struct ProjectBuckets {
71 pub buckets: Vec<Bucket>,
73 pub scoping: Scoping,
75 pub project_info: Arc<ProjectInfo>,
77 pub rate_limits: Arc<RateLimits>,
79}
80
81impl Extend<Bucket> for ProjectBuckets {
82 fn extend<T: IntoIterator<Item = Bucket>>(&mut self, iter: T) {
83 self.buckets.extend(iter)
84 }
85}
86
87pub struct AggregatorService {
89 aggregator: aggregator::Aggregator,
90 receiver: Option<Recipient<FlushBuckets, NoResponse>>,
91 project_cache: ProjectCacheHandle,
92 config: AggregatorServiceConfig,
93 can_accept_metrics: Arc<AtomicBool>,
94 next_flush: Pin<Box<Sleep>>,
95}
96
97impl AggregatorService {
98 pub fn new(
103 config: AggregatorServiceConfig,
104 receiver: Option<Recipient<FlushBuckets, NoResponse>>,
105 project_cache: ProjectCacheHandle,
106 ) -> Self {
107 Self::named("default".to_owned(), config, receiver, project_cache)
108 }
109
110 pub(crate) fn named(
112 name: String,
113 config: AggregatorServiceConfig,
114 receiver: Option<Recipient<FlushBuckets, NoResponse>>,
115 project_cache: ProjectCacheHandle,
116 ) -> Self {
117 let aggregator = aggregator::Aggregator::named(name, &config.aggregator);
118 Self {
119 receiver,
120 config,
121 can_accept_metrics: Arc::new(AtomicBool::new(true)),
122 aggregator,
123 project_cache,
124 next_flush: Box::pin(tokio::time::sleep(Duration::from_secs(0))),
125 }
126 }
127
128 pub fn handle(&self) -> AggregatorHandle {
129 AggregatorHandle {
130 can_accept_metrics: Arc::clone(&self.can_accept_metrics),
131 }
132 }
133
134 fn try_flush(&mut self) -> Duration {
140 let partition = match self.aggregator.try_flush_next(SystemTime::now()) {
141 Ok(partition) => partition,
142 Err(duration) => return duration,
143 };
144 self.can_accept_metrics.store(true, Ordering::Relaxed);
145
146 self.flush_partition(partition);
147
148 self.aggregator.next_flush_at(SystemTime::now())
149 }
150
151 fn flush_partition(&mut self, partition: Partition) {
152 let Some(receiver) = &self.receiver else {
153 return;
154 };
155
156 let mut buckets_by_project = hashbrown::HashMap::new();
157
158 let partition_key = partition.partition_key;
159 for (project_key, bucket) in partition {
160 let s = match buckets_by_project.entry(project_key) {
161 Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
162 Entry::Vacant(vacant_entry) => {
163 let project = self.project_cache.get(project_key);
164
165 let project_info = match project.state() {
166 ProjectState::Enabled(info) => Arc::clone(info),
167 ProjectState::Dummy => {
170 relay_log::error!(
171 tags.aggregator = self.aggregator.name(),
172 tags.project_key = project_key.as_str(),
173 "metrics aggregator requires a project config"
174 );
175 continue;
177 }
178 ProjectState::Disabled => continue, ProjectState::Pending => {
180 if let Err(error) = self.aggregator.merge(project_key, bucket) {
182 relay_log::error!(
183 tags.aggregator = self.aggregator.name(),
184 tags.project_key = project_key.as_str(),
185 bucket.error = &error as &dyn std::error::Error,
186 "failed to return metric bucket back to the aggregator"
187 );
188 }
189 relay_statsd::metric!(
190 counter(RelayCounters::ProjectStateFlushMetricsNoProject) += 1
191 );
192 continue;
193 }
194 };
195
196 let rate_limits = project.rate_limits().current_limits();
197 let Some(scoping) = project_info.scoping(project_key) else {
198 relay_log::error!(
203 tags.project_key = project_key.as_str(),
204 "dropping buckets because of missing scope",
205 );
206 continue;
207 };
208
209 vacant_entry.insert(ProjectBuckets {
210 buckets: Vec::new(),
211 scoping,
212 project_info,
213 rate_limits,
214 })
215 }
216 };
217
218 s.buckets.push(bucket);
219 }
220
221 if !buckets_by_project.is_empty() {
222 relay_log::debug!(
223 "flushing buckets for {} projects in partition {partition_key}",
224 buckets_by_project.len()
225 );
226
227 receiver.send(FlushBuckets {
228 partition_key,
229 buckets: buckets_by_project,
230 });
231 }
232 }
233
234 fn handle_merge_buckets(&mut self, msg: MergeBuckets) {
235 let MergeBuckets {
236 project_key,
237 buckets,
238 } = msg;
239
240 for mut bucket in buckets.into_iter() {
241 if !validate_bucket(&mut bucket, &self.config) {
242 continue;
243 };
244
245 match self.aggregator.merge(project_key, bucket) {
246 Err(AggregateMetricsError::InvalidTimestamp(_)) => {}
248 Err(AggregateMetricsError::TotalLimitExceeded) => {
249 relay_log::error!(
250 tags.aggregator = self.aggregator.name(),
251 "aggregator limit exceeded"
252 );
253 self.can_accept_metrics.store(false, Ordering::Relaxed);
254 break;
255 }
256 Err(AggregateMetricsError::ProjectLimitExceeded) => {
257 relay_log::error!(
258 tags.aggregator = self.aggregator.name(),
259 tags.project_key = project_key.as_str(),
260 "project metrics limit exceeded for project {project_key}"
261 );
262 break;
263 }
264 Err(error) => {
265 relay_log::error!(
266 tags.aggregator = self.aggregator.name(),
267 tags.project_key = project_key.as_str(),
268 bucket.error = &error as &dyn std::error::Error,
269 "failed to aggregate metric bucket"
270 );
271 }
272 Ok(()) => {}
273 };
274 }
275 }
276
277 fn handle_message(&mut self, message: Aggregator) {
278 match message {
279 Aggregator::MergeBuckets(msg) => self.handle_merge_buckets(msg),
280 }
281 }
282
283 fn handle_shutdown(&mut self) {
284 relay_log::info!(
285 "Shutting down metrics aggregator {}",
286 self.aggregator.name()
287 );
288
289 let aggregator = aggregator::Aggregator::named(
291 self.aggregator.name().to_owned(),
292 &AggregatorConfig {
293 bucket_interval: 1,
294 aggregator_size: 1,
295 initial_delay: 0,
296 ..self.config.aggregator
297 },
298 );
299
300 let previous = std::mem::replace(&mut self.aggregator, aggregator);
301
302 let mut partitions = 0;
303 for partition in previous.into_partitions() {
304 self.flush_partition(partition);
305 partitions += 1;
306 }
307 relay_log::debug!("Force flushed {partitions} partitions");
308
309 self.next_flush
311 .as_mut()
312 .reset(Instant::now() + self.aggregator.next_flush_at(SystemTime::now()));
313 }
314}
315
316impl Service for AggregatorService {
317 type Interface = Aggregator;
318
319 async fn run(mut self, mut rx: relay_system::Receiver<Self::Interface>) {
320 let mut shutdown = Controller::shutdown_handle();
321
322 macro_rules! timed {
323 ($task:expr, $body:expr) => {{
324 let task_name = $task;
325 relay_statsd::metric!(
326 timer(RelayTimers::AggregatorServiceDuration),
327 task = task_name,
328 aggregator = self.aggregator.name(),
329 { $body }
330 )
331 }};
332 }
333
334 loop {
335 tokio::select! {
336 biased;
337
338 _ = &mut self.next_flush => timed!(
339 "try_flush", {
340 let next = self.try_flush();
341 self.next_flush.as_mut().reset(Instant::now() + next);
342 }
343 ),
344 Some(message) = rx.recv() => timed!(message.variant(), self.handle_message(message)),
345 _ = shutdown.notified() => timed!("shutdown", self.handle_shutdown()),
346
347 else => break,
348 }
349 }
350 }
351}
352
353impl Drop for AggregatorService {
354 fn drop(&mut self) {
355 if !self.aggregator.is_empty() {
356 relay_log::error!(
357 tags.aggregator = self.aggregator.name(),
358 "metrics aggregator dropping buckets"
359 );
360 relay_statsd::metric!(
361 counter(RelayCounters::BucketsDropped) += 1,
362 aggregator = self.aggregator.name(),
363 );
364 }
365 }
366}
367
368#[derive(Debug)]
370pub struct MergeBuckets {
371 pub project_key: ProjectKey,
372 pub buckets: Vec<Bucket>,
373}
374
375impl MergeBuckets {
376 pub fn new(project_key: ProjectKey, buckets: Vec<Bucket>) -> Self {
378 Self {
379 project_key,
380 buckets,
381 }
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct AggregatorHandle {
388 can_accept_metrics: Arc<AtomicBool>,
389}
390
391impl AggregatorHandle {
392 pub fn can_accept_metrics(&self) -> bool {
394 self.can_accept_metrics.load(Ordering::Relaxed)
395 }
396}
397
398fn validate_bucket(bucket: &mut Bucket, config: &AggregatorServiceConfig) -> bool {
402 if bucket.name.len() > config.max_name_length {
403 relay_log::debug!(
404 "Invalid metric name, too long (> {}): {:?}",
405 config.max_name_length,
406 bucket.name
407 );
408 return false;
409 }
410
411 bucket.tags.retain(|tag_key, tag_value| {
412 if tag_key.len() > config.max_tag_key_length {
413 relay_log::debug!("Invalid metric tag key {tag_key:?}");
414 return false;
415 }
416 if bytecount::num_chars(tag_value.as_bytes()) > config.max_tag_value_length {
417 relay_log::debug!("Invalid metric tag value");
418 return false;
419 }
420
421 true
422 });
423
424 true
425}
426
427#[cfg(test)]
428mod tests {
429 use std::collections::BTreeMap;
430 use std::sync::{Arc, RwLock};
431
432 use relay_base_schema::organization::OrganizationId;
433 use relay_base_schema::project::ProjectId;
434 use relay_common::time::UnixTimestamp;
435 use relay_metrics::{BucketMetadata, BucketValue, aggregator::AggregatorConfig};
436
437 use super::*;
438
439 #[derive(Default)]
440 struct ReceivedData {
441 buckets: Vec<Bucket>,
442 }
443
444 struct TestInterface(FlushBuckets);
445
446 impl Interface for TestInterface {}
447
448 impl FromMessage<FlushBuckets> for TestInterface {
449 type Response = NoResponse;
450
451 fn from_message(message: FlushBuckets, _: ()) -> Self {
452 Self(message)
453 }
454 }
455
456 #[derive(Clone, Default)]
457 struct TestReceiver {
458 data: Arc<RwLock<ReceivedData>>,
459 reject_all: bool,
460 }
461
462 impl TestReceiver {
463 fn add_buckets(&self, buckets: HashMap<ProjectKey, ProjectBuckets>) {
464 let buckets = buckets.into_values().flat_map(|s| s.buckets);
465 self.data.write().unwrap().buckets.extend(buckets);
466 }
467
468 fn bucket_count(&self) -> usize {
469 self.data.read().unwrap().buckets.len()
470 }
471 }
472
473 impl Service for TestReceiver {
474 type Interface = TestInterface;
475
476 async fn run(self, mut rx: relay_system::Receiver<Self::Interface>) {
477 while let Some(message) = rx.recv().await {
478 let buckets = message.0.buckets;
479 relay_log::debug!(?buckets, "received buckets");
480 if !self.reject_all {
481 self.add_buckets(buckets);
482 }
483 }
484 }
485 }
486
487 fn some_bucket() -> Bucket {
488 let timestamp = UnixTimestamp::from_secs(999994711);
489 Bucket {
490 timestamp,
491 width: 0,
492 name: "c:transactions/foo".into(),
493 value: BucketValue::counter(42.into()),
494 tags: BTreeMap::new(),
495 metadata: BucketMetadata::new(timestamp),
496 }
497 }
498
499 #[tokio::test(start_paused = true)]
500 async fn test_flush_bucket() {
501 relay_test::setup();
502
503 let project_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap();
504
505 let receiver = TestReceiver::default();
506 let recipient = receiver.clone().start_detached().recipient();
507 let project_cache = ProjectCacheHandle::for_test();
508 project_cache.test_set_project_state(
509 project_key,
510 ProjectState::Enabled({
511 Arc::new(ProjectInfo {
512 project_id: Some(ProjectId::new(3)),
514 organization_id: Some(OrganizationId::new(1)),
515 ..Default::default()
516 })
517 }),
518 );
519
520 let config = AggregatorServiceConfig {
521 aggregator: AggregatorConfig {
522 bucket_interval: 1,
523 initial_delay: 0,
524 ..Default::default()
525 },
526 ..Default::default()
527 };
528 let aggregator =
529 AggregatorService::new(config, Some(recipient), project_cache).start_detached();
530
531 let mut bucket = some_bucket();
532 bucket.timestamp = UnixTimestamp::now();
533
534 aggregator.send(MergeBuckets::new(project_key, vec![bucket]));
535
536 assert_eq!(receiver.bucket_count(), 0);
538
539 tokio::time::sleep(Duration::from_millis(2100)).await;
542 assert_eq!(receiver.bucket_count(), 1);
544 }
545
546 fn test_config() -> AggregatorServiceConfig {
547 AggregatorServiceConfig {
548 max_name_length: 200,
549 max_tag_key_length: 200,
550 max_tag_value_length: 200,
551 ..Default::default()
552 }
553 }
554
555 #[test]
556 fn test_validate_bucket_key_str_length() {
557 relay_test::setup();
558 let mut short_metric = Bucket {
559 timestamp: UnixTimestamp::now(),
560 name: "c:transactions/a_short_metric".into(),
561 tags: BTreeMap::new(),
562 metadata: Default::default(),
563 width: 0,
564 value: BucketValue::Counter(0.into()),
565 };
566 assert!(validate_bucket(&mut short_metric, &test_config()));
567
568 let mut long_metric = Bucket {
569 timestamp: UnixTimestamp::now(),
570 name: "c:transactions/long_name_a_very_long_name_its_super_long_really_but_like_super_long_probably_the_longest_name_youve_seen_and_even_the_longest_name_ever_its_extremly_long_i_cant_tell_how_long_it_is_because_i_dont_have_that_many_fingers_thus_i_cant_count_the_many_characters_this_long_name_is".into(),
571 tags: BTreeMap::new(),
572 metadata: Default::default(),
573 width: 0,
574 value: BucketValue::Counter(0.into()),
575 };
576 assert!(!validate_bucket(&mut long_metric, &test_config()));
577
578 let mut short_metric_long_tag_key = Bucket {
579 timestamp: UnixTimestamp::now(),
580 name: "c:transactions/a_short_metric_with_long_tag_key".into(),
581 tags: BTreeMap::from([("i_run_out_of_creativity_so_here_we_go_Lorem_Ipsum_is_simply_dummy_text_of_the_printing_and_typesetting_industry_Lorem_Ipsum_has_been_the_industrys_standard_dummy_text_ever_since_the_1500s_when_an_unknown_printer_took_a_galley_of_type_and_scrambled_it_to_make_a_type_specimen_book".into(), "tag_value".into())]),
582 metadata: Default::default(),
583 width: 0,
584 value: BucketValue::Counter(0.into()),
585 };
586 assert!(validate_bucket(
587 &mut short_metric_long_tag_key,
588 &test_config()
589 ));
590 assert_eq!(short_metric_long_tag_key.tags.len(), 0);
591
592 let mut short_metric_long_tag_value = Bucket {
593 timestamp: UnixTimestamp::now(),
594 name: "c:transactions/a_short_metric_with_long_tag_value".into(),
595 tags: BTreeMap::from([("tag_key".into(), "i_run_out_of_creativity_so_here_we_go_Lorem_Ipsum_is_simply_dummy_text_of_the_printing_and_typesetting_industry_Lorem_Ipsum_has_been_the_industrys_standard_dummy_text_ever_since_the_1500s_when_an_unknown_printer_took_a_galley_of_type_and_scrambled_it_to_make_a_type_specimen_book".into())]),
596 metadata: Default::default(),
597 width: 0,
598 value: BucketValue::Counter(0.into()),
599 };
600 assert!(validate_bucket(
601 &mut short_metric_long_tag_value,
602 &test_config()
603 ));
604 assert_eq!(short_metric_long_tag_value.tags.len(), 0);
605 }
606
607 #[test]
608 fn test_validate_tag_values_special_chars() {
609 relay_test::setup();
610
611 let tag_value = "x".repeat(199) + "ΓΈ";
612 assert_eq!(tag_value.chars().count(), 200); let mut short_metric = Bucket {
615 timestamp: UnixTimestamp::now(),
616 name: "c:transactions/a_short_metric".into(),
617 tags: BTreeMap::from([("foo".into(), tag_value.clone())]),
618 metadata: Default::default(),
619 width: 0,
620 value: BucketValue::Counter(0.into()),
621 };
622 assert!(validate_bucket(&mut short_metric, &test_config()));
623 assert_eq!(short_metric.tags["foo"], tag_value);
624 }
625}