use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use parking_lot::RwLock;
use rocketmq_runtime::OperationContext;
use rocketmq_runtime::ScheduledExecutionPolicy;
use rocketmq_runtime::ScheduledTaskConfig;
use rocketmq_runtime::ScheduledTaskGroup;
use rocketmq_runtime::TaskGroup;
use rocketmq_runtime::TaskKind;
use crate::statistics::statistics_item::StatisticsItem;
use crate::statistics::statistics_item_state_getter::StatisticsItemStateGetter;
use crate::statistics::statistics_kind_meta::StatisticsKindMeta;
use rocketmq_runtime::common::time_utils::current_millis;
type StatsTable = Arc<RwLock<HashMap<String, HashMap<String, Arc<StatisticsItem>>>>>;
const CLEANUP_TASK_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
struct CleanupTask {
task_group: TaskGroup,
operation: OperationContext,
}
impl CleanupTask {
async fn shutdown(self, timeout: Duration) {
match self.operation.cancel_and_wait(&self.task_group, timeout).await {
Ok(true) => {}
Ok(false) => tracing::warn!("statistics cleanup operation exceeded its shutdown deadline"),
Err(error) => tracing::warn!(%error, "statistics cleanup operation used an invalid owner"),
}
}
fn abort(self) {
self.operation.cancel();
}
}
pub struct StatisticsManager {
kind_meta_map: Arc<RwLock<HashMap<String, Arc<StatisticsKindMeta>>>>,
brief_metas: Option<Vec<(String, Vec<Vec<i64>>)>>,
stats_table: StatsTable,
statistics_item_state_getter: Arc<RwLock<Option<Arc<dyn StatisticsItemStateGetter + Send + Sync>>>>,
cleanup_task: Arc<Mutex<Option<CleanupTask>>>,
parent_task_group: TaskGroup,
}
impl StatisticsManager {
const MAX_IDLE_TIME: u64 = 10 * 60 * 1000;
pub fn new(parent_task_group: TaskGroup) -> Self {
Self::with_kind_meta(HashMap::new(), parent_task_group)
}
pub fn new_with_task_group(parent_task_group: TaskGroup) -> Self {
Self::new(parent_task_group)
}
pub fn with_kind_meta(kind_meta: HashMap<String, Arc<StatisticsKindMeta>>, parent_task_group: TaskGroup) -> Self {
let manager = Self {
kind_meta_map: Arc::new(RwLock::new(kind_meta)),
brief_metas: None,
stats_table: Arc::new(RwLock::new(HashMap::new())),
statistics_item_state_getter: Arc::new(RwLock::new(None)),
cleanup_task: Arc::new(Mutex::new(None)),
parent_task_group,
};
manager.start();
manager
}
pub fn with_kind_meta_and_task_group(
kind_meta: HashMap<String, Arc<StatisticsKindMeta>>,
parent_task_group: TaskGroup,
) -> Self {
Self::with_kind_meta(kind_meta, parent_task_group)
}
pub fn add_statistics_kind_meta(&self, kind_meta: Arc<StatisticsKindMeta>) {
let mut kind_meta_map = self.kind_meta_map.write();
kind_meta_map.insert(kind_meta.get_name().to_string(), kind_meta.clone());
let mut stats_table = self.stats_table.write();
stats_table.entry(kind_meta.get_name().to_string()).or_default();
}
pub fn set_brief_meta(&mut self, brief_metas: Vec<(String, Vec<Vec<i64>>)>) {
self.brief_metas = Some(brief_metas);
}
fn start(&self) {
let mut cleanup_task = self.cleanup_task.lock();
if cleanup_task.is_some() {
return;
}
let task_group = self.parent_task_group.clone();
let operation = OperationContext::without_deadline(TaskKind::ScheduledDriver);
let scheduled_tasks = ScheduledTaskGroup::new(task_group.clone());
let stats_table = self.stats_table.clone();
let kind_meta_map = self.kind_meta_map.clone();
let statistics_item_state_getter = self.statistics_item_state_getter.clone();
if let Err(error) = scheduled_tasks.schedule_operation(
&operation,
ScheduledTaskConfig::fixed_rate_no_overlap(
"common.statistics.cleanup",
Duration::from_millis(Self::MAX_IDLE_TIME / 3),
),
ScheduledExecutionPolicy::default(),
move || {
let stats_table = stats_table.clone();
let kind_meta_map = kind_meta_map.clone();
let statistics_item_state_getter = statistics_item_state_getter.clone();
async move {
let statistics_item_state_getter = statistics_item_state_getter.read().clone();
let expired_items =
expired_statistics_items(&stats_table, statistics_item_state_getter, current_millis());
for item in expired_items {
remove(item.as_ref(), &stats_table, &kind_meta_map);
}
}
},
) {
tracing::warn!(%error, "failed to spawn StatisticsManager cleanup task");
return;
}
*cleanup_task = Some(CleanupTask { task_group, operation });
}
pub async fn shutdown(&self) {
let cleanup_task = self.cleanup_task.lock().take();
if let Some(cleanup_task) = cleanup_task {
cleanup_task.shutdown(CLEANUP_TASK_SHUTDOWN_TIMEOUT).await;
}
}
pub async fn inc(&self, kind: &str, key: &str, item_accumulates: Vec<i64>) -> bool {
self.start();
if let Some(item_map) = self.stats_table.write().get_mut(kind) {
if let Some(item) = item_map.get(key) {
item.inc_items(item_accumulates);
return true;
} else {
let kind_meta_map = self.kind_meta_map.read();
if let Some(kind_meta) = kind_meta_map.get(kind) {
let item_names = kind_meta.get_item_names().iter().map(|item| item.as_str()).collect();
let Ok(new_item) = StatisticsItem::new(kind, key, item_names) else {
return false;
};
let new_item = Arc::new(new_item);
item_map.insert(key.to_string(), new_item.clone());
new_item.inc_items(item_accumulates);
self.schedule_statistics_item(new_item);
return true;
}
}
}
false
}
fn schedule_statistics_item(&self, item: Arc<StatisticsItem>) {
let kind_meta_map = self.kind_meta_map.read();
if let Some(kind_meta) = kind_meta_map.get(item.stat_kind()) {
kind_meta.get_scheduled_printer().schedule(item.as_ref());
}
}
pub fn set_statistics_item_state_getter(&mut self, getter: Arc<dyn StatisticsItemStateGetter + Send + Sync>) {
*self.statistics_item_state_getter.write() = Some(getter);
self.start();
}
}
impl Drop for StatisticsManager {
fn drop(&mut self) {
if let Some(cleanup_task) = self.cleanup_task.lock().take() {
cleanup_task.abort();
}
}
}
fn expired_statistics_items(
stats_table: &StatsTable,
statistics_item_state_getter: Option<Arc<dyn StatisticsItemStateGetter + Send + Sync>>,
now_millis: u64,
) -> Vec<Arc<StatisticsItem>> {
let stats_table = stats_table.read();
let mut expired_items = Vec::new();
for item_map in stats_table.values() {
for item in item_map.values() {
let expired = now_millis.saturating_sub(item.last_timestamp()) > StatisticsManager::MAX_IDLE_TIME;
let offline = statistics_item_state_getter
.as_ref()
.is_none_or(|getter| !getter.online(item));
if expired && offline {
expired_items.push(item.clone());
}
}
}
expired_items
}
pub fn remove(
item: &StatisticsItem,
stats_table: &StatsTable,
kind_meta_map: &Arc<RwLock<HashMap<String, Arc<StatisticsKindMeta>>>>,
) {
let stat_kind = item.stat_kind();
let stat_object = item.stat_object();
if let Some(item_map) = stats_table.write().get_mut(stat_kind) {
item_map.remove(stat_object);
}
let kind_meta_map = kind_meta_map.write();
if let Some(kind_meta) = kind_meta_map.get(stat_kind) {
kind_meta.get_scheduled_printer().remove(item);
}
}
#[cfg(test)]
mod tests {
use rocketmq_runtime::RuntimeContext;
use super::*;
use crate::statistics::statistics_item_scheduled_printer::StatisticsItemScheduledPrinter;
fn test_parent(name: &'static str) -> TaskGroup {
RuntimeContext::from_current(name)
.service_context("statistics-service")
.task_group()
.clone()
}
#[tokio::test]
async fn inc_rejects_empty_item_names_without_panicking() {
let manager = StatisticsManager::new(test_parent("statistics-invalid-item-test"));
manager.add_statistics_kind_meta(Arc::new(StatisticsKindMeta::new(
"kind".to_string(),
Vec::new(),
StatisticsItemScheduledPrinter,
)));
assert!(!manager.inc("kind", "key", vec![1]).await);
}
#[tokio::test]
async fn shutdown_stops_idle_cleanup_task() {
let manager = StatisticsManager::new(test_parent("statistics-shutdown-test"));
assert!(
manager.cleanup_task.lock().is_some(),
"cleanup task should be running after manager creation"
);
manager.shutdown().await;
assert!(manager.cleanup_task.lock().is_none());
manager.shutdown().await;
assert!(manager.cleanup_task.lock().is_none());
}
#[tokio::test]
async fn new_with_task_group_parents_cleanup_task() {
let context = RuntimeContext::from_current("statistics-manager-parent-test");
let service = context.service_context("statistics-service");
let manager = StatisticsManager::new_with_task_group(service.task_group().clone());
assert!(
manager.cleanup_task.lock().is_some(),
"cleanup task should be running after parented manager creation"
);
assert_eq!(service.task_group().task_count(), 1);
manager.shutdown().await;
let report = service.task_group().shutdown(Duration::from_secs(1)).await;
assert!(report.is_healthy(), "{}", report.to_json());
assert_eq!(report.completed, 1, "{}", report.to_json());
assert!(report.children.is_empty(), "{}", report.to_json());
}
#[test]
fn expired_items_are_collected_without_holding_remove_lock() {
let item = Arc::new(StatisticsItem::new("kind", "key", vec!["count"]).expect("valid statistics item"));
let stats_table: StatsTable = Arc::new(RwLock::new(HashMap::from([(
"kind".to_string(),
HashMap::from([("key".to_string(), item.clone())]),
)])));
let kind_meta_map = Arc::new(RwLock::new(HashMap::from([(
"kind".to_string(),
Arc::new(StatisticsKindMeta::new(
"kind".to_string(),
vec!["count".to_string()],
StatisticsItemScheduledPrinter,
)),
)])));
let expired_items = expired_statistics_items(
&stats_table,
None,
item.last_timestamp() + StatisticsManager::MAX_IDLE_TIME + 1,
);
assert_eq!(expired_items.len(), 1);
for item in expired_items {
remove(item.as_ref(), &stats_table, &kind_meta_map);
}
assert!(stats_table.read().get("kind").is_some_and(HashMap::is_empty));
}
#[tokio::test]
async fn cleanup_task_shutdown_awaits_operation_completion() {
let context = RuntimeContext::from_current("statistics-cleanup-test");
let task_group = context
.service_context("statistics-cleanup-worker")
.task_group()
.clone();
let task_group_probe = task_group.clone();
let operation = OperationContext::without_deadline(TaskKind::ScheduledDriver);
task_group
.spawn_operation(&operation, "statistics-cleanup-test-worker", std::future::pending())
.expect("cleanup task test worker should spawn");
let cleanup_task = CleanupTask { task_group, operation };
assert_eq!(task_group_probe.task_count(), 1);
cleanup_task.shutdown(Duration::from_secs(1)).await;
assert_eq!(task_group_probe.task_count(), 0);
let report = task_group_probe.shutdown(Duration::from_secs(1)).await;
assert!(report.is_healthy(), "{}", report.to_json());
assert_eq!(report.completed, 1, "{}", report.to_json());
}
#[tokio::test]
async fn drop_aborts_idle_cleanup_task_and_releases_state() {
let stats_table = {
let manager = StatisticsManager::new(test_parent("statistics-drop-test"));
Arc::downgrade(&manager.stats_table)
};
tokio::time::timeout(Duration::from_secs(1), async {
while stats_table.upgrade().is_some() {
tokio::task::yield_now().await;
}
})
.await
.expect("dropping StatisticsManager should release cleanup task state");
}
}