增加rtr一些必要的api接口
This commit is contained in:
@@ -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,
|
||||
¬ifier,
|
||||
&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,
|
||||
¬ifier,
|
||||
&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,
|
||||
¬ifier,
|
||||
&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,
|
||||
¬ifier,
|
||||
&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,
|
||||
¬ifier,
|
||||
&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,
|
||||
¬ifier,
|
||||
&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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user