@@ -104,38 +104,49 @@ pub fn spawn_updater(
104104 db_path : PathBuf ,
105105 update_url : String ,
106106 update_time_utc : ( u8 , u8 ) ,
107+ interval_hours : u8 ,
107108 cancel : CancellationToken ,
108109) -> JoinHandle < ( ) > {
109110 tokio:: spawn ( run_updater (
110111 shared_db,
111112 db_path,
112113 update_url,
113114 update_time_utc,
115+ interval_hours,
114116 cancel,
115117 ) )
116118}
117119
118120/// Returns the number of seconds from `now_secs` (seconds since UNIX epoch)
119- /// until the next occurrence of the given UTC `(hour, minute)`.
120- fn secs_until_next ( now_secs : u64 , hour : u8 , minute : u8 ) -> u64 {
121+ /// until the next tick. Ticks occur at `(anchor_hour, anchor_minute)` UTC and
122+ /// every `interval_hours` thereafter. `interval_hours` must divide 24 evenly.
123+ fn secs_until_next_tick (
124+ now_secs : u64 ,
125+ anchor_hour : u8 ,
126+ anchor_minute : u8 ,
127+ interval_hours : u8 ,
128+ ) -> u64 {
121129 const SECS_PER_DAY : u64 = 86_400 ;
130+ let interval = u64:: from ( interval_hours) * 3600 ;
131+ let anchor = u64:: from ( anchor_hour) * 3600 + u64:: from ( anchor_minute) * 60 ;
122132 let time_of_day = now_secs % SECS_PER_DAY ;
123- let target = u64:: from ( hour) * 3600 + u64:: from ( minute) * 60 ;
124- if target > time_of_day {
125- target - time_of_day
133+ let offset_from_anchor = if time_of_day >= anchor {
134+ time_of_day - anchor
126135 } else {
127- // Target time already passed today; schedule for tomorrow.
128- SECS_PER_DAY - time_of_day + target
129- }
136+ SECS_PER_DAY - ( anchor - time_of_day)
137+ } ;
138+ let next_boundary = ( ( offset_from_anchor / interval) + 1 ) * interval;
139+ next_boundary - offset_from_anchor
130140}
131141
132- /// Runs the daily database updater loop. Sleeps until the configured UTC time
133- /// each day, then downloads and swaps in a fresh database.
142+ /// Runs the database updater loop. Sleeps until the next scheduled tick, then
143+ /// downloads and hot- swaps a fresh database.
134144pub async fn run_updater (
135145 shared_db : SharedDb ,
136146 db_path : PathBuf ,
137147 update_url : String ,
138148 update_time_utc : ( u8 , u8 ) ,
149+ interval_hours : u8 ,
139150 cancel : CancellationToken ,
140151) {
141152 let ( hour, minute) = update_time_utc;
@@ -144,13 +155,15 @@ pub async fn run_updater(
144155 . duration_since ( UNIX_EPOCH )
145156 . expect ( "system clock before UNIX epoch" )
146157 . as_secs ( ) ;
147- let delay = Duration :: from_secs ( secs_until_next ( now_secs, hour, minute) ) ;
158+ let delay =
159+ Duration :: from_secs ( secs_until_next_tick ( now_secs, hour, minute, interval_hours) ) ;
148160 info ! (
149- "next database update in {}h {}m (at {:02}:{:02} UTC)" ,
161+ "next database update in {}h {}m (anchor {:02}:{:02} UTC, every {}h )" ,
150162 delay. as_secs( ) / 3600 ,
151163 ( delay. as_secs( ) % 3600 ) / 60 ,
152164 hour,
153165 minute,
166+ interval_hours,
154167 ) ;
155168 tokio:: select! {
156169 ( ) = tokio:: time:: sleep( delay) => { }
@@ -172,7 +185,12 @@ async fn update_db(
172185) -> Result < ( ) , Box < dyn std:: error:: Error + Send + Sync > > {
173186 let reader = download_db ( update_url, db_path) . await ?;
174187 shared_db. store ( Arc :: new ( Some ( reader) ) ) ;
175- info ! ( "database updated successfully" ) ;
188+ let size = fs:: metadata ( db_path)
189+ . await
190+ . ok ( )
191+ . map ( |m| m. len ( ) )
192+ . unwrap_or ( 0 ) ;
193+ info ! ( "database updated successfully ({size} bytes)" ) ;
176194 Ok ( ( ) )
177195}
178196
@@ -279,7 +297,8 @@ mod tests {
279297 shared_db,
280298 PathBuf :: from ( "/nonexistent/path.mmdb" ) ,
281299 "https://invalid.example.com/db.mmdb" . to_string ( ) ,
282- ( 1 , 20 ) ,
300+ ( 0 , 20 ) ,
301+ 6 ,
283302 cancel. clone ( ) ,
284303 ) ;
285304
@@ -293,31 +312,57 @@ mod tests {
293312 }
294313
295314 #[ test]
296- fn secs_until_next_later_today ( ) {
297- // 00:00:00 UTC, target 01:20 UTC => 1h 20m = 4800s
298- assert_eq ! ( secs_until_next( 0 , 1 , 20 ) , 4800 ) ;
315+ fn secs_until_next_tick_daily_from_midnight ( ) {
316+ // 00:00 UTC, anchor 01:20, interval 24h => 1h 20m = 4800s.
317+ assert_eq ! ( secs_until_next_tick( 0 , 1 , 20 , 24 ) , 4800 ) ;
318+ }
319+
320+ #[ test]
321+ fn secs_until_next_tick_daily_wraps_to_tomorrow ( ) {
322+ // 02:00 UTC (7200s), anchor 01:20 (4800s), interval 24h
323+ // => 86400 - 7200 + 4800 = 84000s.
324+ assert_eq ! ( secs_until_next_tick( 7200 , 1 , 20 , 24 ) , 84000 ) ;
325+ }
326+
327+ #[ test]
328+ fn secs_until_next_tick_daily_exact_time_wraps ( ) {
329+ // Exactly at the anchor with 24h interval => schedule for next day.
330+ let target_secs = 3600 + 20 * 60 ;
331+ assert_eq ! ( secs_until_next_tick( target_secs, 1 , 20 , 24 ) , 86400 ) ;
332+ }
333+
334+ #[ test]
335+ fn secs_until_next_tick_every_six_hours_at_midnight ( ) {
336+ // 00:00 UTC with anchor 00:20 and interval 6h => next tick in 20m = 1200s.
337+ assert_eq ! ( secs_until_next_tick( 0 , 0 , 20 , 6 ) , 1200 ) ;
338+ }
339+
340+ #[ test]
341+ fn secs_until_next_tick_every_six_hours_just_after_tick ( ) {
342+ // 00:21 UTC (1260s), anchor 00:20, interval 6h
343+ // => next tick is 06:20 UTC = 6h - 1m = 21540s.
344+ let now_secs = 20 * 60 + 60 ; // 00:21
345+ assert_eq ! ( secs_until_next_tick( now_secs, 0 , 20 , 6 ) , 21540 ) ;
299346 }
300347
301348 #[ test]
302- fn secs_until_next_wraps_to_tomorrow ( ) {
303- // 02:00:00 UTC (7200s into the day), target 01:20 (4800s into the day)
304- // => should wrap to next day: 86400 - 7200 + 4800 = 84000s
305- let now_secs = 7200 ; // midnight + 2 hours
306- assert_eq ! ( secs_until_next( now_secs, 1 , 20 ) , 84000 ) ;
349+ fn secs_until_next_tick_every_six_hours_at_tick ( ) {
350+ // Exactly at a tick (00:20) with 6h interval => schedule for 6h later.
351+ let target_secs = 20 * 60 ;
352+ assert_eq ! ( secs_until_next_tick( target_secs, 0 , 20 , 6 ) , 6 * 3600 ) ;
307353 }
308354
309355 #[ test]
310- fn secs_until_next_exact_time_wraps_to_tomorrow ( ) {
311- // Exactly at the target time => should schedule for next day (full 24h).
312- let target_secs = 1 * 3600 + 20 * 60 ; // 01:20 = 4800s
313- assert_eq ! ( secs_until_next( target_secs, 1 , 20 ) , 86400 ) ;
356+ fn secs_until_next_tick_every_six_hours_before_anchor ( ) {
357+ // 00:10 UTC (600s), anchor 00:20, interval 6h => next tick in 10m = 600s.
358+ assert_eq ! ( secs_until_next_tick( 600 , 0 , 20 , 6 ) , 600 ) ;
314359 }
315360
316361 #[ test]
317- fn secs_until_next_midnight_target ( ) {
318- // 23:00:00 UTC, target 00:00 UTC => 1 hour = 3600s
362+ fn secs_until_next_tick_midnight_target ( ) {
363+ // 23:00 UTC, anchor 00:00, interval 24h => 1h = 3600s.
319364 let now_secs = 23 * 3600 ;
320- assert_eq ! ( secs_until_next ( now_secs, 0 , 0 ) , 3600 ) ;
365+ assert_eq ! ( secs_until_next_tick ( now_secs, 0 , 0 , 24 ) , 3600 ) ;
321366 }
322367
323368 #[ tokio:: test]
0 commit comments