@@ -93,19 +93,28 @@ where
93
93
Ok ( sender)
94
94
}
95
95
96
- /// Converts the URL to expiration timestamp .
97
- fn http_url_cache_expires ( url : & str , mimetype : Option < & str > ) -> i64 {
96
+ /// Converts the URL to expiration and stale timestamps .
97
+ fn http_url_cache_timestamps ( url : & str , mimetype : Option < & str > ) -> ( i64 , i64 ) {
98
98
let now = time ( ) ;
99
- if url. ends_with ( ".xdc" ) {
100
- // WebXDCs expire in 5 weeks.
101
- now + 3600 * 24 * 35
99
+
100
+ let expires = now + 3600 * 24 * 35 ;
101
+ let stale = if url. ends_with ( ".xdc" ) {
102
+ // WebXDCs are never stale, they just expire.
103
+ expires
102
104
} else if mimetype. is_some_and ( |s| s. starts_with ( "image/" ) ) {
103
105
// Cache images for 1 day.
106
+ //
107
+ // As of 2024-12-12 WebXDC icons at <https://webxdc.org/apps/>
108
+ // use the same path for all app versions,
109
+ // so may change, but it is not critical if outdated icon is displayed.
104
110
now + 3600 * 24
105
111
} else {
106
- // Cache everything else for 1 hour.
112
+ // Revalidate everything else after 1 hour.
113
+ //
114
+ // This includes HTML, CSS and JS.
107
115
now + 3600
108
- }
116
+ } ;
117
+ ( expires, stale)
109
118
}
110
119
111
120
/// Places the binary into HTTP cache.
@@ -117,14 +126,16 @@ async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Re
117
126
)
118
127
. await ?;
119
128
129
+ let ( expires, stale) = http_url_cache_timestamps ( url, response. mimetype . as_deref ( ) ) ;
120
130
context
121
131
. sql
122
132
. insert (
123
- "INSERT OR REPLACE INTO http_cache (url, expires, blobname, mimetype, encoding)
124
- VALUES (?, ?, ?, ?, ?)" ,
133
+ "INSERT OR REPLACE INTO http_cache (url, expires, stale, blobname, mimetype, encoding)
134
+ VALUES (?, ?, ?, ?, ?, ? )" ,
125
135
(
126
136
url,
127
- http_url_cache_expires ( url, response. mimetype . as_deref ( ) ) ,
137
+ expires,
138
+ stale,
128
139
blob. as_name ( ) ,
129
140
response. mimetype . as_deref ( ) . unwrap_or_default ( ) ,
130
141
response. encoding . as_deref ( ) . unwrap_or_default ( ) ,
@@ -136,18 +147,22 @@ async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Re
136
147
}
137
148
138
149
/// Retrieves the binary from HTTP cache.
139
- async fn http_cache_get ( context : & Context , url : & str ) -> Result < Option < Response > > {
140
- let Some ( ( blob_name, mimetype, encoding) ) = context
150
+ ///
151
+ /// Also returns if the response is stale and should be revalidated in the background.
152
+ async fn http_cache_get ( context : & Context , url : & str ) -> Result < Option < ( Response , bool ) > > {
153
+ let now = time ( ) ;
154
+ let Some ( ( blob_name, mimetype, encoding, is_stale) ) = context
141
155
. sql
142
156
. query_row_optional (
143
- "SELECT blobname, mimetype, encoding
157
+ "SELECT blobname, mimetype, encoding, stale
144
158
FROM http_cache WHERE url=? AND expires > ?" ,
145
- ( url, time ( ) ) ,
159
+ ( url, now ) ,
146
160
|row| {
147
161
let blob_name: String = row. get ( 0 ) ?;
148
162
let mimetype: Option < String > = Some ( row. get ( 1 ) ?) . filter ( |s : & String | !s. is_empty ( ) ) ;
149
163
let encoding: Option < String > = Some ( row. get ( 2 ) ?) . filter ( |s : & String | !s. is_empty ( ) ) ;
150
- Ok ( ( blob_name, mimetype, encoding) )
164
+ let stale_timestamp: i64 = row. get ( 3 ) ?;
165
+ Ok ( ( blob_name, mimetype, encoding, now > stale_timestamp) )
151
166
} ,
152
167
)
153
168
. await ?
@@ -170,14 +185,20 @@ async fn http_cache_get(context: &Context, url: &str) -> Result<Option<Response>
170
185
}
171
186
} ;
172
187
173
- let expires = http_url_cache_expires ( url, mimetype. as_deref ( ) ) ;
188
+ let ( expires, _stale ) = http_url_cache_timestamps ( url, mimetype. as_deref ( ) ) ;
174
189
let response = Response {
175
190
blob,
176
191
mimetype,
177
192
encoding,
178
193
} ;
179
194
180
- // Update expiration timestamp.
195
+ // Update expiration timestamp
196
+ // to prevent deletion of the file still in use.
197
+ //
198
+ // We do not update stale timestamp here
199
+ // as we have not revalidated the response.
200
+ // Stale timestamp is updated only
201
+ // when the URL is sucessfully fetched.
181
202
context
182
203
. sql
183
204
. execute (
@@ -186,7 +207,7 @@ async fn http_cache_get(context: &Context, url: &str) -> Result<Option<Response>
186
207
)
187
208
. await ?;
188
209
189
- Ok ( Some ( response) )
210
+ Ok ( Some ( ( response, is_stale ) ) )
190
211
}
191
212
192
213
/// Removes expired cache entries.
@@ -205,14 +226,10 @@ pub(crate) async fn http_cache_cleanup(context: &Context) -> Result<()> {
205
226
Ok ( ( ) )
206
227
}
207
228
208
- /// Retrieves the binary contents of URL using HTTP GET request.
209
- pub async fn read_url_blob ( context : & Context , original_url : & str ) -> Result < Response > {
210
- if let Some ( response) = http_cache_get ( context, original_url) . await ? {
211
- info ! ( context, "Returning {original_url:?} from cache." ) ;
212
- return Ok ( response) ;
213
- }
214
-
215
- info ! ( context, "Not found {original_url:?} in cache." ) ;
229
+ /// Fetches URL and updates the cache.
230
+ ///
231
+ /// URL is fetched regardless of whether there is an existing result in the cache.
232
+ async fn fetch_url ( context : & Context , original_url : & str ) -> Result < Response > {
216
233
let mut url = original_url. to_string ( ) ;
217
234
218
235
// Follow up to 10 http-redirects
@@ -273,6 +290,31 @@ pub async fn read_url_blob(context: &Context, original_url: &str) -> Result<Resp
273
290
Err ( anyhow ! ( "Followed 10 redirections" ) )
274
291
}
275
292
293
+ /// Retrieves the binary contents of URL using HTTP GET request.
294
+ pub async fn read_url_blob ( context : & Context , url : & str ) -> Result < Response > {
295
+ if let Some ( ( response, is_stale) ) = http_cache_get ( context, url) . await ? {
296
+ info ! ( context, "Returning {url:?} from cache." ) ;
297
+ if is_stale {
298
+ let context = context. clone ( ) ;
299
+ let url = url. to_string ( ) ;
300
+ tokio:: spawn ( async move {
301
+ // Fetch URL in background to update the cache.
302
+ info ! ( context, "Fetching stale {url:?} in background." ) ;
303
+ if let Err ( err) = fetch_url ( & context, & url) . await {
304
+ warn ! ( context, "Failed to revalidate {url:?}: {err:#}." ) ;
305
+ }
306
+ } ) ;
307
+ }
308
+
309
+ // Return stale result.
310
+ return Ok ( response) ;
311
+ }
312
+
313
+ info ! ( context, "Not found {url:?} in cache, fetching." ) ;
314
+ let response = fetch_url ( context, url) . await ?;
315
+ Ok ( response)
316
+ }
317
+
276
318
/// Sends an empty POST request to the URL.
277
319
///
278
320
/// Returns response text and whether request was successful or not.
@@ -401,51 +443,55 @@ mod tests {
401
443
assert_eq ! ( http_cache_get( t, xdc_pixel_url) . await ?, None ) ;
402
444
assert_eq ! (
403
445
http_cache_get( t, "https://webxdc.org/" ) . await ?,
404
- Some ( html_response. clone( ) )
446
+ Some ( ( html_response. clone( ) , false ) )
405
447
) ;
406
448
407
449
http_cache_put ( t, xdc_editor_url, & xdc_response) . await ?;
408
450
http_cache_put ( t, xdc_pixel_url, & xdc_response) . await ?;
409
451
assert_eq ! (
410
452
http_cache_get( t, xdc_editor_url) . await ?,
411
- Some ( xdc_response. clone( ) )
453
+ Some ( ( xdc_response. clone( ) , false ) )
412
454
) ;
413
455
assert_eq ! (
414
456
http_cache_get( t, xdc_pixel_url) . await ?,
415
- Some ( xdc_response. clone( ) )
457
+ Some ( ( xdc_response. clone( ) , false ) )
416
458
) ;
417
459
418
460
assert_eq ! (
419
461
http_cache_get( t, "https://webxdc.org/" ) . await ?,
420
- Some ( html_response. clone( ) )
462
+ Some ( ( html_response. clone( ) , false ) )
421
463
) ;
422
464
423
- // HTML expires after 1 hour, but .xdc does not.
465
+ // HTML is stale after 1 hour, but .xdc is not.
424
466
SystemTime :: shift ( Duration :: from_secs ( 3600 + 100 ) ) ;
425
- assert_eq ! ( http_cache_get( t, "https://webxdc.org/" ) . await ?, None ) ;
467
+ assert_eq ! (
468
+ http_cache_get( t, "https://webxdc.org/" ) . await ?,
469
+ Some ( ( html_response. clone( ) , true ) )
470
+ ) ;
426
471
assert_eq ! (
427
472
http_cache_get( t, xdc_editor_url) . await ?,
428
- Some ( xdc_response. clone( ) )
473
+ Some ( ( xdc_response. clone( ) , false ) )
429
474
) ;
430
475
431
- // Expired cache entry can be renewed
476
+ // Stale cache entry can be renewed
432
477
// even before housekeeping removes old one.
433
478
http_cache_put ( t, "https://webxdc.org/" , & html_response) . await ?;
434
479
assert_eq ! (
435
480
http_cache_get( t, "https://webxdc.org/" ) . await ?,
436
- Some ( html_response. clone( ) )
481
+ Some ( ( html_response. clone( ) , false ) )
437
482
) ;
438
483
439
484
// 35 days later pixel .xdc expires because we did not request it for 35 days and 1 hour.
440
485
// But editor is still there because we did not request it for just 35 days.
486
+ // We have not renewed the editor however, so it becomes stale.
441
487
SystemTime :: shift ( Duration :: from_secs ( 3600 * 24 * 35 - 100 ) ) ;
442
488
443
489
// Run housekeeping to test that it does not delete the blob too early.
444
490
housekeeping ( t) . await ?;
445
491
446
492
assert_eq ! (
447
493
http_cache_get( t, xdc_editor_url) . await ?,
448
- Some ( xdc_response. clone( ) )
494
+ Some ( ( xdc_response. clone( ) , true ) )
449
495
) ;
450
496
assert_eq ! ( http_cache_get( t, xdc_pixel_url) . await ?, None ) ;
451
497
0 commit comments