1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
use crate::cache::Cache;
use crate::tracker::{Package, Tracker, TrackerContext};
use crate::{Error, Result};
use tokio::sync::Mutex;
/// Composed type with pluggable tracker + cache handlers. Orchestrates:
/// - Fetching a raw value from either the Tracker or the Cache
/// - Parsing the raw value with Tracker
pub struct CachedTracker<'a> {
pub tracker: Box<dyn Tracker>,
pub cache: &'a Mutex<dyn Cache>,
}
impl<'a> CachedTracker<'a> {
pub async fn track(
&mut self,
url: &str,
cache_seconds: usize,
use_cache: bool,
ctx: &'a TrackerContext<'_>,
) -> Result<Package> {
if use_cache {
match self
.get_cached(url, cache_seconds)
.await
{
Ok(Some(package)) => return Ok(package),
Err(err) => log::warn!(
"Error loading from cache: {err}. Getting a fresh value."
),
Ok(None) => log::info!(
"No cache entry found for {url}. Getting a fresh value."
),
}
}
self.get_fresh(url, ctx).await
}
async fn get_fresh(
&mut self,
url: &str,
ctx: &'a TrackerContext<'_>,
) -> Result<Package> {
let text = match self.tracker.get_raw(url, ctx).await {
Ok(text) => text,
// If we receive a client error (4xx) it is sometimes because we
// tried to use the user's home postcode on a package for which the
// user is not the recipient (for example, a return). This results
// in a 404 from the carrier API because the postcodes don't match.
// In this case, we want to retry _without_ the user's default
// postcode, because then we will at least get a response.
Err(Error::Reqwest(err))
if err
.status()
.is_some_and(|s| s.is_client_error()) =>
{
log::warn!(
"Bad response: {err}, trying again without default postcode..."
);
let mut ctx = ctx.clone();
ctx.recipient_postcode = None;
self.tracker.get_raw(url, &ctx).await?
}
Err(err) => return Err(err),
};
self.cache
.lock()
.await
.insert(url.to_owned(), text.clone());
let package = self.tracker.parse(text)?;
Ok(package)
}
async fn get_cached(
&mut self,
url: &str,
cache_seconds: usize,
) -> Result<Option<Package>> {
let cache = self.cache.lock().await;
let cached = cache.get(url).cloned();
drop(cache); // allows other async threads to use it
if let Some(entry) = cached {
match self.tracker.parse(entry.text.clone()) {
Err(err) => {
return Err(
format!(
"Couldn't parse cache entry to package! url: {url}, cache entry: {entry:?}, error: {err:?}").into()
);
}
Ok(package) => {
let age = entry.age().num_seconds().unsigned_abs() as usize;
// Always cache packages with a final status, because they
// will receive no more updates.
if package.status.is_final() {
log::info!(
"Reusing {age}s old cache entry for delivered {} {} from url {url}",
package.channel,
package.barcode,
);
return Ok(Some(package));
}
// Cache undelivered packages if the entry is young enough
if age <= cache_seconds {
log::info!(
"Reusing {age}s old cache entry for undelivered {} {} from url {url}",
package.channel,
package.barcode,
);
return Ok(Some(package));
}
}
}
}
Ok(None)
}
}