22 * License, v. 2.0. If a copy of the MPL was not distributed with this
33 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
5- use std:: { collections:: HashMap , time:: Duration } ;
5+ use std:: { collections:: HashMap , sync :: Arc , time:: Duration } ;
66
7- use crate :: http_cache:: { request_hash:: RequestHash , ByteSize } ;
7+ use crate :: http_cache:: {
8+ clock:: { CacheClock , Clock } ,
9+ request_hash:: RequestHash ,
10+ ByteSize ,
11+ } ;
812use parking_lot:: Mutex ;
913use rusqlite:: { params, Connection , OptionalExtension , Result as SqliteResult } ;
1014use viaduct:: { Header , Request , Response } ;
@@ -21,6 +25,7 @@ pub enum FaultKind {
2125
2226pub struct HttpCacheStore {
2327 conn : Mutex < Connection > ,
28+ clock : Arc < dyn Clock > ,
2429 #[ cfg( test) ]
2530 fault : parking_lot:: Mutex < FaultKind > ,
2631}
@@ -29,11 +34,29 @@ impl HttpCacheStore {
2934 pub fn new ( conn : Connection ) -> Self {
3035 Self {
3136 conn : Mutex :: new ( conn) ,
37+ clock : Arc :: new ( CacheClock ) ,
3238 #[ cfg( test) ]
3339 fault : parking_lot:: Mutex :: new ( FaultKind :: None ) ,
3440 }
3541 }
3642
43+ #[ cfg( test) ]
44+ pub fn new_with_test_clock ( conn : Connection ) -> Self {
45+ use crate :: http_cache:: clock:: TestClock ;
46+
47+ Self {
48+ conn : Mutex :: new ( conn) ,
49+ clock : Arc :: new ( TestClock :: new ( chrono:: Utc :: now ( ) . timestamp ( ) ) ) ,
50+ #[ cfg( test) ]
51+ fault : parking_lot:: Mutex :: new ( FaultKind :: None ) ,
52+ }
53+ }
54+
55+ #[ cfg( test) ]
56+ pub fn get_clock ( & self ) -> & ( dyn Clock ) {
57+ & * self . clock
58+ }
59+
3760 /// Removes all entries from cache.
3861 pub fn clear_all ( & self ) -> SqliteResult < usize > {
3962 let conn = self . conn . lock ( ) ;
@@ -60,7 +83,7 @@ impl HttpCacheStore {
6083 let conn = self . conn . lock ( ) ;
6184 conn. execute (
6285 "DELETE FROM http_cache WHERE expires_at < ?1" ,
63- params ! [ chrono :: Utc :: now ( ) . timestamp ( ) ] ,
86+ params ! [ self . clock . now_epoch_seconds ( ) ] ,
6487 )
6588 }
6689
@@ -118,7 +141,7 @@ impl HttpCacheStore {
118141 let headers_map: HashMap < String , String > = response. headers . clone ( ) . into ( ) ;
119142 let response_headers = serde_json:: to_vec ( & headers_map) . unwrap_or_default ( ) ;
120143 let size_bytes = ( response_headers. len ( ) + response. body . len ( ) ) as i64 ;
121- let now = chrono :: Utc :: now ( ) . timestamp ( ) ;
144+ let now = self . clock . now_epoch_seconds ( ) ;
122145 let ttl_seconds = ttl. as_secs ( ) ;
123146 let expires_at = now + ttl_seconds as i64 ;
124147
@@ -252,7 +275,7 @@ mod tests {
252275 let initializer = HttpCacheConnectionInitializer { } ;
253276 let conn = open_database:: open_memory_database ( & initializer)
254277 . expect ( "failed to open memory cache db" ) ;
255- HttpCacheStore :: new ( conn)
278+ HttpCacheStore :: new_with_test_clock ( conn)
256279 }
257280
258281 #[ test]
@@ -358,16 +381,16 @@ mod tests {
358381 let ( c1, e1, t1) = fetch_timestamps ( & store, & req) ;
359382 assert_eq ! ( t1, 300 ) ;
360383
361- // Change TTL to 1s and upsert; wait a tick so cached_at likely changes
362- std :: thread :: sleep ( std :: time :: Duration :: from_millis ( 50 ) ) ;
384+ store . get_clock ( ) . advance ( 3 ) ;
385+
363386 store
364387 . store_with_ttl ( & req, & resp, & Duration :: new ( 1 , 0 ) )
365388 . unwrap ( ) ;
366389 let ( c2, e2, t2) = fetch_timestamps ( & store, & req) ;
367390 assert_eq ! ( t2, 1 ) ;
368391 // cached_at should be >= previous cached_at; expires_at should move accordingly
369- assert ! ( c2 >= c1) ;
370- assert ! ( e2 <= e1, "expires_at should move earlier when TTL shrinks" ) ;
392+ assert ! ( c2 > c1) ;
393+ assert ! ( e2 < e1, "expires_at should move earlier when TTL shrinks" ) ;
371394 }
372395
373396 #[ test]
@@ -390,7 +413,7 @@ mod tests {
390413 assert ! ( store. lookup( & req_fresh) . unwrap( ) . is_some( ) ) ;
391414
392415 // Let first one expire; then cleanup
393- std :: thread :: sleep ( std :: time :: Duration :: from_secs ( 2 ) ) ;
416+ store . clock . advance ( 2 ) ;
394417 let removed = store. delete_expired_entries ( ) . unwrap ( ) ;
395418 assert ! (
396419 removed >= 1 ,
@@ -412,7 +435,7 @@ mod tests {
412435 . store_with_ttl ( & req, & resp, & Duration :: new ( 1 , 0 ) )
413436 . unwrap ( ) ;
414437 // Check that lookup still returns (store is policy-agnostic).
415- std :: thread :: sleep ( std :: time :: Duration :: from_secs ( 2 ) ) ;
438+ store . clock . advance ( 2 ) ;
416439 assert ! ( store. lookup( & req) . unwrap( ) . is_some( ) ) ;
417440
418441 // Test cleanup still removes it
@@ -434,7 +457,7 @@ mod tests {
434457 assert ! ( store. lookup( & req) . unwrap( ) . is_some( ) ) ;
435458
436459 // Advance a second so now > expires_at
437- std :: thread :: sleep ( std :: time :: Duration :: from_secs ( 1 ) ) ;
460+ store . clock . advance ( 2 ) ;
438461 let removed = store. delete_expired_entries ( ) . unwrap ( ) ;
439462 assert ! ( removed >= 1 ) ;
440463 assert ! ( store. lookup( & req) . unwrap( ) . is_none( ) ) ;
@@ -458,10 +481,7 @@ mod tests {
458481
459482 #[ test]
460483 fn test_ttl_expiration ( ) {
461- let initializer = HttpCacheConnectionInitializer { } ;
462- let conn = open_database:: open_memory_database ( & initializer)
463- . expect ( "failed to open memory cache db" ) ;
464- let store = HttpCacheStore :: new ( conn) ;
484+ let store = create_test_store ( ) ;
465485
466486 let request = create_test_request ( "https://example.com/api" , b"test body" ) ;
467487 let response = create_test_response ( 200 , b"test response" ) ;
@@ -473,7 +493,7 @@ mod tests {
473493 let retrieved = store. lookup ( & request) . unwrap ( ) . unwrap ( ) ;
474494 assert_eq ! ( retrieved. 0 . body, b"test response" ) ;
475495
476- std :: thread :: sleep ( Duration :: from_secs ( 2 ) ) ;
496+ store . clock . advance ( 2 ) ;
477497
478498 let retrieved_after_expiry = store. lookup ( & request) . unwrap ( ) ;
479499 assert ! ( retrieved_after_expiry. is_some( ) ) ;
0 commit comments