pw_kvs: Don't write key values that don't change

Do not write incoming key vales that match the already-stored entry. The
check it done by comparing entry state, value length, checksum, and
actual value.

Change-Id: Ibf2f1145cf59144b832af8ae8b2e5580e91091a1
diff --git a/pw_kvs/BUILD b/pw_kvs/BUILD
index dbbddd3..15899ad 100644
--- a/pw_kvs/BUILD
+++ b/pw_kvs/BUILD
@@ -52,6 +52,7 @@
     ],
     includes = ["public"],
     deps = [
+        "//pw_assert",
         "//pw_checksum",
         "//pw_containers",
         "//pw_log",
diff --git a/pw_kvs/BUILD.gn b/pw_kvs/BUILD.gn
index 0f38b2b..43ef1ec 100644
--- a/pw_kvs/BUILD.gn
+++ b/pw_kvs/BUILD.gn
@@ -52,6 +52,7 @@
     dir_pw_status,
   ]
   deps = [
+    dir_pw_assert,
     dir_pw_checksum,
     dir_pw_log,
   ]
diff --git a/pw_kvs/CMakeLists.txt b/pw_kvs/CMakeLists.txt
index 65b31ee..d2f3380 100644
--- a/pw_kvs/CMakeLists.txt
+++ b/pw_kvs/CMakeLists.txt
@@ -17,6 +17,7 @@
     pw_containers
     pw_status
   PRIVATE_DEPS
+    pw_assert
     pw_checksum
     pw_log
     pw_string
diff --git a/pw_kvs/entry.cc b/pw_kvs/entry.cc
index c393253..0429455 100644
--- a/pw_kvs/entry.cc
+++ b/pw_kvs/entry.cc
@@ -151,6 +151,31 @@
   return StatusWithSize(read_size);
 }
 
