pw_kvs: Add garbage collection when fixing redundancy

- When adding redundancy make sure to garbage collect as part of finding
  a sector to write to.
- Move the KVS initialization check out of GarbageCollect.
- Do not repair during a sector GC to prevent infinite GC loops.

Change-Id: Ic16d32a30bbddfd89e2497a2ef0d5572a265189d
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
index 1f3737e..539be83 100644
--- a/pw_kvs/key_value_store.cc
+++ b/pw_kvs/key_value_store.cc
@@ -83,6 +83,8 @@
   if (!error_detected_) {
     initialized_ = InitializationState::kReady;
   } else {
+    initialized_ = InitializationState::kNeedsMaintenance;
+
     if (options_.recovery != ErrorRecovery::kManual) {
       size_t pre_fix_redundancy_errors =
           error_stats_.missing_redundant_entries_recovered;
@@ -99,14 +101,11 @@
         initialized_ = InitializationState::kReady;
       } else if (recovery_status == Status::RESOURCE_EXHAUSTED) {
         WRN("KVS init: Unable to maintain required free sector");
-        initialized_ = InitializationState::kNeedsMaintenance;
       } else {
         WRN("KVS init: Corruption detected and unable repair");
-        initialized_ = InitializationState::kNeedsMaintenance;
       }
     } else {
       WRN("KVS init: Corruption detected, no repair attempted due to options");
-      initialized_ = InitializationState::kNeedsMaintenance;
     }
   }
 
@@ -874,20 +873,24 @@
   return Status::OK;
 }
 
