implement caching for vvs api

This commit is contained in:
mark 2025-06-11 19:45:54 +02:00
parent 3550ab1fc4
commit 98a697f576
4 changed files with 745 additions and 385 deletions

751
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
[package] [package]
name = "merklingen_connection_check" name = "merklingen_connection_check"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
html-escape = "0.2.13" html-escape = "0.2.13"
quick-xml = { version = "0.37.2", features = ["serialize"] } quick-xml = { version = "0.37.5", features = ["serialize"] }
reqwest = { version = "0.12.12", features = ["charset", "h2", "http2", "macos-system-configuration", "rustls-tls"], default-features = false } reqwest = { version = "0.12.20", features = ["charset", "h2", "http2", "macos-system-configuration", "rustls-tls"], default-features = false }
rocket = "0.5.1" rocket = "0.5.1"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.138" serde_json = "1.0.140"
tokio = { version = "1.43.0", features = ["full"] } tokio = { version = "1.45.1", features = ["full"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"

View File

@ -1,17 +1,51 @@
mod bahnhof; mod bahnhof;
mod vvs; mod vvs;
use std::{collections::BTreeMap, sync::Arc, time::Duration}; use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
time::Duration,
};
use chrono::{Datelike, Local, TimeDelta, Timelike}; use chrono::{Datelike, Local, TimeDelta, Timelike};
use rocket::{get, http::Status, response::content::RawHtml, routes, FromForm, State}; use rocket::{FromForm, State, get, http::Status, response::content::RawHtml, routes};
use tokio::{ use tokio::{
sync::Mutex, sync::Mutex,
time::{sleep, Instant}, time::{Instant, sleep},
}; };
use crate::vvs::VvsStopIdentifier;
#[rocket::launch] #[rocket::launch]
async fn rocket() -> _ { async fn rocket() -> _ {
let vvs_stop_cache = Arc::new(Mutex::new(HashMap::<
String,
(Instant, Result<Option<(String, VvsStopIdentifier)>, String>),
>::new()));
let vvs_time_cache = Arc::new(Mutex::new(HashMap::<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>::new()));
let vvs_tc = Arc::clone(&vvs_time_cache);
let vvs_sc = Arc::clone(&vvs_stop_cache);
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(60 * 30)).await;
let now = tokio::time::Instant::now();
vvs_tc.lock().await.retain(|_, (t, ..)| *t >= now);
vvs_sc.lock().await.retain(|_, (t, ..)| *t >= now);
}
});
rocket::build() rocket::build()
.manage(Arc::new(())) .manage(Arc::new(()))
.manage(Mutex::new( .manage(Mutex::new(
@ -21,6 +55,8 @@ async fn rocket() -> _ {
String, String,
)>::None, )>::None,
)) ))
.manage(vvs_stop_cache)
.manage(vvs_time_cache)
.mount( .mount(
"/", "/",
routes![ routes![
@ -271,9 +307,15 @@ async fn rate_limit(rate_limit: &State<Arc<()>>) -> Arc<()> {
} }
#[get("/vvs/find_stop/simple/<stop>")] #[get("/vvs/find_stop/simple/<stop>")]
async fn vvs_find_stop_simple(stop: &str, counter: &State<Arc<()>>) -> String { async fn vvs_find_stop_simple(
stop: &str,
counter: &State<Arc<()>>,
vvs_stop_cache: &State<
Arc<Mutex<HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>>>,
>,
) -> String {
let _counter = rate_limit(counter).await; let _counter = rate_limit(counter).await;
match vvs::find_stop_best(stop).await { match vvs::find_stop_best(stop, &mut *vvs_stop_cache.lock().await).await {
Ok(Some((name, _))) => { Ok(Some((name, _))) => {
format!("ok: {name}") format!("ok: {name}")
} }
@ -296,6 +338,29 @@ async fn vvs_short_in_hrs_mins_before_simple(
to: &str, to: &str,
form: VvsShortInHrsMinsBeforeSimple, form: VvsShortInHrsMinsBeforeSimple,
counter: &State<Arc<()>>, counter: &State<Arc<()>>,
vvs_stop_cache: &State<
Arc<Mutex<HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>>>,
>,
vvs_time_cache: &State<
Arc<
Mutex<
HashMap<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>,
>,
>,
>,
) -> (Status, String) { ) -> (Status, String) {
let time = Local::now() + Duration::from_secs_f64(hrs * 60.0 * 60.0); let time = Local::now() + Duration::from_secs_f64(hrs * 60.0 * 60.0);
let form = VvsMinsBeforeSimpleForm { let form = VvsMinsBeforeSimpleForm {
@ -307,7 +372,7 @@ async fn vvs_short_in_hrs_mins_before_simple(
interchanges: form.interchanges, interchanges: form.interchanges,
allowtaxi: form.allowtaxi, allowtaxi: form.allowtaxi,
}; };
vvs_mins_before_simple_impl(from, to, form, counter).await vvs_mins_before_simple_impl(from, to, form, counter, vvs_stop_cache, vvs_time_cache).await
} }
#[derive(FromForm)] #[derive(FromForm)]
struct VvsShortInHrsMinsBeforeCheckSimple { struct VvsShortInHrsMinsBeforeCheckSimple {
@ -324,6 +389,29 @@ async fn vvs_short_in_hrs_mins_before_check_simple(
to: &str, to: &str,
form: VvsShortInHrsMinsBeforeCheckSimple, form: VvsShortInHrsMinsBeforeCheckSimple,
counter: &State<Arc<()>>, counter: &State<Arc<()>>,
vvs_stop_cache: &State<
Arc<Mutex<HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>>>,
>,
vvs_time_cache: &State<
Arc<
Mutex<
HashMap<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>,
>,
>,
>,
) -> (Status, String) { ) -> (Status, String) {
let time = Local::now() + Duration::from_secs_f64(hrs * 60.0 * 60.0); let time = Local::now() + Duration::from_secs_f64(hrs * 60.0 * 60.0);
let form = VvsMinsBeforeCheckSimpleForm { let form = VvsMinsBeforeCheckSimpleForm {
@ -336,7 +424,7 @@ async fn vvs_short_in_hrs_mins_before_check_simple(
minutes: form.minutes, minutes: form.minutes,
allowtaxi: form.allowtaxi, allowtaxi: form.allowtaxi,
}; };
vvs_mins_before_check_simple_impl(from, to, form, counter).await vvs_mins_before_check_simple_impl(from, to, form, counter, vvs_stop_cache, vvs_time_cache).await
} }
#[derive(FromForm)] #[derive(FromForm)]
struct VvsMinsBeforeSimpleForm { struct VvsMinsBeforeSimpleForm {
@ -354,40 +442,87 @@ async fn vvs_mins_before_simple(
to: &str, to: &str,
form: VvsMinsBeforeSimpleForm, form: VvsMinsBeforeSimpleForm,
counter: &State<Arc<()>>, counter: &State<Arc<()>>,
vvs_stop_cache: &State<
Arc<Mutex<HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>>>,
>,
vvs_time_cache: &State<
Arc<
Mutex<
HashMap<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>,
>,
>,
>,
) -> (Status, String) { ) -> (Status, String) {
vvs_mins_before_simple_impl(from, to, form, counter).await vvs_mins_before_simple_impl(from, to, form, counter, vvs_stop_cache, vvs_time_cache).await
} }
async fn vvs_mins_before_simple_impl( async fn vvs_mins_before_simple_impl(
from: &str, from: &str,
to: &str, to: &str,
form: VvsMinsBeforeSimpleForm, form: VvsMinsBeforeSimpleForm,
counter: &State<Arc<()>>, counter: &State<Arc<()>>,
vvs_stop_cache: &State<
Arc<Mutex<HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>>>,
>,
vvs_time_cache: &State<
Arc<
Mutex<
HashMap<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>,
>,
>,
>,
) -> (Status, String) { ) -> (Status, String) {
let _counter = rate_limit(counter).await; let _counter = rate_limit(counter).await;
if Arc::strong_count(&counter) > 2 {} let mut vvs_stop_cache_lock = vvs_stop_cache.lock().await;
let (_, from) = match vvs::find_stop_best(from).await { let (_, from) = match vvs::find_stop_best(from, &mut *vvs_stop_cache_lock).await {
Ok(Some(v)) => v, Ok(Some(v)) => v,
Ok(None) => { Ok(None) => {
return ( return (
Status::NotFound, Status::NotFound,
format!("err: Couldn't find origin stop {from}"), format!("err: Couldn't find origin stop {from}"),
) );
} }
Err(e) => return (Status::InternalServerError, format!("err: {e}")), Err(e) => return (Status::InternalServerError, format!("err: {e}")),
}; };
let (_, to) = match vvs::find_stop_best(to).await { let (_, to) = match vvs::find_stop_best(to, &mut *vvs_stop_cache_lock).await {
Ok(Some(v)) => v, Ok(Some(v)) => v,
Ok(None) => { Ok(None) => {
return ( return (
Status::NotFound, Status::NotFound,
format!("err: Couldn't find destination stop {to}"), format!("err: Couldn't find destination stop {to}"),
) );
} }
Err(e) => return (Status::InternalServerError, format!("err: {e}")), Err(e) => return (Status::InternalServerError, format!("err: {e}")),
}; };
drop(vvs_stop_cache_lock);
match vvs::find_trip_arrival_before( match vvs::find_trip_arrival_before(
&from, from,
&to, to,
form.year, form.year,
form.month, form.month,
form.day, form.day,
@ -395,6 +530,7 @@ async fn vvs_mins_before_simple_impl(
form.minute, form.minute,
form.interchanges, form.interchanges,
form.allowtaxi.is_some_and(|allow| allow), form.allowtaxi.is_some_and(|allow| allow),
&mut *vvs_time_cache.lock().await,
) )
.await .await
{ {
@ -423,39 +559,87 @@ async fn vvs_mins_before_check_simple(
to: &str, to: &str,
form: VvsMinsBeforeCheckSimpleForm, form: VvsMinsBeforeCheckSimpleForm,
counter: &State<Arc<()>>, counter: &State<Arc<()>>,
vvs_stop_cache: &State<
Arc<Mutex<HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>>>,
>,
vvs_time_cache: &State<
Arc<
Mutex<
HashMap<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>,
>,
>,
>,
) -> (Status, String) { ) -> (Status, String) {
vvs_mins_before_check_simple_impl(from, to, form, counter).await vvs_mins_before_check_simple_impl(from, to, form, counter, vvs_stop_cache, vvs_time_cache).await
} }
async fn vvs_mins_before_check_simple_impl( async fn vvs_mins_before_check_simple_impl(
from: &str, from: &str,
to: &str, to: &str,
form: VvsMinsBeforeCheckSimpleForm, form: VvsMinsBeforeCheckSimpleForm,
counter: &State<Arc<()>>, counter: &State<Arc<()>>,
vvs_stop_cache: &State<
Arc<Mutex<HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>>>,
>,
vvs_time_cache: &State<
Arc<
Mutex<
HashMap<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>,
>,
>,
>,
) -> (Status, String) { ) -> (Status, String) {
let _counter = rate_limit(counter).await; let _counter = rate_limit(counter).await;
let (_, from) = match vvs::find_stop_best(from).await { let mut vvs_stop_cache_lock = vvs_stop_cache.lock().await;
let (_, from) = match vvs::find_stop_best(from, &mut *vvs_stop_cache_lock).await {
Ok(Some(v)) => v, Ok(Some(v)) => v,
Ok(None) => { Ok(None) => {
return ( return (
Status::NotFound, Status::NotFound,
format!("err: Couldn't find origin stop {from}"), format!("err: Couldn't find origin stop {from}"),
) );
} }
Err(e) => return (Status::InternalServerError, format!("err: {e}")), Err(e) => return (Status::InternalServerError, format!("err: {e}")),
}; };
let (_, to) = match vvs::find_stop_best(to).await { let (_, to) = match vvs::find_stop_best(to, &mut *vvs_stop_cache_lock).await {
Ok(Some(v)) => v, Ok(Some(v)) => v,
Ok(None) => { Ok(None) => {
return ( return (
Status::NotFound, Status::NotFound,
format!("err: Couldn't find destination stop {to}"), format!("err: Couldn't find destination stop {to}"),
) );
} }
Err(e) => return (Status::InternalServerError, format!("err: {e}")), Err(e) => return (Status::InternalServerError, format!("err: {e}")),
}; };
drop(vvs_stop_cache_lock);
match vvs::find_trip_arrival_before( match vvs::find_trip_arrival_before(
&from, from,
&to, to,
form.year, form.year,
form.month, form.month,
form.day, form.day,
@ -463,16 +647,17 @@ async fn vvs_mins_before_check_simple_impl(
form.minute, form.minute,
form.interchanges, form.interchanges,
form.allowtaxi.is_some_and(|allow| allow), form.allowtaxi.is_some_and(|allow| allow),
&mut *vvs_time_cache.lock().await,
) )
.await .await
{ {
Ok(Some(v)) => { Ok(Some(v)) => {
if v <= form.minutes { if *v <= form.minutes {
(Status::Ok, String::new()) (Status::Ok, String::new())
} else { } else {
( (
Status::Gone, Status::Gone,
format!("gone: {v} > {} minutes", form.minutes), format!("gone: {} > {} minutes", *v, form.minutes),
) )
} }
} }

View File

@ -1,21 +1,57 @@
use std::{collections::HashMap, time::Duration};
use chrono::{DateTime, Local, TimeZone}; use chrono::{DateTime, Local, TimeZone};
use serde::Deserialize; use serde::Deserialize;
use tokio::time::Instant;
#[derive(Debug)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct VvsStopIdentifier { pub struct VvsStopIdentifier {
id: String, id: String,
gid: String, gid: String,
} }
pub async fn find_stop_best(query: &str) -> Result<Option<(String, VvsStopIdentifier)>, String> { pub async fn find_stop_best(
Ok(find_stop(query) query: &str,
cache: &mut HashMap<String, (Instant, Result<Option<(String, VvsStopIdentifier)>, String>)>,
) -> Result<Option<(String, VvsStopIdentifier)>, String> {
if let Some(cache_hit) = cache
.get(query)
.filter(|cache_hit| cache_hit.0 >= Instant::now())
{
cache_hit.1.clone()
} else {
let r = find_stop_best_no_cache(query).await;
cache
.entry(query.to_owned())
.insert_entry((
Instant::now()
// cached value's max age:
+ match &r {
// found stop, unlikely to change
Ok(Some((_, _))) => Duration::from_secs(20 * 60),
// found no stop, not very likely to change either
Ok(None) => Duration::from_secs(5 * 60),
// something went wrong, allow relatively quick retries
Err(_) => Duration::from_secs(30),
},
r,
))
.get()
.1
.clone()
}
}
pub async fn find_stop_best_no_cache(
query: &str,
) -> Result<Option<(String, VvsStopIdentifier)>, String> {
Ok(find_stop_no_cache(query)
.await? .await?
.into_iter() .into_iter()
.filter_map(|(name, _types, identifier)| Some((name, identifier?))) .filter_map(|(name, _types, identifier)| Some((name, identifier?)))
.next()) .next())
} }
pub async fn find_stop( pub async fn find_stop_no_cache(
query: &str, query: &str,
) -> Result< ) -> Result<
Vec<( Vec<(
@ -132,7 +168,79 @@ pub async fn find_stop(
/// of the remaining trips, calculates how many minutes before the requested arrival time the departure time is, and returns the minimum of these durations in minutes. /// of the remaining trips, calculates how many minutes before the requested arrival time the departure time is, and returns the minimum of these durations in minutes.
/// For example, if you request to arrive at 8:50, a trip arrives at 8:45 and departs at 8:40, then this function would return `Some(10)`. /// For example, if you request to arrive at 8:50, a trip arrives at 8:45 and departs at 8:40, then this function would return `Some(10)`.
/// Returns `None` if no valid trips were found. /// Returns `None` if no valid trips were found.
pub async fn find_trip_arrival_before( pub async fn find_trip_arrival_before<'a>(
from: VvsStopIdentifier,
to: VvsStopIdentifier,
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
max_interchanges: Option<u16>,
allow_taxi: bool,
cache: &'a mut HashMap<
(
VvsStopIdentifier,
VvsStopIdentifier,
u16,
u8,
u8,
u8,
u8,
Option<u16>,
bool,
),
(Instant, Result<Option<u32>, String>),
>,
) -> &'a Result<Option<u32>, String> {
let query = (
from,
to,
year,
month,
day,
hour,
minute,
max_interchanges,
allow_taxi,
);
if cache
.get(&query)
.is_none_or(|cache_hit| cache_hit.0 < Instant::now())
{
let r = find_trip_arrival_before_no_cache(
&query.0,
&query.1,
year,
month,
day,
hour,
minute,
max_interchanges,
allow_taxi,
)
.await;
cache.insert(
query.clone(),
(
Instant::now()
// cached value's max age:
+ match &r {
// found connection, may change, especially shortly before arrival,
// because delays can cause the requested arrival time to no longer be met.
Ok(Some(minutes)) => Duration::from_secs((*minutes as u64 * 10).clamp(30, 180)),
// found no connection, not extremely likely to change
Ok(None) => Duration::from_secs(180),
// something went wrong, allow relatively quick retries
Err(_) => Duration::from_secs(30),
},
r,
),
);
}
&cache.get(&query).unwrap().1
}
pub async fn find_trip_arrival_before_no_cache(
from: &VvsStopIdentifier, from: &VvsStopIdentifier,
to: &VvsStopIdentifier, to: &VvsStopIdentifier,
year: u16, year: u16,
@ -234,9 +342,9 @@ pub async fn find_trip_arrival_before(
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Response2 { struct Response2 {
/// in seconds // /// in seconds
#[serde(rename = "duration")] // #[serde(rename = "duration")]
duration: i32, // duration: i32,
#[serde(rename = "origin")] #[serde(rename = "origin")]
origin: Response3, origin: Response3,
#[serde(rename = "destination")] #[serde(rename = "destination")]
@ -246,8 +354,8 @@ pub async fn find_trip_arrival_before(
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Response3 { struct Response3 {
#[serde(rename = "name")] // #[serde(rename = "name")]
name: String, // name: String,
#[serde(rename = "departureTimeBaseTimetable")] #[serde(rename = "departureTimeBaseTimetable")]
pub departure_time_base_timetable: Option<DateTime<Local>>, pub departure_time_base_timetable: Option<DateTime<Local>>,
#[serde(rename = "departureTimePlanned")] #[serde(rename = "departureTimePlanned")]
@ -257,8 +365,8 @@ pub async fn find_trip_arrival_before(
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Response4 { struct Response4 {
#[serde(rename = "name")] // #[serde(rename = "name")]
name: String, // name: String,
#[serde(rename = "arrivalTimeBaseTimetable")] #[serde(rename = "arrivalTimeBaseTimetable")]
pub arrival_time_base_timetable: Option<DateTime<Local>>, pub arrival_time_base_timetable: Option<DateTime<Local>>,
#[serde(rename = "arrivalTimePlanned")] #[serde(rename = "arrivalTimePlanned")]