+Status Entry::ValueMatches(span<const std::byte> value) const {
+  if (value_size() != value.size_bytes()) {
+    return Status::NOT_FOUND;
+  }
+
+  Address address = address_ + sizeof(EntryHeader) + key_length();
+  Address end = address + value_size();
+  const std::byte* value_ptr = value.data();
+
+  std::array<std::byte, 2 * kMinAlignmentBytes> buffer;
+  while (address < end) {
+    const size_t read_size = std::min(size_t(end - address), buffer.size());
+    TRY(partition_->Read(address, span(buffer).first(read_size)));
+
+    if (std::memcmp(buffer.data(), value_ptr, read_size) != 0) {
+      return Status::NOT_FOUND;
+    }
+
+    address += read_size;
+    value_ptr += read_size;
+  }
+
+  return Status::OK;
+}
+
 Status Entry::VerifyChecksum(string_view key, span<const byte> value) const {
   if (checksum_algo_ == nullptr) {
     return header_.checksum == 0 ? Status::OK : Status::DATA_LOSS;
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
index 80bb8cc..fa85c42 100644
--- a/pw_kvs/key_value_store.cc
+++ b/pw_kvs/key_value_store.cc
@@ -22,6 +22,7 @@
 #include <type_traits>
 
 #define PW_LOG_USE_ULTRA_SHORT_NAMES 1
+#include "pw_assert/assert.h"
 #include "pw_kvs_private/macros.h"
 #include "pw_log/log.h"
 
@@ -619,7 +620,7 @@
   Entry entry;
   TRY(ReadEntry(metadata, entry));
 
-  return WriteEntry(key, value, new_state, &metadata, entry.size());
+  return WriteEntry(key, value, new_state, &metadata, &entry);
 }
 
 Status KeyValueStore::WriteEntryForNewKey(string_view key,
@@ -637,22 +638,46 @@
                                  span<const byte> value,
                                  EntryState new_state,
                                  EntryMetadata* prior_metadata,
-                                 size_t prior_size) {
-  const size_t entry_size = Entry::size(partition_, key, value);
+                                 const Entry* prior_entry) {
+  Entry entry = CreateEntry(key, value, new_state);
+
+  // If new entry and prior entry have matching value size, state, and checksum,
+  // check if the values match. Directly compare the prior and new values
+  // because the checksum can not be depended on to establish equality, it can
+  // only be depended on to establish inequality.
+  if (prior_entry != nullptr &&
+      prior_entry->value_size() == entry.value_size() &&
+      prior_metadata->state() == new_state &&
+      prior_entry->checksum() == entry.checksum() &&
+      prior_entry->ValueMatches(value).ok()) {
+    // The new value matches the prior value, don't need to write anything.
+    // Just keep the existing entry.
+    DBG("Write for key 0x%08x with matching value skipped",
+        unsigned(prior_metadata->hash()));
+    return Status::OK;
+  }
 
   // List of addresses for sectors with space for this entry.
   Address* reserved_addresses = entry_cache_.TempReservedAddressesForWrite();
 
   // Find addresses to write the entry to. This may involve garbage collecting
   // one or more sectors.
+  const size_t entry_size = Entry::size(partition_, key, value);
   TRY(GetAddressesForWrite(reserved_addresses, entry_size));
 
+  // Commiting to do the write, time to update last_transaction_id_. Update
+  // here, rather in CreateEntry, so last_transaction_id_ only increments for
+  // writes that are actually attempted.
+  last_transaction_id_ += 1;
+  PW_CHECK(last_transaction_id_ == entry.transaction_id());
+
   // Write the entry at the first address that was found.
-  Entry entry = CreateEntry(reserved_addresses[0], key, value, new_state);
+  entry.set_address(reserved_addresses[0]);
   TRY(AppendEntry(entry, key, value));
 
   // After writing the first entry successfully, update the key descriptors.
   // Once a single new the entry is written, the old entries are invalidated.
+  size_t prior_size = prior_entry != nullptr ? prior_entry->size() : 0;
   EntryMetadata new_metadata =
       CreateOrUpdateKeyDescriptor(entry, key, prior_metadata, prior_size);
 
@@ -1181,8 +1206,7 @@
   return FixErrors();
 }
 
-KeyValueStore::Entry KeyValueStore::CreateEntry(Address address,
-                                                string_view key,
+KeyValueStore::Entry KeyValueStore::CreateEntry(string_view key,
                                                 span<const byte> value,
                                                 EntryState state) {
   // Always bump the transaction ID when creating a new entry.
@@ -1196,19 +1220,20 @@
   //   2. The transaction ID is NOT incremented, because of the failure
   //   3. (later) A new entry is written, re-using the transaction ID (oops)
   //
-  // By always burning transaction IDs, the above problem can't happen.
-  last_transaction_id_ += 1;
+  // By always burning transaction IDs, the above problem can't happen. The
+  // actual updating of last_transaction_id_ is done once the write method is
+  // ready to commit to attempting an actual write.
+  uint32_t new_transaction_id = last_transaction_id_ + 1;
+
+  // Set address to zero, the address of the entry is set later.
+  const Address address = 0;
 
   if (state == EntryState::kDeleted) {
     return Entry::Tombstone(
-        partition_, address, formats_.primary(), key, last_transaction_id_);
+        partition_, address, formats_.primary(), key, new_transaction_id);
   }
-  return Entry::Valid(partition_,
-                      address,
-                      formats_.primary(),
-                      key,
-                      value,
-                      last_transaction_id_);
+  return Entry::Valid(
+      partition_, address, formats_.primary(), key, value, new_transaction_id);
 }
 
 void KeyValueStore::LogDebugInfo() const {
diff --git a/pw_kvs/key_value_store_map_test.cc b/pw_kvs/key_value_store_map_test.cc
index 5bd8535..4b15880 100644
--- a/pw_kvs/key_value_store_map_test.cc
+++ b/pw_kvs/key_value_store_map_test.cc
@@ -197,7 +197,7 @@
     std::cout << "Entries: " << map_.size() << '\n';
     std::cout << "------------------------------------------------\n";
     for (const auto& [key, value] : map_) {
-      std::cout << key << " = " << value << '\n';
+      std::cout << key << " = [" << value << "]\n";
       map_keys.insert(key);
     }
     std::cout << "\\===============================================/\n";
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
index c6c3b0a..d2f33fc 100644
--- a/pw_kvs/key_value_store_test.cc
+++ b/pw_kvs/key_value_store_test.cc
@@ -717,7 +717,7 @@
                                                           format);
   ASSERT_OK(kvs.Init());
 
-  // Add two entries with different keys and values.
+  // Add one entry.
   const char* key = "Key1";
   DBG("PUT value for key: %s", key);
   uint8_t written_value = 0xDA;
@@ -732,6 +732,38 @@
   EXPECT_EQ(kvs.size(), 1u);
 }
 
