@@ -375,9 +375,13 @@ pub struct RecentRun {
375375 pub error : Option < String > ,
376376 /// Bytes the device reported uploading, if any (reported runs only).
377377 pub bytes_uploaded : Option < i64 > ,
378- /// Snapshot the run produced, if any (reported runs only).
378+ /// Snapshot the run produced (backup) or used (restore), if any (reported
379+ /// runs only).
379380 pub snapshot_id : Option < String > ,
380- /// Logical snapshot size from repo inspection, if known (reported runs only).
381+ /// Logical size of the run's snapshot, if known (reported runs only). For
382+ /// a backup this comes from repo inspection. For a restore it is resolved
383+ /// from the backup run that produced the snapshot (matched by snapshot id),
384+ /// falling back to inspection's backfill onto the restore run itself.
381385 pub snapshot_logical_bytes : Option < i64 > ,
382386 /// Raw bytes sent to S3 during the run, if tallied (reported runs only).
383387 pub s3_sent_raw_bytes : Option < i64 > ,
@@ -409,6 +413,12 @@ pub struct RecentRun {
409413/// surface as inferred rows — in-flight while their creds are still valid,
410414/// otherwise unreported past attempts (e.g. manual restores, which don't report).
411415///
416+ /// `source_snapshot_sizes` maps snapshot ids to sizes already known from the
417+ /// backup runs that produced them (see [`BackupRun::snapshot_sizes_by_id`]);
418+ /// a restore run's snapshot size resolves from it first, so the figure shows
419+ /// immediately rather than waiting for inspection to backfill the restore
420+ /// run's own row (which stays as the fallback).
421+ ///
412422/// Pairing is exact when the client stamped its run-uuid on the credential
413423/// request (`run_id`): those issuances group by that id, so concurrent runs of
414424/// the same type on one server never conflate. Issuances without a `run_id`
@@ -419,6 +429,7 @@ fn build_recent_runs(
419429 runs : Vec < BackupRun > ,
420430 issuances : Vec < BackupCredentialIssuance > ,
421431 device_to_server : & std:: collections:: HashMap < Uuid , Uuid > ,
432+ source_snapshot_sizes : & std:: collections:: HashMap < String , i64 > ,
422433 now : Timestamp ,
423434 limit : usize ,
424435) -> Vec < RecentRun > {
@@ -439,6 +450,15 @@ fn build_recent_runs(
439450 . zip ( starts)
440451 . map ( |( run, started_at) | {
441452 let duration_seconds = started_at. map ( |s| run. reported_at . as_second ( ) - s. as_second ( ) ) ;
453+ let snapshot_logical_bytes = if run. purpose == BackupPurpose :: Restore {
454+ run. snapshot_id
455+ . as_ref ( )
456+ . and_then ( |id| source_snapshot_sizes. get ( id) )
457+ . copied ( )
458+ . or ( run. snapshot_logical_bytes )
459+ } else {
460+ run. snapshot_logical_bytes
461+ } ;
442462 RecentRun {
443463 key : format ! ( "run-{}" , run. id) ,
444464 server_id : run. server_id ,
@@ -449,7 +469,7 @@ fn build_recent_runs(
449469 error : run. error ,
450470 bytes_uploaded : run. bytes_uploaded ,
451471 snapshot_id : run. snapshot_id ,
452- snapshot_logical_bytes : run . snapshot_logical_bytes ,
472+ snapshot_logical_bytes,
453473 s3_sent_raw_bytes : run. s3_sent_raw_bytes ,
454474 s3_sent_payload_bytes : run. s3_sent_payload_bytes ,
455475 s3_received_raw_bytes : run. s3_received_raw_bytes ,
@@ -1819,10 +1839,22 @@ pub async fn stats(
18191839 RECENT_LIMIT * 4 ,
18201840 )
18211841 . await ?;
1842+ // A restore's snapshot was already sized when the backup that produced it
1843+ // ran (or was inspected), so resolve restore snapshot sizes from that data
1844+ // rather than waiting for inspection to backfill the restore run's row.
1845+ let restore_snapshot_ids: Vec < String > = reported_runs
1846+ . iter ( )
1847+ . filter ( |r| r. purpose == BackupPurpose :: Restore )
1848+ . filter_map ( |r| r. snapshot_id . clone ( ) )
1849+ . collect ( ) ;
1850+ let source_snapshot_sizes =
1851+ BackupRun :: snapshot_sizes_by_id ( & mut conn, args. server_group_id , & restore_snapshot_ids)
1852+ . await ?;
18221853 let recent_runs = build_recent_runs (
18231854 reported_runs,
18241855 issuances,
18251856 & device_to_server,
1857+ & source_snapshot_sizes,
18261858 now,
18271859 RECENT_LIMIT as usize ,
18281860 ) ;
@@ -2334,13 +2366,18 @@ mod tests {
23342366 std:: collections:: HashMap :: from ( [ ( device, Uuid :: from_u128 ( server) ) ] )
23352367 }
23362368
2369+ fn no_sizes ( ) -> std:: collections:: HashMap < String , i64 > {
2370+ std:: collections:: HashMap :: new ( )
2371+ }
2372+
23372373 #[ test]
23382374 fn reported_run_gets_duration_from_its_issuance ( ) {
23392375 let d = Uuid :: from_u128 ( 1 ) ;
23402376 let rows = build_recent_runs (
23412377 vec ! [ run( 10 , d, BackupPurpose :: Backup , 4000 ) ] ,
23422378 vec ! [ issuance( 1 , d, BackupPurpose :: Backup , 1000 , 4600 ) ] ,
23432379 & device_map ( d, 9 ) ,
2380+ & no_sizes ( ) ,
23442381 ts ( 5000 ) ,
23452382 20 ,
23462383 ) ;
@@ -2358,6 +2395,7 @@ mod tests {
23582395 vec ! [ ] ,
23592396 vec ! [ issuance( 1 , d, BackupPurpose :: Restore , 4900 , 8500 ) ] ,
23602397 & device_map ( d, 9 ) ,
2398+ & no_sizes ( ) ,
23612399 ts ( 5000 ) ,
23622400 20 ,
23632401 ) ;
@@ -2375,6 +2413,7 @@ mod tests {
23752413 vec ! [ ] ,
23762414 vec ! [ issuance( 1 , d, BackupPurpose :: Restore , 1000 , 4600 ) ] ,
23772415 & device_map ( d, 9 ) ,
2416+ & no_sizes ( ) ,
23782417 ts ( 5000 ) ,
23792418 20 ,
23802419 ) ;
@@ -2389,6 +2428,7 @@ mod tests {
23892428 vec ! [ run( 10 , d, BackupPurpose :: Restore , 4000 ) ] ,
23902429 vec ! [ issuance( 1 , d, BackupPurpose :: Restore , 1000 , 4600 ) ] ,
23912430 & device_map ( d, 9 ) ,
2431+ & no_sizes ( ) ,
23922432 ts ( 5000 ) ,
23932433 20 ,
23942434 ) ;
@@ -2407,6 +2447,7 @@ mod tests {
24072447 issuance( 2 , d, BackupPurpose :: Backup , 4500 , 8100 ) ,
24082448 ] ,
24092449 & device_map ( d, 9 ) ,
2450+ & no_sizes ( ) ,
24102451 ts ( 9000 ) ,
24112452 20 ,
24122453 ) ;
@@ -2423,6 +2464,7 @@ mod tests {
24232464 vec ! [ run( 10 , d, BackupPurpose :: Backup , 8000 ) ] ,
24242465 vec ! [ issuance( 1 , d, BackupPurpose :: Backup , 1000 , 4600 ) ] ,
24252466 & device_map ( d, 9 ) ,
2467+ & no_sizes ( ) ,
24262468 ts ( 9000 ) ,
24272469 20 ,
24282470 ) ;
@@ -2461,6 +2503,7 @@ mod tests {
24612503 ) ,
24622504 ] ,
24632505 & device_map ( d, 9 ) ,
2506+ & no_sizes ( ) ,
24642507 ts ( 5000 ) ,
24652508 20 ,
24662509 ) ;
@@ -2483,6 +2526,7 @@ mod tests {
24832526 vec ! [ run( 200 , d, BackupPurpose :: Backup , 100_000 ) ] ,
24842527 vec ! [ issuance_for( 1 , d, BackupPurpose :: Backup , 1000 , 4600 , rid) ] ,
24852528 & device_map ( d, 9 ) ,
2529+ & no_sizes ( ) ,
24862530 ts ( 101_000 ) ,
24872531 20 ,
24882532 ) ;
@@ -2491,6 +2535,67 @@ mod tests {
24912535 assert_eq ! ( rows[ 0 ] . duration_seconds, Some ( 99_000 ) ) ;
24922536 }
24932537
2538+ #[ test]
2539+ fn restore_snapshot_size_resolves_from_the_producing_backup ( ) {
2540+ let d = Uuid :: from_u128 ( 9 ) ;
2541+ let mut restore = run ( 10 , d, BackupPurpose :: Restore , 4000 ) ;
2542+ restore. snapshot_id = Some ( "snap-a" . into ( ) ) ;
2543+ // The size the snapshot was recorded at when its backup ran/was inspected.
2544+ let sizes = std:: collections:: HashMap :: from ( [ ( "snap-a" . to_string ( ) , 8_192_i64 ) ] ) ;
2545+ let rows = build_recent_runs (
2546+ vec ! [ restore] ,
2547+ vec ! [ ] ,
2548+ & device_map ( d, 9 ) ,
2549+ & sizes,
2550+ ts ( 5000 ) ,
2551+ 20 ,
2552+ ) ;
2553+ assert_eq ! ( rows. len( ) , 1 ) ;
2554+ assert_eq ! (
2555+ rows[ 0 ] . snapshot_logical_bytes,
2556+ Some ( 8192 ) ,
2557+ "restore shows its source snapshot's size without inspection of its own row"
2558+ ) ;
2559+ }
2560+
2561+ #[ test]
2562+ fn restore_snapshot_size_falls_back_to_its_own_backfill ( ) {
2563+ let d = Uuid :: from_u128 ( 9 ) ;
2564+ let mut restore = run ( 10 , d, BackupPurpose :: Restore , 4000 ) ;
2565+ restore. snapshot_id = Some ( "snap-a" . into ( ) ) ;
2566+ restore. snapshot_logical_bytes = Some ( 4096 ) ;
2567+ // No producing backup carries the size — the restore row's own
2568+ // (inspection-backfilled) figure still surfaces.
2569+ let rows = build_recent_runs (
2570+ vec ! [ restore] ,
2571+ vec ! [ ] ,
2572+ & device_map ( d, 9 ) ,
2573+ & no_sizes ( ) ,
2574+ ts ( 5000 ) ,
2575+ 20 ,
2576+ ) ;
2577+ assert_eq ! ( rows[ 0 ] . snapshot_logical_bytes, Some ( 4096 ) ) ;
2578+ }
2579+
2580+ #[ test]
2581+ fn backup_snapshot_size_ignores_the_lookup ( ) {
2582+ let d = Uuid :: from_u128 ( 9 ) ;
2583+ let mut backup = run ( 10 , d, BackupPurpose :: Backup , 4000 ) ;
2584+ backup. snapshot_id = Some ( "snap-a" . into ( ) ) ;
2585+ // A backup's size is its own (device-reported or backfilled) figure; the
2586+ // source-snapshot lookup is restore-only.
2587+ let sizes = std:: collections:: HashMap :: from ( [ ( "snap-a" . to_string ( ) , 8_192_i64 ) ] ) ;
2588+ let rows = build_recent_runs (
2589+ vec ! [ backup] ,
2590+ vec ! [ ] ,
2591+ & device_map ( d, 9 ) ,
2592+ & sizes,
2593+ ts ( 5000 ) ,
2594+ 20 ,
2595+ ) ;
2596+ assert_eq ! ( rows[ 0 ] . snapshot_logical_bytes, None ) ;
2597+ }
2598+
24942599 #[ test]
24952600 fn inferred_rows_only_for_member_devices ( ) {
24962601 let member = Uuid :: from_u128 ( 1 ) ;
@@ -2503,6 +2608,7 @@ mod tests {
25032608 ] ,
25042609 // Only the member device maps to a server; the consumer device doesn't.
25052610 & device_map ( member, 9 ) ,
2611+ & no_sizes ( ) ,
25062612 ts ( 5000 ) ,
25072613 20 ,
25082614 ) ;
0 commit comments