-Status KeyValueStore::GarbageCollect(span<const Address> reserved_addresses) {
+Status KeyValueStore::PartialMaintenance() {
   if (initialized_ == InitializationState::kNotInitialized) {
     return Status::FAILED_PRECONDITION;
   }
 
-  DBG("Garbage Collect a single sector");
-  for (Address address : reserved_addresses) {
-    DBG("   Avoid address %u", unsigned(address));
-  }
-
+  CheckForErrors();
   // Do automatic repair, if KVS options allow for it.
   if (error_detected_ && options_.recovery != ErrorRecovery::kManual) {
     TRY(Repair());
   }
+  return GarbageCollect(span<const Address>());
+}
+
+Status KeyValueStore::GarbageCollect(span<const Address> reserved_addresses) {
+  DBG("Garbage Collect a single sector");
+  for (Address address : reserved_addresses) {
+    DBG("   Avoid address %u", unsigned(address));
+  }
 
   // Step 1: Find the sector to garbage collect
   SectorDescriptor* sector_to_gc =
@@ -993,17 +996,13 @@
 
 // Add any missing redundant entries/copies for a key.
 Status KeyValueStore::AddRedundantEntries(EntryMetadata& metadata) {
-  SectorDescriptor* new_sector;
-
   Entry entry;
-
   TRY(ReadEntry(metadata, entry));
   TRY(entry.VerifyChecksumInFlash());
 
-  for (size_t i = metadata.addresses().size();
-       metadata.addresses().size() < redundancy();
-       i++) {
-    TRY(sectors_.FindSpace(&new_sector, entry.size(), metadata.addresses()));
+  while (metadata.addresses().size() < redundancy()) {
+    SectorDescriptor* new_sector;
+    TRY(GetSectorForWrite(&new_sector, entry.size(), metadata.addresses()));
 
     Address new_address = sectors_.NextWritableAddress(*new_sector);
     TRY(CopyEntryToSector(entry, new_sector, new_address));
diff --git a/pw_kvs/key_value_store_binary_format_test.cc b/pw_kvs/key_value_store_binary_format_test.cc
index b900d44..f2f8308 100644
--- a/pw_kvs/key_value_store_binary_format_test.cc
+++ b/pw_kvs/key_value_store_binary_format_test.cc
@@ -960,5 +960,101 @@
   EXPECT_EQ(stats.missing_redundant_entries_recovered, 4u);
 }
 
+class InitializedLazyRecoveryKvs : public ::testing::Test {
+ protected:
+  static constexpr auto kInitialContents =
+      AsBytes(kEntry1, kEntry2, kEntry3, kEntry4);
+
+  InitializedLazyRecoveryKvs()
+      : flash_(internal::Entry::kMinAlignmentBytes),
+        partition_(&flash_),
+        kvs_(&partition_,
+             {.magic = kMagic, .checksum = &checksum},
+             kRecoveryLazyGcOptions) {
+    partition_.Erase();
+    std::memcpy(flash_.buffer().data(),
+                kInitialContents.data(),
+                kInitialContents.size());
+
+    EXPECT_EQ(Status::OK, kvs_.Init());
+  }
+
+  FakeFlashBuffer<512, 8> flash_;
+  FlashPartition partition_;
+  KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
+};
+
+// Block of data to use for entry value. Sized to 470 so the total entry results
+// in the 512 byte sector having 16 bytes remaining.
+constexpr uint8_t test_data[470] = {1, 2, 3, 4, 5, 6};
+
+// Test a KVS with a number of entries, several sectors that are nearly full
+// of stale (reclaimable) space, and not enough writable (free) space to add a
+// redundant copy for any of the entries. Tests that the add redundancy step of
+// repair is able to use garbage collection to free up space needed for the new
+// copies.
+TEST_F(InitializedLazyRecoveryKvs, AddRedundancyToKvsFullOfStaleData) {
+  // Verify the pre-initialized key are present in the KVS.
+  ASSERT_CONTAINS_ENTRY("key1", "value1");
+  ASSERT_CONTAINS_ENTRY("k2", "value2");
+  ASSERT_CONTAINS_ENTRY("k3y", "value3");
+  ASSERT_CONTAINS_ENTRY("4k", "value4");
+
+  EXPECT_EQ(false, kvs_.error_detected());
+
+  auto stats = kvs_.GetStorageStats();
+  EXPECT_EQ(stats.in_use_bytes, (128u * kvs_.redundancy()));
+  EXPECT_EQ(stats.reclaimable_bytes, 0u);
+  EXPECT_EQ(stats.writable_bytes, 7 * 512u - (128u * kvs_.redundancy()));
+  EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
+  EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
+
+  // Add a near-sector size key entry to fill the KVS with a valid large entry
+  // and stale data.
+
+  EXPECT_EQ(Status::OK, kvs_.Put("big_key", test_data));
+  EXPECT_EQ(Status::OK, kvs_.Put("big_key", test_data));
+  EXPECT_EQ(Status::OK, kvs_.Put("big_key", test_data));
+  EXPECT_EQ(Status::OK, kvs_.Put("big_key", test_data));
+  EXPECT_EQ(Status::OK, kvs_.Put("big_key", test_data));
+  EXPECT_EQ(Status::OK, kvs_.Put("big_key", test_data));
+
+  // Instantiate a new KVS with redundancy of 2. This KVS should add an extra
+  // copy of each valid key as part of the init process. Because there is not
+  // enough writable space, the adding redundancy will need to garbage collect
+  // two sectors.
+  KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 2> local_kvs(
+      &partition_,
+      {.magic = kMagic, .checksum = &checksum},
+      kRecoveryLazyGcOptions);
+  ASSERT_EQ(Status::OK, local_kvs.Init());
+
+  // Verify no errors found in the new KVS and all the entries are present.
+  EXPECT_EQ(false, local_kvs.error_detected());
+  ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "key1", "value1");
+  ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k2", "value2");
+  ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k3y", "value3");
+  ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "4k", "value4");
+  StatusWithSize big_key_size = local_kvs.ValueSize("big_key");
+  EXPECT_EQ(Status::OK, big_key_size.status());
+  EXPECT_EQ(sizeof(test_data), big_key_size.size());
+
+  // Verify that storage stats of the new redundant KVS match expected values.
+  stats = local_kvs.GetStorageStats();
+
+  // Expected in-use bytes is size of (pre-init keys + big key) * redundancy.
+  EXPECT_EQ(stats.in_use_bytes, ((128u + 496u) * local_kvs.redundancy()));
+
+  // Expected reclaimable space is number of stale entries remaining for big_key
+  // (3 after GC to add redundancy) * total sizeof big_key entry (496 bytes).
+
+  EXPECT_EQ(stats.reclaimable_bytes, 496u * 3u);
+  // Expected writable bytes is total writable size (512 * 7) - valid bytes (in
+  // use) - reclaimable bytes.
+  EXPECT_EQ(stats.writable_bytes, 848u);
+  EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
+  EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
+}
+
 }  // namespace
 }  // namespace pw::kvs
diff --git a/pw_kvs/public/pw_kvs/key_value_store.h b/pw_kvs/public/pw_kvs/key_value_store.h
index e8fde2c..4ca5102 100644
--- a/pw_kvs/public/pw_kvs/key_value_store.h
+++ b/pw_kvs/public/pw_kvs/key_value_store.h
@@ -188,10 +188,7 @@
   // recovery, will do any needed repairing of corruption. Does garbage
   // collection of part of the KVS, typically a single sector or similar unit
   // that makes sense for the KVS implementation.
-  Status PartialMaintenance() {
-    CheckForErrors();
-    return GarbageCollect(span<const Address>());
-  }
+  Status PartialMaintenance();
 
   void LogDebugInfo() const;