+TEST(InMemoryKvs, WriteOneKeyValueMultipleTimes) {
+  // Create and erase the fake flash.
+  Flash flash;
+  ASSERT_OK(flash.partition.Erase());
+
+  // Create and initialize the KVS.
+  constexpr EntryFormat format{.magic = 0xBAD'C0D3, .checksum = nullptr};
+  KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs(&flash.partition,
+                                                          format);
+  ASSERT_OK(kvs.Init());
+
+  // Add one entry, with the same key and value, multiple times.
+  const char* key = "Key1";
+  uint8_t written_value = 0xDA;
+  for (int i = 0; i < 50; i++) {
+    DBG("PUT [%d] value for key: %s", i, key);
+    ASSERT_OK(kvs.Put(key, written_value));
+    EXPECT_EQ(kvs.size(), 1u);
+  }
+
+  DBG("GET value for key: %s", key);
+  uint8_t actual_value;
+  ASSERT_OK(kvs.Get(key, &actual_value));
+  EXPECT_EQ(actual_value, written_value);
+
+  // Verify that only one entry was written to the KVS.
+  EXPECT_EQ(kvs.size(), 1u);
+  EXPECT_EQ(kvs.transaction_count(), 1u);
+  KeyValueStore::StorageStats stats = kvs.GetStorageStats();
+  EXPECT_EQ(stats.reclaimable_bytes, 0u);
+}
+
 TEST(InMemoryKvs, Basic) {
   const char* key1 = "Key1";
   const char* key2 = "Key2";
diff --git a/pw_kvs/public/pw_kvs/internal/entry.h b/pw_kvs/public/pw_kvs/internal/entry.h
index aa81e16..a8f297f 100644
--- a/pw_kvs/public/pw_kvs/internal/entry.h
+++ b/pw_kvs/public/pw_kvs/internal/entry.h
@@ -122,6 +122,8 @@
   StatusWithSize ReadValue(span<std::byte> buffer,
                            size_t offset_bytes = 0) const;
 
+  Status ValueMatches(span<const std::byte> value) const;
+
   Status VerifyChecksum(std::string_view key,
                         span<const std::byte> value) const;
 
@@ -156,6 +158,8 @@
 
   uint32_t magic() const { return header_.magic; }
 
+  uint32_t checksum() const { return header_.checksum; }
+
   uint32_t transaction_id() const { return header_.transaction_id; }
 
   // True if this is a tombstone entry.
diff --git a/pw_kvs/public/pw_kvs/key_value_store.h b/pw_kvs/public/pw_kvs/key_value_store.h
index 061b512..e1a3b18 100644
--- a/pw_kvs/public/pw_kvs/key_value_store.h
+++ b/pw_kvs/public/pw_kvs/key_value_store.h
@@ -398,7 +398,7 @@
                     span<const std::byte> value,
                     EntryState new_state,
                     EntryMetadata* prior_metadata = nullptr,
-                    size_t prior_size = 0);
+                    const internal::Entry* prior_entry = nullptr);
 
   EntryMetadata CreateOrUpdateKeyDescriptor(const Entry& new_entry,
                                             std::string_view key,
@@ -459,8 +459,7 @@
 
   Status Repair();
 
-  internal::Entry CreateEntry(Address address,
-                              std::string_view key,
+  internal::Entry CreateEntry(std::string_view key,
                               span<const std::byte> value,
                               EntryState state);