add backend for history

This commit is contained in:
2024-12-30 20:13:03 -05:00
parent c7ca250b66
commit aa1763cbe7
11 changed files with 319 additions and 28 deletions

View File

@@ -0,0 +1,195 @@
use crate::telemetry::data_value::TelemetryDataValue;
use chrono::{DateTime, SecondsFormat, TimeDelta, Utc};
use std::collections::VecDeque;
use std::sync::RwLock;
use log::error;
use crate::telemetry::data_item::TelemetryDataItem;
struct SegmentData {
values: Vec<TelemetryDataValue>,
timestamps: Vec<DateTime<Utc>>
}
struct HistorySegment {
start: DateTime<Utc>,
end: DateTime<Utc>,
data: RwLock<SegmentData>,
}
impl HistorySegment {
fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
Self {
start,
end,
data: RwLock::new(SegmentData {
values: vec![],
timestamps: vec![],
}),
}
}
fn insert(&self, value: TelemetryDataValue, timestamp: DateTime<Utc>) {
if timestamp < self.start || timestamp >= self.end {
return;
}
let mut data = self.data.write().unwrap_or_else(|err| {
error!("HistorySegment::insert - data was poisoned: {}", err);
let lock = err.into_inner();
self.data.clear_poison();
lock
});
// Find the point where we should insert this item
let index = data.timestamps.partition_point(|item| item <= &timestamp);
// Insert the item
data.timestamps.insert(index, timestamp);
data.values.insert(index, value);
}
fn get(&self, from: DateTime<Utc>, to: DateTime<Utc>, maximum_resolution: TimeDelta) -> (DateTime<Utc>, Vec<TelemetryDataItem>) {
let mut result = vec![];
let mut next_from = from;
if from < self.end && self.start < to { // If there is overlap with the range
let data = self.data.read().unwrap_or_else(|err| {
error!("HistorySegment::get - data was poisoned: {}", err);
let lock = err.into_inner();
self.data.clear_poison();
lock
});
let start = data.timestamps.partition_point(|x| x < &from);
let end = data.timestamps.partition_point(|x| x < &to);
if start < data.timestamps.len() {
let t = data.timestamps[start];
result.push(TelemetryDataItem {
value: data.values[start].clone(),
timestamp: t.to_rfc3339_opts(SecondsFormat::Millis, true),
});
for i in (start + 1)..end {
let t = data.timestamps[i];
if t >= next_from {
let time_since_next_from = t - next_from;
next_from = match (time_since_next_from.num_nanoseconds(), maximum_resolution.num_nanoseconds()) {
(Some(nanos_since_next_from), Some(maximum_resolution_nanos)) => {
let nanos_since_next_from = nanos_since_next_from as u64;
let maximum_resolution_nanos = maximum_resolution_nanos as u64;
let num_steps = nanos_since_next_from.div_ceil(maximum_resolution_nanos);
let subsec_nanos = maximum_resolution.subsec_nanos() as u64;
// This will break once this can't be represented in 2^63 nanoseconds (over 200 years)
let seconds = ((maximum_resolution.num_seconds() as u64) * num_steps) as i64;
let nanos = (num_steps * subsec_nanos) as i64;
next_from + TimeDelta::seconds(seconds) + TimeDelta::nanoseconds(nanos)
},
_ => t + maximum_resolution, // If there is a gap so big it can't be represented in 2^63 nanoseconds (over 200 years) just skip forward
};
result.push(TelemetryDataItem {
value: data.values[i].clone(),
timestamp: t.to_rfc3339_opts(SecondsFormat::Millis, true),
});
}
}
}
}
(next_from, result)
}
}
pub struct TelemetryHistory {
segments: RwLock<VecDeque<HistorySegment>>,
}
impl TelemetryHistory {
pub fn new() -> Self {
Self {
segments: RwLock::new(VecDeque::new()),
}
}
pub fn insert(&self, service: &TelemetryHistoryService, value: TelemetryDataValue, timestamp: DateTime<Utc>) {
let segments = self.segments.read().unwrap_or_else(|err| {
error!("TelemetryHistory::insert - segments was poisoned: {}", err);
let lock = err.into_inner();
self.segments.clear_poison();
lock
});
let segments = if segments.is_empty() || segments[segments.len() - 1].end < timestamp {
// We want to insert something that doesn't fit into our history
drop(segments);
let mut segments = self.segments.write().unwrap_or_else(|err| {
error!("TelemetryHistory::insert - segments was poisoned: {}", err);
let lock = err.into_inner();
self.segments.clear_poison();
lock
});
if segments.len() == 0 {
segments.push_back(HistorySegment::new(timestamp, timestamp + service.segment_width));
} else {
while segments[segments.len() - 1].end < timestamp {
if segments.len() == service.max_segments {
let _ = segments.pop_front();
}
let start_time = segments[segments.len() - 1].end;
segments.push_back(HistorySegment::new(start_time, start_time + service.segment_width));
}
}
drop(segments);
self.segments.read().unwrap_or_else(|err| {
error!("TelemetryHistory::insert - segments was poisoned: {}", err);
let lock = err.into_inner();
self.segments.clear_poison();
lock
})
} else {
segments
};
// Get the index of the first segment which has an end time AFTER the above timestamp
let segment_index = segments.partition_point(|segment| segment.end < timestamp);
segments[segment_index].insert(value, timestamp);
}
pub fn get(&self, from: DateTime<Utc>, to: DateTime<Utc>, maximum_resolution: TimeDelta) -> Vec<TelemetryDataItem> {
let mut result = vec![];
let segments = self.segments.read().unwrap_or_else(|err| {
error!("TelemetryHistory::get - segments was poisoned: {}", err);
let lock = err.into_inner();
self.segments.clear_poison();
lock
});
let mut from = from;
for i in 0..segments.len() {
let (new_from, new_data) = segments[i].get(from, to, maximum_resolution);
from = new_from;
result.extend(new_data);
}
result
}
}
pub struct TelemetryHistoryService {
segment_width: TimeDelta,
max_segments: usize
}
impl TelemetryHistoryService {
pub fn new() -> Self {
Self {
segment_width: TimeDelta::minutes(1),
max_segments: 5
}
}
}