Refactor so that no static is necessary for the maps cache

* Replace the lazy `once_cell` by a maps handle type
* Use Rocket's managed state to manage a handle
* Ensure that the handlers have access to it
* Pass another handle to the maps updater loop
* Try to keep the lock as short as possible

Still, long downloads block the lock. Add a FIXME to refactor this
so the lock is only taken when updating the maps fields.
This commit is contained in:
Paul van Tilburg 2022-02-12 21:35:58 +01:00
parent 72fe9577bd
commit 9b9b1a5f77
Signed by: paul
GPG Key ID: C6DE073EDA9EEC4D
4 changed files with 36 additions and 20 deletions

1
Cargo.lock generated
View File

@ -1942,7 +1942,6 @@ dependencies = [
"color-eyre",
"geocoding",
"image",
"once_cell",
"reqwest 0.11.9",
"rocket",
]

View File

@ -8,6 +8,5 @@ chrono = "0.4.19"
color-eyre = "0.5.6"
geocoding = "0.3.1"
image = "0.24.0"
once_cell = "1.9.0"
reqwest = "0.11.9"
rocket = { version = "0.5.0-rc.1", features = ["json"] }

View File

@ -11,21 +11,22 @@
)]
#![deny(missing_docs)]
use std::sync::Arc;
use color_eyre::Result;
use geocoding::{Forward, Openstreetmap, Point};
use once_cell::sync::Lazy;
use rocket::serde::json::Json;
use rocket::serde::Serialize;
use rocket::tokio::sync::Mutex;
use rocket::tokio::{self, select};
use rocket::{get, routes, FromFormField};
use rocket::{get, routes, FromFormField, State};
use self::maps::Maps;
mod maps;
/// Global maps cache refreshed by a separate task.
static MAPS: Lazy<Mutex<Maps>> = Lazy::new(|| Mutex::new(Maps::new()));
/// A handle to access the in-memory cached maps.
type MapsHandle = Arc<Mutex<Maps>>;
/// The current for a specific location.
///
@ -129,7 +130,12 @@ impl Metric {
/// Calculates and returns the forecast.
///
/// The provided list `metrics` determines what will be included in the forecast.
async fn forecast(lat: f64, lon: f64, metrics: Vec<Metric>) -> Forecast {
async fn forecast(
lat: f64,
lon: f64,
metrics: Vec<Metric>,
_maps_handle: &State<MapsHandle>,
) -> Forecast {
let mut forecast = Forecast::new(lat, lon);
// Expand the `All` metric if present, deduplicate otherwise.
@ -173,17 +179,26 @@ async fn address_position(address: String) -> Option<(f64, f64)> {
/// Handler for retrieving the forecast for an address.
#[get("/forecast?<address>&<metrics>")]
async fn forecast_address(address: String, metrics: Vec<Metric>) -> Option<Json<Forecast>> {
async fn forecast_address(
address: String,
metrics: Vec<Metric>,
maps_handle: &State<MapsHandle>,
) -> Option<Json<Forecast>> {
let (lat, lon) = address_position(address).await?;
let forecast = forecast(lat, lon, metrics).await;
let forecast = forecast(lat, lon, metrics, maps_handle).await;
Some(Json(forecast))
}
/// Handler for retrieving the forecast for a geocoded position.
#[get("/forecast?<lat>&<lon>&<metrics>", rank = 2)]
async fn forecast_geo(lat: f64, lon: f64, metrics: Vec<Metric>) -> Json<Forecast> {
let forecast = forecast(lat, lon, metrics).await;
async fn forecast_geo(
lat: f64,
lon: f64,
metrics: Vec<Metric>,
maps_handle: &State<MapsHandle>,
) -> Json<Forecast> {
let forecast = forecast(lat, lon, metrics, maps_handle).await;
Json(forecast)
}
@ -193,14 +208,17 @@ async fn forecast_geo(lat: f64, lon: f64, metrics: Vec<Metric>) -> Json<Forecast
async fn main() -> Result<()> {
color_eyre::install()?;
let maps = Maps::new();
let maps_handle = Arc::new(Mutex::new(maps));
let maps_updater = tokio::spawn(maps::run(Arc::clone(&maps_handle)));
let rocket = rocket::build()
.manage(maps_handle)
.mount("/", routes![forecast_address, forecast_geo])
.ignite()
.await?;
let shutdown = rocket.shutdown();
let maps_updater = tokio::spawn(maps::run());
select! {
result = rocket.launch() => {
result?

View File

@ -7,7 +7,7 @@ use chrono::DurationRound;
use image::DynamicImage;
use rocket::tokio::time::{sleep, Duration, Instant};
use crate::MAPS;
use crate::MapsHandle;
/// The interval between map refreshes (in seconds).
const SLEEP_INTERVAL: Duration = Duration::from_secs(60);
@ -37,14 +37,14 @@ const UVI_BASE_URL: &str = "https://image.buienradar.nl/2.0/image/sprite/Weather
const UVI_INTERVAL: Duration = Duration::from_secs(3600);
/// Runs a loop that keeps refreshing the maps when necessary.
pub(crate) async fn run() -> ! {
pub(crate) async fn run(maps_handle: MapsHandle) -> ! {
loop {
let mut maps = MAPS.lock().await;
println!("🕔 Refreshing the maps (if necessary)...");
maps.refresh_precipitation().await;
maps.refresh_pollen().await;
maps.refresh_uvi().await;
// FIXME: Refactor this so that the lock is only held when updating the maps fields.
maps_handle.lock().await.refresh_precipitation().await;
maps_handle.lock().await.refresh_pollen().await;
maps_handle.lock().await.refresh_uvi().await;
sleep(SLEEP_INTERVAL).await;
}