implement caching for vvs api
This commit is contained in:
parent
3550ab1fc4
commit
98a697f576
751
Cargo.lock
generated
751
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -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"
|
||||
|
233
src/main.rs
233
src/main.rs
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
132
src/vvs.rs
132
src/vvs.rs
@ -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")]
|
||||
|
Loading…
x
Reference in New Issue
Block a user