@@ -422,6 +422,202 @@ TEST(AccessTimeMapTest, RecoverNoShm) {
422422 ShmManager::cleanup (shmDir, true );
423423}
424424
425+ TEST (AccessTimeMapTest, RecoverMissingDataSegment) {
426+ const auto shmDir =
427+ " /tmp/atm_missing_data_test_" + std::to_string (::getpid ());
428+ constexpr size_t kNumShards = 4 ;
429+
430+ // Populate and persist, then remove the data segment
431+ {
432+ ShmManager shm (shmDir, true );
433+ AccessTimeMap m (kNumShards );
434+
435+ m.set (10 , 100 );
436+ m.set (20 , 200 );
437+ m.persist (shm);
438+
439+ // Remove only the data segment, leaving the info segment intact
440+ shm.removeShm (std::string (AccessTimeMap::kShmDataName ));
441+ shm.shutDown ();
442+ }
443+
444+ // Recovery should detect missing data segment and start empty
445+ {
446+ ShmManager shm (shmDir, true );
447+ AccessTimeMap m (kNumShards );
448+ m.recover (shm);
449+ EXPECT_EQ (m.size (), 0 );
450+ shm.shutDown ();
451+ }
452+
453+ ShmManager::cleanup (shmDir, true );
454+ }
455+
456+ TEST (AccessTimeMapTest, RecoverDataSegmentTooSmall) {
457+ const auto shmDir = " /tmp/atm_small_data_test_" + std::to_string (::getpid ());
458+ constexpr size_t kNumShards = 4 ;
459+
460+ // Populate and persist, then overwrite the info segment to claim more
461+ // entries than the data segment can hold. The data segment created by
462+ // persist() for 3 entries is page-aligned to 4096 bytes. Setting
463+ // entryCount to 500 makes expectedSize = 500 * sizeof(ShmEntry) = 6000,
464+ // which exceeds the 4096-byte data segment.
465+ {
466+ ShmManager shm (shmDir, true );
467+ AccessTimeMap m (kNumShards );
468+
469+ m.set (10 , 100 );
470+ m.set (20 , 200 );
471+ m.set (30 , 300 );
472+ m.persist (shm);
473+
474+ // Overwrite the info segment with an inflated entryCount
475+ shm.removeShm (std::string (AccessTimeMap::kShmInfoName ));
476+ navy::serialization::AccessTimeMapConfig cfg;
477+ *cfg.version () = static_cast <int32_t >(AccessTimeMap::kVersion );
478+ *cfg.numShards () = static_cast <int64_t >(kNumShards );
479+ *cfg.maxSize () = 0 ;
480+ *cfg.entryCount () = 500 ; // data segment is only 4096 bytes
481+
482+ auto ioBuf = Serializer::serializeToIOBuf (cfg);
483+ auto infoAddr = shm.createShm (std::string (AccessTimeMap::kShmInfoName ),
484+ ioBuf->length ());
485+ Serializer serializer (
486+ reinterpret_cast <uint8_t *>(infoAddr.addr ),
487+ reinterpret_cast <uint8_t *>(infoAddr.addr ) + ioBuf->length ());
488+ serializer.writeToBuffer (std::move (ioBuf));
489+
490+ shm.shutDown ();
491+ }
492+
493+ // Recovery should detect size mismatch and start empty
494+ {
495+ ShmManager shm (shmDir, true );
496+ AccessTimeMap m (kNumShards );
497+ m.recover (shm);
498+ EXPECT_EQ (m.size (), 0 );
499+ shm.shutDown ();
500+ }
501+
502+ ShmManager::cleanup (shmDir, true );
503+ }
504+
505+ TEST (AccessTimeMapTest, RecoverCleansUpSegments) {
506+ const auto shmDir = " /tmp/atm_cleanup_test_" + std::to_string (::getpid ());
507+ constexpr size_t kNumShards = 4 ;
508+ constexpr size_t kMaxSize = 1000 ;
509+
510+ // Populate and persist
511+ {
512+ ShmManager shm (shmDir, true );
513+ AccessTimeMap m (kNumShards , kMaxSize );
514+
515+ m.set (10 , 100 );
516+ m.set (20 , 200 );
517+ m.persist (shm);
518+ shm.shutDown ();
519+ }
520+
521+ // Recover and verify entries loaded, then check segments are gone
522+ {
523+ ShmManager shm (shmDir, true );
524+ AccessTimeMap m (kNumShards , kMaxSize );
525+ m.recover (shm);
526+ ASSERT_EQ (m.size (), 2 );
527+ EXPECT_EQ (m.get (10 ), 100 );
528+ EXPECT_EQ (m.get (20 ), 200 );
529+
530+ // Segments should have been removed after successful recovery
531+ EXPECT_THROW (shm.attachShm (std::string (AccessTimeMap::kShmInfoName )),
532+ std::invalid_argument);
533+ EXPECT_THROW (shm.attachShm (std::string (AccessTimeMap::kShmDataName )),
534+ std::invalid_argument);
535+
536+ shm.shutDown ();
537+ }
538+
539+ ShmManager::cleanup (shmDir, true );
540+ }
541+
542+ TEST (AccessTimeMapTest, PersistOverwritesPriorSegments) {
543+ const auto shmDir = " /tmp/atm_overwrite_test_" + std::to_string (::getpid ());
544+ constexpr size_t kNumShards = 4 ;
545+
546+ {
547+ ShmManager shm (shmDir, true );
548+ AccessTimeMap m (kNumShards );
549+
550+ // First persist with two entries
551+ m.set (1 , 100 );
552+ m.set (2 , 200 );
553+ m.persist (shm);
554+
555+ // Clear the map and add a different entry
556+ m.remove (1 );
557+ m.remove (2 );
558+ m.set (3 , 300 );
559+ ASSERT_EQ (m.size (), 1 );
560+
561+ // Second persist should replace the prior segments
562+ m.persist (shm);
563+ shm.shutDown ();
564+ }
565+
566+ // Recover should only see the second persist's data
567+ {
568+ ShmManager shm (shmDir, true );
569+ AccessTimeMap m (kNumShards );
570+ m.recover (shm);
571+ ASSERT_EQ (m.size (), 1 );
572+ EXPECT_EQ (m.get (1 ), std::nullopt );
573+ EXPECT_EQ (m.get (2 ), std::nullopt );
574+ EXPECT_EQ (m.get (3 ), 300 );
575+ shm.shutDown ();
576+ }
577+
578+ ShmManager::cleanup (shmDir, true );
579+ }
580+
581+ TEST (AccessTimeMapTest, RecoverZeroEntryCount) {
582+ const auto shmDir = " /tmp/atm_zero_count_test_" + std::to_string (::getpid ());
583+ constexpr size_t kNumShards = 4 ;
584+
585+ // Create an info segment with valid version/numShards but entryCount = 0.
586+ // This exercises the entryCount == 0 branch in recover(), which is NOT
587+ // reached by PersistEmptyMap (persist() returns early for empty maps
588+ // without creating segments, so recovery hits the "no shm" path instead).
589+ {
590+ ShmManager shm (shmDir, true );
591+
592+ navy::serialization::AccessTimeMapConfig cfg;
593+ *cfg.version () = static_cast <int32_t >(AccessTimeMap::kVersion );
594+ *cfg.numShards () = static_cast <int64_t >(kNumShards );
595+ *cfg.maxSize () = 0 ;
596+ *cfg.entryCount () = 0 ;
597+
598+ auto ioBuf = Serializer::serializeToIOBuf (cfg);
599+ auto infoAddr = shm.createShm (std::string (AccessTimeMap::kShmInfoName ),
600+ ioBuf->length ());
601+ Serializer serializer (
602+ reinterpret_cast <uint8_t *>(infoAddr.addr ),
603+ reinterpret_cast <uint8_t *>(infoAddr.addr ) + ioBuf->length ());
604+ serializer.writeToBuffer (std::move (ioBuf));
605+
606+ shm.shutDown ();
607+ }
608+
609+ // Recovery should hit the entryCount == 0 branch and start empty
610+ {
611+ ShmManager shm (shmDir, true );
612+ AccessTimeMap m (kNumShards );
613+ m.recover (shm);
614+ EXPECT_EQ (m.size (), 0 );
615+ shm.shutDown ();
616+ }
617+
618+ ShmManager::cleanup (shmDir, true );
619+ }
620+
425621} // namespace tests
426622} // namespace cachelib
427623} // namespace facebook
0 commit comments