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

View File

@ -1,17 +1,51 @@
mod bahnhof;
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 rocket::{get, http::Status, response::content::RawHtml, routes, FromForm, State};
use rocket::{FromForm, State, get, http::Status, response::content::RawHtml, routes};
use tokio::{
sync::Mutex,
time::{sleep, Instant},
time::{Instant, sleep},
};
use crate::vvs::VvsStopIdentifier;
#[rocket::launch]
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()
.manage(Arc::new(()))
.manage(Mutex::new(
@ -21,6 +55,8 @@ async fn rocket() -> _ {
String,
)>::None,
))
.manage(vvs_stop_cache)
.manage(vvs_time_cache)
.mount(
"/",
routes![
@ -271,9 +307,15 @@ async fn rate_limit(rate_limit: &State<Arc<()>>) -> Arc<()> {
}
#[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;
match vvs::find_stop_best(stop).await {
match vvs::find_stop_best(stop, &mut *vvs_stop_cache.lock().await).await {
Ok(Some((name, _))) => {
format!("ok: {name}")
}
@ -296,6 +338,29 @@ async fn vvs_short_in_hrs_mins_before_simple(
to: &str,
form: VvsShortInHrsMinsBeforeSimple,
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) {
let time = Local::now() + Duration::from_secs_f64(hrs * 60.0 * 60.0);
let form = VvsMinsBeforeSimpleForm {
@ -307,7 +372,7 @@ async fn vvs_short_in_hrs_mins_before_simple(
interchanges: form.interchanges,
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)]
struct VvsShortInHrsMinsBeforeCheckSimple {
@ -324,6 +389,29 @@ async fn vvs_short_in_hrs_mins_before_check_simple(
to: &str,
form: VvsShortInHrsMinsBeforeCheckSimple,
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) {
let time = Local::now() + Duration::from_secs_f64(hrs * 60.0 * 60.0);
let form = VvsMinsBeforeCheckSimpleForm {
@ -336,7 +424,7 @@ async fn vvs_short_in_hrs_mins_before_check_simple(
minutes: form.minutes,
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)]
struct VvsMinsBeforeSimpleForm {
@ -354,40 +442,87 @@ async fn vvs_mins_before_simple(
to: &str,
form: VvsMinsBeforeSimpleForm,
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) {
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(
from: &str,
to: &str,
form: VvsMinsBeforeSimpleForm,
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) {
let _counter = rate_limit(counter).await;
if Arc::strong_count(&counter) > 2 {}
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(None) => {
return (
Status::NotFound,
format!("err: Couldn't find origin stop {from}"),
)
);
}
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(None) => {
return (
Status::NotFound,
format!("err: Couldn't find destination stop {to}"),
)
);
}
Err(e) => return (Status::InternalServerError, format!("err: {e}")),
};
drop(vvs_stop_cache_lock);
match vvs::find_trip_arrival_before(
&from,
&to,
from,
to,
form.year,
form.month,
form.day,
@ -395,6 +530,7 @@ async fn vvs_mins_before_simple_impl(
form.minute,
form.interchanges,
form.allowtaxi.is_some_and(|allow| allow),
&mut *vvs_time_cache.lock().await,
)
.await
{
@ -423,39 +559,87 @@ async fn vvs_mins_before_check_simple(
to: &str,
form: VvsMinsBeforeCheckSimpleForm,
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) {
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(
from: &str,
to: &str,
form: VvsMinsBeforeCheckSimpleForm,
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) {
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(None) => {
return (
Status::NotFound,
format!("err: Couldn't find origin stop {from}"),
)
);
}
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(None) => {
return (
Status::NotFound,
format!("err: Couldn't find destination stop {to}"),
)
);
}
Err(e) => return (Status::InternalServerError, format!("err: {e}")),
};
drop(vvs_stop_cache_lock);
match vvs::find_trip_arrival_before(
&from,
&to,
from,
to,
form.year,
form.month,
form.day,
@ -463,16 +647,17 @@ async fn vvs_mins_before_check_simple_impl(
form.minute,
form.interchanges,
form.allowtaxi.is_some_and(|allow| allow),
&mut *vvs_time_cache.lock().await,
)
.await
{
Ok(Some(v)) => {
if v <= form.minutes {
if *v <= form.minutes {
(Status::Ok, String::new())
} else {
(
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 serde::Deserialize;
use tokio::time::Instant;
#[derive(Debug)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct VvsStopIdentifier {
id: String,
gid: String,
}
pub async fn find_stop_best(query: &str) -> Result<Option<(String, VvsStopIdentifier)>, String> {
Ok(find_stop(query)
pub async fn find_stop_best(
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?
.into_iter()
.filter_map(|(name, _types, identifier)| Some((name, identifier?)))
.next())
}
pub async fn find_stop(
pub async fn find_stop_no_cache(
query: &str,
) -> Result<
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.
/// 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.
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,
to: &VvsStopIdentifier,
year: u16,
@ -234,9 +342,9 @@ pub async fn find_trip_arrival_before(
}
#[derive(Deserialize)]
struct Response2 {
/// in seconds
#[serde(rename = "duration")]
duration: i32,
// /// in seconds
// #[serde(rename = "duration")]
// duration: i32,
#[serde(rename = "origin")]
origin: Response3,
#[serde(rename = "destination")]
@ -246,8 +354,8 @@ pub async fn find_trip_arrival_before(
}
#[derive(Deserialize)]
struct Response3 {
#[serde(rename = "name")]
name: String,
// #[serde(rename = "name")]
// name: String,
#[serde(rename = "departureTimeBaseTimetable")]
pub departure_time_base_timetable: Option<DateTime<Local>>,
#[serde(rename = "departureTimePlanned")]
@ -257,8 +365,8 @@ pub async fn find_trip_arrival_before(
}
#[derive(Deserialize)]
struct Response4 {
#[serde(rename = "name")]
name: String,
// #[serde(rename = "name")]
// name: String,
#[serde(rename = "arrivalTimeBaseTimetable")]
pub arrival_time_base_timetable: Option<DateTime<Local>>,
#[serde(rename = "arrivalTimePlanned")]