mod common;
use async_nats::jetstream::stream::{Config, Source, SubjectTransform};
use nats_counters::CounterExt;
use num_bigint::BigInt;
#[tokio::test]
async fn test_counter_source_tracking() {
let server = common::start_jetstream_server();
let js = common::create_jetstream_context(&server.client_url()).await;
let es_config = Config {
name: "COUNT_ES".to_string(),
subjects: vec!["count.es.*".to_string()],
allow_message_counter: true,
allow_direct: true,
..Default::default()
};
js.create_stream(es_config)
.await
.expect("Failed to create ES stream");
let pl_config = Config {
name: "COUNT_PL".to_string(),
subjects: vec!["count.pl.*".to_string()],
allow_message_counter: true,
allow_direct: true,
..Default::default()
};
js.create_stream(pl_config)
.await
.expect("Failed to create PL stream");
let eu_config = Config {
name: "COUNT_EU".to_string(),
subjects: vec!["count.eu.*".to_string()],
allow_message_counter: true,
allow_direct: true,
sources: Some(vec![
Source {
name: "COUNT_ES".to_string(),
subject_transforms: vec![SubjectTransform {
source: "count.es.>".to_string(),
destination: "count.eu.>".to_string(),
}],
..Default::default()
},
Source {
name: "COUNT_PL".to_string(),
subject_transforms: vec![SubjectTransform {
source: "count.pl.>".to_string(),
destination: "count.eu.>".to_string(),
}],
..Default::default()
},
]),
..Default::default()
};
js.create_stream(eu_config)
.await
.expect("Failed to create EU stream");
let es_counter = js
.get_counter("COUNT_ES")
.await
.expect("Failed to get ES counter");
let pl_counter = js
.get_counter("COUNT_PL")
.await
.expect("Failed to get PL counter");
let eu_counter = js
.get_counter("COUNT_EU")
.await
.expect("Failed to get EU counter");
es_counter.add("count.es.requests", 100).await.unwrap();
es_counter.add("count.es.errors", 5).await.unwrap();
pl_counter.add("count.pl.requests", 150).await.unwrap();
pl_counter.add("count.pl.errors", 3).await.unwrap();
eu_counter.add("count.eu.requests", 50).await.unwrap();
eu_counter.add("count.eu.errors", 2).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let eu_requests = eu_counter
.get("count.eu.requests")
.await
.expect("Failed to get EU requests");
let eu_errors = eu_counter
.get("count.eu.errors")
.await
.expect("Failed to get EU errors");
println!("EU requests value: {}", eu_requests.value);
println!("EU requests sources: {:?}", eu_requests.sources);
println!("EU errors value: {}", eu_errors.value);
println!("EU errors sources: {:?}", eu_errors.sources);
assert_eq!(eu_requests.value, BigInt::from(300));
assert_eq!(eu_errors.value, BigInt::from(10));
assert!(
!eu_requests.sources.is_empty(),
"Sources should be populated for aggregated stream"
);
assert_eq!(
eu_requests.sources.len(),
2,
"Expected 2 source streams (COUNT_ES, COUNT_PL)"
);
assert!(
eu_requests.sources.contains_key("COUNT_ES"),
"COUNT_ES should be in sources"
);
if let Some(es_sources) = eu_requests.sources.get("COUNT_ES") {
assert_eq!(
es_sources.get("count.es.requests"),
Some(&BigInt::from(100)),
"COUNT_ES should contribute 100 requests"
);
}
assert!(
eu_requests.sources.contains_key("COUNT_PL"),
"COUNT_PL should be in sources"
);
if let Some(pl_sources) = eu_requests.sources.get("COUNT_PL") {
assert_eq!(
pl_sources.get("count.pl.requests"),
Some(&BigInt::from(150)),
"COUNT_PL should contribute 150 requests"
);
}
assert_eq!(
eu_errors.sources.len(),
2,
"Expected 2 source streams for errors"
);
if let Some(es_sources) = eu_errors.sources.get("COUNT_ES") {
assert_eq!(
es_sources.get("count.es.errors"),
Some(&BigInt::from(5)),
"COUNT_ES should contribute 5 errors"
);
}
if let Some(pl_sources) = eu_errors.sources.get("COUNT_PL") {
assert_eq!(
pl_sources.get("count.pl.errors"),
Some(&BigInt::from(3)),
"COUNT_PL should contribute 3 errors"
);
}
}
#[tokio::test]
async fn test_counter_sources_single_stream() {
let server = common::start_jetstream_server();
let js = common::create_jetstream_context(&server.client_url()).await;
let _stream = common::create_counter_stream(&js, "SINGLE", vec!["single.*".to_string()]).await;
let counter = js
.get_counter("SINGLE")
.await
.expect("Failed to get counter");
counter.add("single.value", 10).await.unwrap();
counter.add("single.value", 20).await.unwrap();
counter.add("single.value", 30).await.unwrap();
let entry = counter
.get("single.value")
.await
.expect("Failed to get entry");
assert_eq!(entry.value, BigInt::from(60));
assert_eq!(entry.increment, Some(BigInt::from(30)));
println!("Sources map: {:?}", entry.sources);
if !entry.sources.is_empty() {
assert!(entry.sources.contains_key("SINGLE"));
if let Some(sources) = entry.sources.get("SINGLE") {
assert_eq!(sources.get("single.value"), Some(&BigInt::from(60)));
}
}
}
#[tokio::test]
async fn test_counter_three_level_source_aggregation() {
let server = common::start_jetstream_server();
let js = common::create_jetstream_context(&server.client_url()).await;
let es_config = Config {
name: "COUNT_ES".to_string(),
subjects: vec!["count.es.*".to_string()],
allow_message_counter: true,
allow_direct: true,
..Default::default()
};
js.create_stream(es_config)
.await
.expect("Failed to create ES stream");
let pl_config = Config {
name: "COUNT_PL".to_string(),
subjects: vec!["count.pl.*".to_string()],
allow_message_counter: true,
allow_direct: true,
..Default::default()
};
js.create_stream(pl_config)
.await
.expect("Failed to create PL stream");
let eu_config = Config {
name: "COUNT_EU".to_string(),
subjects: vec!["count.eu.*".to_string()],
allow_message_counter: true,
allow_direct: true,
sources: Some(vec![
Source {
name: "COUNT_ES".to_string(),
subject_transforms: vec![SubjectTransform {
source: "count.es.>".to_string(),
destination: "count.eu.>".to_string(),
}],
..Default::default()
},
Source {
name: "COUNT_PL".to_string(),
subject_transforms: vec![SubjectTransform {
source: "count.pl.>".to_string(),
destination: "count.eu.>".to_string(),
}],
..Default::default()
},
]),
..Default::default()
};
js.create_stream(eu_config)
.await
.expect("Failed to create EU stream");
let global_config = Config {
name: "COUNT_GLOBAL".to_string(),
subjects: vec!["count.global.*".to_string()],
allow_message_counter: true,
allow_direct: true,
sources: Some(vec![Source {
name: "COUNT_EU".to_string(),
subject_transforms: vec![SubjectTransform {
source: "count.eu.>".to_string(),
destination: "count.global.>".to_string(),
}],
..Default::default()
}]),
..Default::default()
};
js.create_stream(global_config)
.await
.expect("Failed to create GLOBAL stream");
let es_counter = js
.get_counter("COUNT_ES")
.await
.expect("Failed to get ES counter");
let pl_counter = js
.get_counter("COUNT_PL")
.await
.expect("Failed to get PL counter");
let eu_counter = js
.get_counter("COUNT_EU")
.await
.expect("Failed to get EU counter");
let global_counter = js
.get_counter("COUNT_GLOBAL")
.await
.expect("Failed to get GLOBAL counter");
println!("Adding to regional streams (ES, PL)...");
es_counter
.add("count.es.hits", 100)
.await
.expect("Failed to add ES hits");
es_counter
.add("count.es.views", 200)
.await
.expect("Failed to add ES views");
pl_counter
.add("count.pl.hits", 150)
.await
.expect("Failed to add PL hits");
pl_counter
.add("count.pl.views", 50)
.await
.expect("Failed to add PL views");
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
println!("Checking EU aggregation (Level 2)...");
let eu_hits = eu_counter
.get("count.eu.hits")
.await
.expect("Failed to get EU hits");
let eu_views = eu_counter
.get("count.eu.views")
.await
.expect("Failed to get EU views");
assert_eq!(
eu_hits.value,
BigInt::from(250),
"EU hits should be 100 + 150 = 250"
);
assert_eq!(
eu_views.value,
BigInt::from(250),
"EU views should be 200 + 50 = 250"
);
println!("EU hits sources: {:?}", eu_hits.sources);
println!("EU views sources: {:?}", eu_views.sources);
assert!(
!eu_hits.sources.is_empty(),
"EU hits sources should be populated"
);
assert_eq!(
eu_hits.sources.len(),
2,
"EU hits should have 2 source streams"
);
assert!(
eu_hits.sources.contains_key("COUNT_ES"),
"EU hits should track COUNT_ES"
);
if let Some(es_sources) = eu_hits.sources.get("COUNT_ES") {
assert_eq!(
es_sources.get("count.es.hits"),
Some(&BigInt::from(100)),
"COUNT_ES should contribute 100 hits"
);
}
assert!(
eu_hits.sources.contains_key("COUNT_PL"),
"EU hits should track COUNT_PL"
);
if let Some(pl_sources) = eu_hits.sources.get("COUNT_PL") {
assert_eq!(
pl_sources.get("count.pl.hits"),
Some(&BigInt::from(150)),
"COUNT_PL should contribute 150 hits"
);
}
assert_eq!(
eu_views.sources.len(),
2,
"EU views should have 2 source streams"
);
if let Some(es_sources) = eu_views.sources.get("COUNT_ES") {
assert_eq!(
es_sources.get("count.es.views"),
Some(&BigInt::from(200)),
"COUNT_ES should contribute 200 views"
);
}
if let Some(pl_sources) = eu_views.sources.get("COUNT_PL") {
assert_eq!(
pl_sources.get("count.pl.views"),
Some(&BigInt::from(50)),
"COUNT_PL should contribute 50 views"
);
}
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
println!("Checking GLOBAL aggregation (Level 3)...");
let global_hits = global_counter
.get("count.global.hits")
.await
.expect("Failed to get GLOBAL hits");
let global_views = global_counter
.get("count.global.views")
.await
.expect("Failed to get GLOBAL views");
assert_eq!(
global_hits.value,
BigInt::from(250),
"GLOBAL hits should match EU: 250"
);
assert_eq!(
global_views.value,
BigInt::from(250),
"GLOBAL views should match EU: 250"
);
println!("GLOBAL hits sources: {:?}", global_hits.sources);
println!("GLOBAL views sources: {:?}", global_views.sources);
assert!(
!global_hits.sources.is_empty(),
"GLOBAL hits sources should be populated"
);
assert_eq!(
global_hits.sources.len(),
1,
"GLOBAL should have 1 source stream (COUNT_EU)"
);
assert!(
global_hits.sources.contains_key("COUNT_EU"),
"GLOBAL should track COUNT_EU"
);
if let Some(eu_source) = global_hits.sources.get("COUNT_EU") {
assert_eq!(
eu_source.get("count.eu.hits"),
Some(&BigInt::from(250)),
"COUNT_EU should contribute 250 hits to GLOBAL"
);
}
assert_eq!(
global_views.sources.len(),
1,
"GLOBAL views should have 1 source stream"
);
if let Some(eu_source) = global_views.sources.get("COUNT_EU") {
assert_eq!(
eu_source.get("count.eu.views"),
Some(&BigInt::from(250)),
"COUNT_EU should contribute 250 views to GLOBAL"
);
}
println!("Adding more values to test propagation...");
es_counter
.add("count.es.hits", 50)
.await
.expect("Failed to add more ES hits");
tokio::time::sleep(tokio::time::Duration::from_millis(700)).await;
let eu_hits_updated = eu_counter.load("count.eu.hits").await.unwrap();
assert_eq!(
eu_hits_updated,
BigInt::from(300),
"EU hits should update to 300 (250 + 50)"
);
let global_hits_updated = global_counter.load("count.global.hits").await.unwrap();
assert_eq!(
global_hits_updated,
BigInt::from(300),
"GLOBAL hits should propagate to 300"
);
println!("Three-level aggregation test completed successfully!");
}