增加rtr一些必要的api接口

This commit is contained in:
xiuting.xu
2026-06-23 17:04:00 +08:00
parent 303bdffd97
commit b105b3b7d0
41 changed files with 3078 additions and 1735 deletions

View File

@@ -10,7 +10,7 @@ use serde::Serialize;
use tracing::warn;
use crate::rtr::cache::{CacheAvailability, CacheMemoryStats, SharedRtrCache, VersionReportStats};
use crate::rtr::config::format_timezone;
use crate::rtr::config::{RuntimeConfig, format_timezone};
use crate::rtr::server::{RtrNotifier, RtrServiceStats, RtrTransportConnectionCounts};
use crate::source::pipeline::{
DataQualityReport, FileFingerprint, SourceFingerprint, SourceLoadReport,
@@ -73,8 +73,7 @@ impl ReportConfiguration {
pub struct ReportContext {
started_at: DateTime<Utc>,
started_instant: Instant,
timezone: Tz,
configuration: ReportConfiguration,
configuration: Arc<RwLock<ReportConfiguration>>,
runtime: Arc<RwLock<RuntimeReportState>>,
}
@@ -148,16 +147,38 @@ impl Default for RefreshReport {
impl ReportContext {
pub fn new(configuration: ReportConfiguration) -> Self {
let timezone = configuration.timezone();
Self {
started_at: Utc::now(),
started_instant: Instant::now(),
timezone,
configuration,
configuration: Arc::new(RwLock::new(configuration)),
runtime: Arc::new(RwLock::new(RuntimeReportState::default())),
}
}
pub fn update_runtime_config(&self, config: &RuntimeConfig) {
let timezone = config
.timezone()
.expect("runtime config timezone should be validated before report update");
let mut configuration = self
.configuration
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*configuration = ReportConfiguration::new(
config.source_refresh_interval_seconds,
config.runtime_report_interval_seconds,
config.report_history_limit,
config.max_delta,
config.prune_delta_by_snapshot_size,
config.strict_ccr_validation,
timezone,
(
config.timing.refresh,
config.timing.retry,
config.timing.expire,
),
);
}
pub fn record_refresh_success(
&self,
attempted_at: DateTime<Utc>,
@@ -365,6 +386,12 @@ impl ReportContext {
service_stats: &RtrServiceStats,
) -> ReportParts {
let cache = shared_cache.load_full();
let configuration = self
.configuration
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone();
let timezone = configuration.timezone();
let runtime = self
.runtime
.read()
@@ -377,14 +404,14 @@ impl ReportContext {
let active_connections = service_stats.active_connections();
let connections_by_transport = service_stats.transport_connections();
let max_connections = service_stats.max_connections();
let generated_at = self.report_now();
let generated_at = self.report_now(timezone);
let metadata = ReportMetadata {
schema_version: 2,
generated_at,
phase: phase.to_string(),
};
let service = ServiceReport {
started_at: self.to_report_time(self.started_at),
started_at: self.to_report_time(self.started_at, timezone),
uptime_seconds: self.started_instant.elapsed().as_secs(),
active_connections,
connections_by_transport: TransportConnectionReport::from(connections_by_transport),
@@ -397,13 +424,13 @@ impl ReportContext {
};
let source = runtime
.source
.map(|source| SourceLoadReportView::from_report(source, self.timezone));
let refresh = RefreshReportView::from_report(runtime.refresh, self.timezone);
.map(|source| SourceLoadReportView::from_report(source, timezone));
let refresh = RefreshReportView::from_report(runtime.refresh, timezone);
let cache = CacheReport {
availability,
created_at: self.to_report_time(cache.created_at().utc()),
last_update_begin: self.to_report_time(cache.last_update_begin().utc()),
last_update_end: self.to_report_time(cache.last_update_end().utc()),
created_at: self.to_report_time(cache.created_at().utc(), timezone),
last_update_begin: self.to_report_time(cache.last_update_begin().utc(), timezone),
last_update_end: self.to_report_time(cache.last_update_end().utc(), timezone),
memory: cache.memory_stats(),
versions: cache.version_report_stats(),
};
@@ -414,7 +441,7 @@ impl ReportContext {
phase: metadata.phase.clone(),
source,
source_fingerprint: runtime.source_fingerprint.map(|fingerprint| {
SourceFingerprintReport::from_fingerprint(fingerprint, self.timezone)
SourceFingerprintReport::from_fingerprint(fingerprint, timezone)
}),
refresh,
data_quality: runtime.data_quality,
@@ -440,7 +467,7 @@ impl ReportContext {
phase: metadata.phase,
service,
process,
configuration: self.configuration.clone(),
configuration,
};
let suffix = report_file_suffix(generated_at);
@@ -462,7 +489,7 @@ impl ReportContext {
report_dir,
"rtr-source",
suffix,
self.configuration.report_history_limit,
self.report_history_limit(),
report,
)
}
@@ -477,7 +504,7 @@ impl ReportContext {
report_dir,
"rtr-clients",
suffix,
self.configuration.report_history_limit,
self.report_history_limit(),
report,
)
}
@@ -492,17 +519,24 @@ impl ReportContext {
report_dir,
"rtr-runtime",
suffix,
self.configuration.report_history_limit,
self.report_history_limit(),
report,
)
}
fn report_now(&self) -> DateTime<Tz> {
self.to_report_time(Utc::now())
fn report_history_limit(&self) -> usize {
self.configuration
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.report_history_limit
}
fn to_report_time(&self, time: DateTime<Utc>) -> DateTime<Tz> {
time.with_timezone(&self.timezone)
fn report_now(&self, timezone: Tz) -> DateTime<Tz> {
self.to_report_time(Utc::now(), timezone)
}
fn to_report_time(&self, time: DateTime<Utc>, timezone: Tz) -> DateTime<Tz> {
time.with_timezone(&timezone)
}
}
@@ -745,308 +779,3 @@ fn prune_rolling_reports(report_dir: &Path, prefix: &str, keep: usize) -> Result
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::rtr::cache::RtrCache;
use crate::rtr::server::RtrService;
use crate::source::pipeline::{
DataQualityReport, PayloadTypeCounts, SlurmRuleCounts, SourceLoadReport,
};
use arc_swap::ArcSwap;
use serde_json::Value;
use super::*;
fn test_configuration() -> ReportConfiguration {
ReportConfiguration::new(
300,
300,
10,
100,
false,
false,
chrono_tz::Asia::Shanghai,
(3600, 600, 7200),
)
}
#[test]
fn write_report_creates_parseable_json() {
let temp = tempfile::tempdir().unwrap();
let report_dir = temp.path().join("report");
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
let service = RtrService::new(shared_cache.clone());
let notifier = service.notifier();
let context = ReportContext::new(test_configuration());
context
.write(
&report_dir,
"test",
&shared_cache,
&notifier,
&service.stats(),
)
.unwrap();
let source_report = read_single_report(&report_dir, "rtr-source");
assert_eq!(source_report["schema_version"], 2);
assert_eq!(source_report["phase"], "test");
assert_report_time_offset(&source_report["generated_at"]);
assert_report_time_offset(&source_report["cache"]["created_at"]);
assert_eq!(source_report["cache"]["availability"], "ready");
assert_eq!(source_report["refresh"]["status"], "not_attempted");
assert!(source_report["source"].is_null());
assert!(source_report["data_quality"].is_null());
let runtime_report = read_single_report(&report_dir, "rtr-runtime");
assert_report_time_offset(&runtime_report["service"]["started_at"]);
assert_eq!(
runtime_report["configuration"]["source_refresh_interval_seconds"],
300
);
assert_eq!(runtime_report["configuration"]["report_history_limit"], 10);
let clients_report = read_single_report(&report_dir, "rtr-clients");
assert_eq!(clients_report["service"]["max_connections"], 1024);
assert_eq!(clients_report["service"]["active_connections"], 0);
assert_eq!(
clients_report["service"]["connections_by_transport"]["tcp"],
0
);
assert_eq!(
clients_report["service"]["connections_by_transport"]["tls"],
0
);
assert_eq!(
clients_report["service"]["connections_by_transport"]["ssh"],
0
);
assert_eq!(
source_report["cache"]["versions"].as_array().unwrap().len(),
3
);
assert_eq!(
source_report["cache"]["versions"][2]["snapshot"]["total"],
0
);
assert_eq!(
source_report["cache"]["memory"]["delta_payload_counts"][2],
0
);
assert!(source_report["source_fingerprint"].is_null());
assert_no_temporary_reports(&report_dir);
}
#[test]
fn refresh_failure_preserves_last_successful_source_data() {
let temp = tempfile::tempdir().unwrap();
let report_dir = temp.path().join("report");
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
let service = RtrService::new(shared_cache.clone());
let notifier = service.notifier();
let context = ReportContext::new(test_configuration());
let source = SourceLoadReport {
ccr_file: "data/example.ccr".to_string(),
ccr_file_size_bytes: 123,
ccr_modified_at: Some(Utc::now()),
ccr_produced_at: Some("20260615000000Z".to_string()),
slurm_enabled: true,
slurm_file_count: 1,
slurm_files: vec!["policy.slurm".to_string()],
slurm_version: Some(2),
};
let quality = DataQualityReport {
ccr_input: PayloadTypeCounts {
total: 11,
vrp: 10,
router_key: 0,
aspa: 1,
},
invalid: PayloadTypeCounts::default(),
before_slurm: PayloadTypeCounts {
total: 11,
vrp: 10,
router_key: 0,
aspa: 1,
},
after_slurm: PayloadTypeCounts {
total: 10,
vrp: 9,
router_key: 0,
aspa: 1,
},
slurm_filters: SlurmRuleCounts {
prefix: 1,
router_key: 0,
aspa: 0,
},
slurm_assertions: SlurmRuleCounts::default(),
};
context.record_source_fingerprint(SourceFingerprint {
ccr: FileFingerprint {
path: "data/example.ccr".to_string(),
len: 123,
modified_unix_secs: 1_781_404_800,
},
slurm_files: vec![FileFingerprint {
path: "policy.slurm".to_string(),
len: 42,
modified_unix_secs: 1_781_408_400,
}],
});
context.record_refresh_success(Utc::now(), 12, true, source, quality);
context.record_refresh_failure(Utc::now(), 5, &anyhow::anyhow!("source unavailable"));
context
.write(
&report_dir,
"refresh_failed",
&shared_cache,
&notifier,
&service.stats(),
)
.unwrap();
let report = read_single_report(&report_dir, "rtr-source");
assert_eq!(report["source"]["ccr_file"], "data/example.ccr");
assert_eq!(
report["source_fingerprint"]["ccr"]["path"],
"data/example.ccr"
);
assert_eq!(report["source_fingerprint"]["ccr"]["len"], 123);
assert_report_time_offset(&report["source_fingerprint"]["ccr"]["modified_at"]);
assert_eq!(
report["source_fingerprint"]["slurm_files"][0]["path"],
"policy.slurm"
);
assert_report_time_offset(&report["source"]["ccr_modified_at"]);
assert_eq!(report["data_quality"]["after_slurm"]["total"], 10);
assert_eq!(report["refresh"]["status"], "failed");
assert_eq!(report["refresh"]["consecutive_failures"], 1);
assert_eq!(report["refresh"]["last_error"], "source unavailable");
assert!(!report["refresh"]["last_success_at"].is_null());
assert_report_time_offset(&report["refresh"]["last_success_at"]);
}
#[test]
fn rolling_reports_keep_latest_files_per_category() {
let temp = tempfile::tempdir().unwrap();
let report_dir = temp.path().join("report");
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
let service = RtrService::new(shared_cache.clone());
let notifier = service.notifier();
let mut configuration = test_configuration();
configuration.report_history_limit = 2;
let context = ReportContext::new(configuration);
for phase in ["one", "two", "three"] {
context
.write(
&report_dir,
phase,
&shared_cache,
&notifier,
&service.stats(),
)
.unwrap();
}
assert_eq!(report_files(&report_dir, "rtr-source").len(), 2);
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 2);
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 2);
assert_no_temporary_reports(&report_dir);
}
#[test]
fn category_writes_only_create_requested_report_type() {
let temp = tempfile::tempdir().unwrap();
let report_dir = temp.path().join("report");
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
let service = RtrService::new(shared_cache.clone());
let notifier = service.notifier();
let context = ReportContext::new(test_configuration());
context
.write_source(
&report_dir,
"source_only",
&shared_cache,
&notifier,
&service.stats(),
)
.unwrap();
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 0);
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 0);
context
.write_clients(
&report_dir,
"clients_only",
&shared_cache,
&notifier,
&service.stats(),
)
.unwrap();
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 1);
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 0);
context
.write_runtime(
&report_dir,
"runtime_only",
&shared_cache,
&notifier,
&service.stats(),
)
.unwrap();
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 1);
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 1);
}
fn assert_report_time_offset(value: &Value) {
let value = value.as_str().expect("report time should be a string");
assert!(
value.ends_with("+08:00"),
"report time should use +08:00 offset, got {value}"
);
}
fn read_single_report(report_dir: &Path, prefix: &str) -> Value {
let files = report_files(report_dir, prefix);
assert_eq!(files.len(), 1, "expected one {prefix} report");
serde_json::from_slice(&fs::read(&files[0]).unwrap()).unwrap()
}
fn report_files(report_dir: &Path, prefix: &str) -> Vec<PathBuf> {
let start = format!("{prefix}-");
let mut files = fs::read_dir(report_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(&start) && name.ends_with(".json"))
})
.collect::<Vec<_>>();
files.sort();
files
}
fn assert_no_temporary_reports(report_dir: &Path) {
let has_temporary = fs::read_dir(report_dir).unwrap().any(|entry| {
entry
.unwrap()
.file_name()
.to_str()
.is_some_and(|name| name.ends_with(".tmp"))
});
assert!(!has_temporary);
}
}