Add result caching to solar api months endpoint
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
use chrono;
|
use chrono;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use shared_api_lib::year_month::{YearMonth, YearMonthRange};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::RwLock;
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
use warp::http::Response;
|
use warp::http::Response;
|
||||||
|
|
||||||
@@ -98,6 +100,8 @@ async fn serve(configuration: &std::sync::Arc<CommandLineArgs>) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let month_cache: std::sync::Arc<RwLock<Vec<CachedMonthsResponseItem>>> =
|
||||||
|
std::sync::Arc::new(RwLock::new(Vec::new()));
|
||||||
let months = warp::get()
|
let months = warp::get()
|
||||||
.and(warp::path("months"))
|
.and(warp::path("months"))
|
||||||
.and(warp::query::<HashMap<String, String>>())
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
@@ -105,8 +109,14 @@ async fn serve(configuration: &std::sync::Arc<CommandLineArgs>) {
|
|||||||
let configuration = configuration.clone();
|
let configuration = configuration.clone();
|
||||||
move || configuration.clone()
|
move || configuration.clone()
|
||||||
}))
|
}))
|
||||||
|
.and(warp::any().map({
|
||||||
|
let month_cache = month_cache.clone();
|
||||||
|
move || month_cache.clone()
|
||||||
|
}))
|
||||||
.map(
|
.map(
|
||||||
|query: HashMap<String, String>, configuration: std::sync::Arc<CommandLineArgs>| {
|
|query: HashMap<String, String>,
|
||||||
|
configuration: std::sync::Arc<CommandLineArgs>,
|
||||||
|
month_cache| {
|
||||||
let maybe_start =
|
let maybe_start =
|
||||||
shared_api_lib::query_helpers::try_parse_query_month_year(query.get("start"));
|
shared_api_lib::query_helpers::try_parse_query_month_year(query.get("start"));
|
||||||
if maybe_start.is_none() {
|
if maybe_start.is_none() {
|
||||||
@@ -131,7 +141,8 @@ async fn serve(configuration: &std::sync::Arc<CommandLineArgs>) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let json = get_months_solar_json(&start, &stop, &configuration.database_path);
|
let json =
|
||||||
|
get_months_solar_json(&start, &stop, &configuration.database_path, month_cache);
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(json);
|
.body(json);
|
||||||
@@ -241,22 +252,94 @@ struct DaysResponseItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_months_solar_json(
|
fn get_months_solar_json(
|
||||||
start: &shared_api_lib::year_month::YearMonth,
|
start: &YearMonth,
|
||||||
stop: &shared_api_lib::year_month::YearMonth,
|
stop: &YearMonth,
|
||||||
database_path: &str,
|
database_path: &str,
|
||||||
|
month_cache: std::sync::Arc<RwLock<Vec<CachedMonthsResponseItem>>>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let summaries = database::get_month_solar_summaries(&start, &stop, &database_path);
|
|
||||||
let mut response_model = MonthsResponse {
|
let mut response_model = MonthsResponse {
|
||||||
month_logs: Vec::new(),
|
month_logs: YearMonthRange::new(start, stop)
|
||||||
|
.unwrap()
|
||||||
|
.map(|ym| MonthsResponseItem {
|
||||||
|
year: ym.year,
|
||||||
|
month: ym.month,
|
||||||
|
envoy_total_watts: 0,
|
||||||
|
zever_total_watts: 0,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for summary in summaries {
|
let cached_months = get_cached_months_solar_json(&start, &stop, &month_cache);
|
||||||
response_model.month_logs.push(MonthsResponseItem {
|
let mut missing_months;
|
||||||
year: summary.year,
|
if cached_months.len() > 0 {
|
||||||
month: summary.month,
|
missing_months = Vec::new();
|
||||||
envoy_total_watts: summary.envoy_total_watts,
|
let mut cached_month_next_index = 0;
|
||||||
zever_total_watts: summary.zever_total_watts,
|
for month in response_model.month_logs.iter_mut() {
|
||||||
});
|
if cached_month_next_index < cached_months.len() {
|
||||||
|
let cached_month = &cached_months[cached_month_next_index];
|
||||||
|
if month.year == cached_month.year && month.month == cached_month.month {
|
||||||
|
month.envoy_total_watts = cached_month.envoy_total_watts;
|
||||||
|
month.zever_total_watts = cached_month.zever_total_watts;
|
||||||
|
|
||||||
|
cached_month_next_index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missing_months.push(YearMonth {
|
||||||
|
year: month.year,
|
||||||
|
month: month.month,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missing_months = response_model
|
||||||
|
.month_logs
|
||||||
|
.iter()
|
||||||
|
.map(|i| YearMonth {
|
||||||
|
year: i.year,
|
||||||
|
month: i.month,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if missing_months.len() > 0 {
|
||||||
|
let missing_ranges = compact_missing_dates_to_ranges(&missing_months);
|
||||||
|
for range in missing_ranges {
|
||||||
|
let range_start = range.start();
|
||||||
|
let summaries =
|
||||||
|
get_month_summary_range_from_database(&range_start, &range.stop(), &database_path);
|
||||||
|
|
||||||
|
let mut response_item_index = response_model
|
||||||
|
.month_logs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(index, item)| {
|
||||||
|
if item.year == range_start.year && item.month == range_start.month {
|
||||||
|
return Some(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
for summary in summaries.iter() {
|
||||||
|
let mut response_model_item = &mut response_model.month_logs[response_item_index];
|
||||||
|
// Database may return only partial results, i.e. not all dates
|
||||||
|
// for the range may be returned. Hence we need to check if the
|
||||||
|
// database result is for the same year month.
|
||||||
|
while summary.year != response_model_item.year
|
||||||
|
|| summary.month != response_model_item.month
|
||||||
|
{
|
||||||
|
response_item_index += 1;
|
||||||
|
response_model_item = &mut response_model.month_logs[response_item_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
response_model_item.envoy_total_watts = summary.envoy_total_watts;
|
||||||
|
response_model_item.zever_total_watts = summary.zever_total_watts;
|
||||||
|
response_item_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_missing_entries_to_month_cache(&summaries, &month_cache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match serde_json::to_string(&response_model) {
|
match serde_json::to_string(&response_model) {
|
||||||
@@ -273,6 +356,153 @@ fn get_months_solar_json(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_month_summary_range_from_database(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
database_path: &str,
|
||||||
|
) -> Vec<MonthsResponseItem> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let summaries = database::get_month_solar_summaries(&start, &stop, &database_path);
|
||||||
|
for summary in summaries.iter() {
|
||||||
|
results.push(MonthsResponseItem {
|
||||||
|
year: summary.year,
|
||||||
|
month: summary.month,
|
||||||
|
envoy_total_watts: summary.envoy_total_watts,
|
||||||
|
zever_total_watts: summary.zever_total_watts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cached_months_solar_json(
|
||||||
|
start: &YearMonth,
|
||||||
|
stop: &YearMonth,
|
||||||
|
month_cache: &std::sync::Arc<RwLock<Vec<CachedMonthsResponseItem>>>,
|
||||||
|
) -> Vec<MonthsResponseItem> {
|
||||||
|
if let Ok(cache) = month_cache.read() {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for item in cache
|
||||||
|
.iter()
|
||||||
|
.filter(|i| i.year_month >= *start && i.year_month <= *stop)
|
||||||
|
{
|
||||||
|
results.push(MonthsResponseItem {
|
||||||
|
year: item.year_month.year,
|
||||||
|
month: item.year_month.month,
|
||||||
|
envoy_total_watts: item.envoy_total_watts,
|
||||||
|
zever_total_watts: item.zever_total_watts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Retrieved {} entries by month summary cache for {} to {} range",
|
||||||
|
results.len(),
|
||||||
|
start,
|
||||||
|
stop
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to acquire read lock for month cache");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compact_missing_dates_to_ranges(missing_year_months: &[YearMonth]) -> Vec<YearMonthRange> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
if missing_year_months.len() < 1 {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut start_index = 0;
|
||||||
|
let mut current_year_month = missing_year_months[start_index].clone();
|
||||||
|
for (index, next_year_month) in missing_year_months.iter().skip(1).enumerate() {
|
||||||
|
if current_year_month.get_next_month() != *next_year_month {
|
||||||
|
results.push(
|
||||||
|
YearMonthRange::new(&missing_year_months[start_index], ¤t_year_month)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
start_index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_year_month = next_year_month.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_last_result_entry = results.last();
|
||||||
|
if maybe_last_result_entry.is_none()
|
||||||
|
|| maybe_last_result_entry.unwrap().stop() < *missing_year_months.last().unwrap()
|
||||||
|
{
|
||||||
|
results.push(
|
||||||
|
YearMonthRange::new(&missing_year_months[start_index], ¤t_year_month).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_missing_entries_to_month_cache(
|
||||||
|
summaries: &[MonthsResponseItem],
|
||||||
|
month_cache: &RwLock<Vec<CachedMonthsResponseItem>>,
|
||||||
|
) {
|
||||||
|
if summaries.len() < 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(mut cache) = month_cache.write() {
|
||||||
|
let first_summary_year_month = YearMonth {
|
||||||
|
year: summaries[0].year,
|
||||||
|
month: summaries[0].month,
|
||||||
|
};
|
||||||
|
let mut summary_insertion_index = cache
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(index, item)| {
|
||||||
|
if item.year_month > first_summary_year_month {
|
||||||
|
return Some(index - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
let current_year_month = YearMonth::today();
|
||||||
|
|
||||||
|
for summary in summaries.iter() {
|
||||||
|
let summary_year_month = YearMonth {
|
||||||
|
year: summary.year,
|
||||||
|
month: summary.month,
|
||||||
|
};
|
||||||
|
// Do not cache results that are subject to change, such as the
|
||||||
|
// current month or any future month
|
||||||
|
if summary_year_month >= current_year_month {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let to_insert = CachedMonthsResponseItem {
|
||||||
|
year_month: summary_year_month,
|
||||||
|
envoy_total_watts: summary.envoy_total_watts,
|
||||||
|
zever_total_watts: summary.zever_total_watts,
|
||||||
|
};
|
||||||
|
if summary_insertion_index < cache.len() {
|
||||||
|
while cache[summary_insertion_index].year_month < summary_year_month {
|
||||||
|
summary_insertion_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary_insertion_index < cache.len() {
|
||||||
|
cache.insert(summary_insertion_index, to_insert);
|
||||||
|
summary_insertion_index += 1;
|
||||||
|
} else {
|
||||||
|
cache.push(to_insert);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache.push(to_insert);
|
||||||
|
summary_insertion_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to acquire write lock for month cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct MonthsResponse {
|
struct MonthsResponse {
|
||||||
@@ -287,3 +517,10 @@ struct MonthsResponseItem {
|
|||||||
envoy_total_watts: i32,
|
envoy_total_watts: i32,
|
||||||
zever_total_watts: i32,
|
zever_total_watts: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct CachedMonthsResponseItem {
|
||||||
|
year_month: YearMonth,
|
||||||
|
envoy_total_watts: i32,
|
||||||
|
zever_total_watts: i32,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user