diff --git a/src/solar_api/src/main.rs b/src/solar_api/src/main.rs index 13e5881..28f8ae6 100644 --- a/src/solar_api/src/main.rs +++ b/src/solar_api/src/main.rs @@ -1,6 +1,8 @@ use chrono; use clap::Parser; +use shared_api_lib::year_month::{YearMonth, YearMonthRange}; use std::collections::HashMap; +use std::sync::RwLock; use warp::Filter; use warp::http::Response; @@ -98,6 +100,8 @@ async fn serve(configuration: &std::sync::Arc) { }, ); + let month_cache: std::sync::Arc>> = + std::sync::Arc::new(RwLock::new(Vec::new())); let months = warp::get() .and(warp::path("months")) .and(warp::query::>()) @@ -105,8 +109,14 @@ async fn serve(configuration: &std::sync::Arc) { let configuration = configuration.clone(); move || configuration.clone() })) + .and(warp::any().map({ + let month_cache = month_cache.clone(); + move || month_cache.clone() + })) .map( - |query: HashMap, configuration: std::sync::Arc| { + |query: HashMap, + configuration: std::sync::Arc, + month_cache| { let maybe_start = shared_api_lib::query_helpers::try_parse_query_month_year(query.get("start")); if maybe_start.is_none() { @@ -131,7 +141,8 @@ async fn serve(configuration: &std::sync::Arc) { )); } - 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() .header("Content-Type", "application/json") .body(json); @@ -241,22 +252,94 @@ struct DaysResponseItem { } fn get_months_solar_json( - start: &shared_api_lib::year_month::YearMonth, - stop: &shared_api_lib::year_month::YearMonth, + start: &YearMonth, + stop: &YearMonth, database_path: &str, + month_cache: std::sync::Arc>>, ) -> String { - let summaries = database::get_month_solar_summaries(&start, &stop, &database_path); 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 { - response_model.month_logs.push(MonthsResponseItem { - year: summary.year, - month: summary.month, - envoy_total_watts: summary.envoy_total_watts, - zever_total_watts: summary.zever_total_watts, - }); + let cached_months = get_cached_months_solar_json(&start, &stop, &month_cache); + let mut missing_months; + if cached_months.len() > 0 { + missing_months = Vec::new(); + let mut cached_month_next_index = 0; + 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) { @@ -273,6 +356,153 @@ fn get_months_solar_json( } } +fn get_month_summary_range_from_database( + start: &YearMonth, + stop: &YearMonth, + database_path: &str, +) -> Vec { + 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>>, +) -> Vec { + 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 { + 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>, +) { + 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)] #[serde(rename_all = "camelCase")] struct MonthsResponse { @@ -287,3 +517,10 @@ struct MonthsResponseItem { envoy_total_watts: i32, zever_total_watts: i32, } + +#[derive(Clone)] +struct CachedMonthsResponseItem { + year_month: YearMonth, + envoy_total_watts: i32, + zever_total_watts: i32, +}