pw_blob_store: Add support for deferred writes

- Add support for deferred writes.
- Use DeferredWriter that is similar to BlobWriter, but does deferred
  writes.
- Decouple write buffer size from flash write size.
- Return DATA_LOSS for all write errors and treat blob as unwritable
  until erased.

Change-Id: I4eeb763875560d058188687e19d61f8ee4582677
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/17641
Commit-Queue: David Rogers <davidrogers@google.com>
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_blob_store/BUILD b/pw_blob_store/BUILD
index dc8689f..188ede7 100644
--- a/pw_blob_store/BUILD
+++ b/pw_blob_store/BUILD
@@ -53,3 +53,19 @@
         "//pw_unit_test",
     ],
 )
+
+pw_cc_test(
+    name = "blob_store_deferred_write_test",
+    srcs = [
+        "blob_store_deferred_write_test.cc",
+    ],
+    deps = [
+        ":pw_blob_store",
+        "//pw_kvs:crc16",
+        "//pw_kvs:fake_flash",
+        "//pw_kvs:fake_flash_test_key_value_store",
+        "//pw_log",
+        "//pw_random",
+        "//pw_unit_test",
+    ],
+)
diff --git a/pw_blob_store/BUILD.gn b/pw_blob_store/BUILD.gn
index 4576ff4..99c2e03 100644
--- a/pw_blob_store/BUILD.gn
+++ b/pw_blob_store/BUILD.gn
@@ -39,7 +39,10 @@
 }
 
 pw_test_group("tests") {
-  tests = [ ":blob_store_test" ]
+  tests = [
+    ":blob_store_test",
+    ":blob_store_deferred_write_test",
+  ]
 }
 
 pw_test("blob_store_test") {
@@ -54,6 +57,18 @@
   sources = [ "blob_store_test.cc" ]
 }
 
+pw_test("blob_store_deferred_write_test") {
+  deps = [
+    ":pw_blob_store",
+    "$dir_pw_kvs:crc16",
+    "$dir_pw_kvs:fake_flash",
+    "$dir_pw_kvs:fake_flash_test_key_value_store",
+    dir_pw_log,
+    dir_pw_random,
+  ]
+  sources = [ "blob_store_deferred_write_test.cc" ]
+}
+
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
 }
diff --git a/pw_blob_store/blob_store.cc b/pw_blob_store/blob_store.cc
index 27da48a..d594d3f 100644
--- a/pw_blob_store/blob_store.cc
+++ b/pw_blob_store/blob_store.cc
@@ -17,6 +17,7 @@
 #include <algorithm>
 
 #include "pw_log/log.h"
+#include "pw_status/try.h"
 
 namespace pw::blob_store {
 
@@ -30,12 +31,13 @@
   PW_CHECK_UINT_EQ((write_buffer_size_alignment), 0);
   PW_CHECK_UINT_GE(write_buffer_.size_bytes(), partition_.alignment_bytes());
   PW_CHECK_UINT_LE(write_buffer_.size_bytes(), partition_.sector_size_bytes());
+  PW_CHECK_UINT_GE(write_buffer_.size_bytes(), flash_write_size_bytes_);
+  PW_CHECK_UINT_GE(flash_write_size_bytes_, partition_.alignment_bytes());
 
   bool erased = false;
   if (partition_.IsErased(&erased).ok() && erased) {
     flash_erased_ = true;
-  } else {
-    flash_erased_ = false;
+    valid_data_ = true;
   }
 
   ResetChecksum();
@@ -91,48 +93,59 @@
 }
 
 Status BlobStore::CloseWrite() {
-  PW_DCHECK(writer_open_);
-  Status status = Status::OK;
-
-  // Only need to finish a blob if the blob has any bytes.
-  if (write_address_ > 0) {
-    // Flush bytes in buffer.
-    if (write_address_ != flash_address_) {
-      PW_CHECK_UINT_GE(write_address_, flash_address_);
-      size_t bytes_in_buffer = write_address_ - flash_address_;
-
-      // Zero out the remainder of the buffer.
-      auto zero_span = write_buffer_.subspan(bytes_in_buffer);
-      std::memset(zero_span.data(), 0, zero_span.size_bytes());
-
-      status = CommitToFlash(write_buffer_, bytes_in_buffer);
+  auto do_close_write = [&]() -> Status {
+    // If not valid to write, there was data loss and the close will result in a
+    // not valid blob. Don't need to flush any write buffered bytes.
+    if (!ValidToWrite()) {
+      return Status::DATA_LOSS;
     }
 
-    if (status.ok()) {
-      // Buffer should be clear unless there was a write error.
-      PW_DCHECK_UINT_EQ(flash_address_, write_address_);
-      // Should not be completing a blob write when it has completed metadata.
-      PW_DCHECK(!metadata_.complete);
-
-      metadata_ = {
-          .checksum = 0, .data_size_bytes = flash_address_, .complete = true};
-
-      if (checksum_algo_ != nullptr) {
-        ConstByteSpan checksum = checksum_algo_->Finish();
-        std::memcpy(&metadata_.checksum,
-                    checksum.data(),
-                    std::min(checksum.size(), sizeof(metadata_.checksum)));
-      }
-
-      status = kvs_.Put(MetadataKey(), metadata_);
+    if (write_address_ == 0) {
+      return Status::OK;
     }
-  }
+
+    PW_LOG_DEBUG("Close with %u bytes in write buffer",
+                 static_cast<unsigned>(WriteBufferBytesUsed()));
+
+    // Do a Flush of any flash_write_size_bytes_ sized chunks so any remaining
+    // bytes in the write buffer are less than flash_write_size_bytes_.
+    PW_TRY(Flush());
+
+    // If any bytes remain in buffer it is because it is a chunk less than
+    // flash_write_size_bytes_. Pad the chunk to flash_write_size_bytes_ and
+    // write it to flash.
+    if (!WriteBufferEmpty()) {
+      PW_TRY(FlushFinalPartialChunk());
+    }
+
+    // If things are still good, save the blob metadata.
+    // Should not be completing a blob write when it has completed metadata.
+    PW_DCHECK(WriteBufferEmpty());
+    PW_DCHECK(!metadata_.complete);
+
+    metadata_ = {
+        .checksum = 0, .data_size_bytes = flash_address_, .complete = true};
+    if (checksum_algo_ != nullptr) {
+      ConstByteSpan checksum = checksum_algo_->Finish();
+      std::memcpy(&metadata_.checksum,
+                  checksum.data(),
+                  std::min(checksum.size(), sizeof(metadata_.checksum)));
+    }
+
+    PW_TRY(kvs_.Put(MetadataKey(), metadata_));
+    PW_TRY(ValidateChecksum());
+
+    return Status::OK;
+  };
+
+  const Status status = do_close_write();
   writer_open_ = false;
 
-  if (status.ok()) {
-    return ValidateChecksum();
+  if (!status.ok()) {
+    valid_data_ = false;
+    return Status::DATA_LOSS;
   }
-  return Status::DATA_LOSS;
+  return Status::OK;
 }
 
 Status BlobStore::CloseRead() {
@@ -142,8 +155,9 @@
 }
 
 Status BlobStore::Write(ConstByteSpan data) {
-  PW_DCHECK(writer_open_);
-
+  if (!ValidToWrite()) {
+    return Status::DATA_LOSS;
+  }
   if (data.size_bytes() == 0) {
     return Status::OK;
   }
@@ -154,8 +168,10 @@
     return Status::RESOURCE_EXHAUSTED;
   }
 
-  if (write_address_ == 0 && !erased()) {
-    Erase();
+  Status status = EraseIfNeeded();
+  // TODO: switch to TRY once available.
+  if (!status.ok()) {
+    return Status::DATA_LOSS;
   }
 
   // Write in (up to) 3 steps:
@@ -166,22 +182,26 @@
 
   // Step 1) If there is any data in the write buffer, finish filling write
   //         buffer and if full write it to flash.
-  const size_t write_chunk_bytes = write_buffer_.size_bytes();
   if (!WriteBufferEmpty()) {
-    PW_CHECK_UINT_GE(write_address_, flash_address_);
-    size_t bytes_in_buffer = write_address_ - flash_address_;
-    PW_CHECK_UINT_GE(write_chunk_bytes, bytes_in_buffer);
+    size_t bytes_in_buffer = WriteBufferBytesUsed();
 
-    size_t buffer_remaining = write_chunk_bytes - bytes_in_buffer;
+    // Non-deferred writes only use the first flash_write_size_bytes_ of the
+    // write buffer to buffer writes less than flash_write_size_bytes_.
+    PW_CHECK_UINT_GT(flash_write_size_bytes_, bytes_in_buffer);
 
-    // Add bytes up to filling the write buffer.
+    // Not using WriteBufferBytesFree() because non-deferred writes (which
+    // is this method) only use the first flash_write_size_bytes_ of the write
+    // buffer.
+    size_t buffer_remaining = flash_write_size_bytes_ - bytes_in_buffer;
+
+    // Add bytes up to filling the flash write size.
     size_t add_bytes = std::min(buffer_remaining, data.size_bytes());
     std::memcpy(write_buffer_.data() + bytes_in_buffer, data.data(), add_bytes);
     write_address_ += add_bytes;
     bytes_in_buffer += add_bytes;
     data = data.subspan(add_bytes);
 
-    if (bytes_in_buffer != write_chunk_bytes) {
+    if (bytes_in_buffer != flash_write_size_bytes_) {
       // If there was not enough bytes to finish filling the write buffer, there
       // should not be any bytes left.
       PW_DCHECK(data.size_bytes() == 0);
@@ -190,8 +210,9 @@
 
     // The write buffer is full, flush to flash.
     Status status = CommitToFlash(write_buffer_);
+    // TODO: switch to TRY once available.
     if (!status.ok()) {
-      return status;
+      return Status::DATA_LOSS;
     }
 
     PW_DCHECK(WriteBufferEmpty());
@@ -202,17 +223,17 @@
 
   // Step 2) Write as many block-sized chunks as the data has remaining after
   //         step 1.
-  // TODO: Decouple write buffer size from optimal bulk write size.
-  while (data.size_bytes() >= write_chunk_bytes) {
+  while (data.size_bytes() >= flash_write_size_bytes_) {
     PW_DCHECK(WriteBufferEmpty());
 
-    write_address_ += write_chunk_bytes;
-    Status status = CommitToFlash(data.first(write_chunk_bytes));
+    write_address_ += flash_write_size_bytes_;
+    Status status = CommitToFlash(data.first(flash_write_size_bytes_));
+    // TODO: switch to TRY once available.
     if (!status.ok()) {
-      return status;
+      return Status::DATA_LOSS;
     }
 
-    data = data.subspan(write_chunk_bytes);
+    data = data.subspan(flash_write_size_bytes_);
   }
 
   // step 3) Put any remaining bytes to the buffer. Put the bytes starting at
@@ -228,6 +249,130 @@
   return Status::OK;
 }
 
+Status BlobStore::AddToWriteBuffer(ConstByteSpan data) {
+  if (!ValidToWrite()) {
+    return Status::DATA_LOSS;
+  }
+  if (WriteBytesRemaining() == 0) {
+    return Status::OUT_OF_RANGE;
+  }
+  if (WriteBufferBytesFree() < data.size_bytes()) {
+    return Status::RESOURCE_EXHAUSTED;
+  }
+
+  size_t bytes_in_buffer = WriteBufferBytesUsed();
+
+  std::memcpy(
+      write_buffer_.data() + bytes_in_buffer, data.data(), data.size_bytes());
+  write_address_ += data.size_bytes();
+
+  return Status::OK;
+}
+
+Status BlobStore::Flush() {
+  if (!ValidToWrite()) {
+    return Status::DATA_LOSS;
+  }
+  if (WriteBufferBytesUsed() == 0) {
+    return Status::OK;
+  }
+  // Don't need to check available space, AddToWriteBuffer() will not enqueue
+  // more than can be written to flash.
+
+  Status status = EraseIfNeeded();
+  // TODO: switch to TRY once available.
+  if (!status.ok()) {
+    return Status::DATA_LOSS;
+  }
+
+  ByteSpan data = std::span(write_buffer_.data(), WriteBufferBytesUsed());
+  while (data.size_bytes() >= flash_write_size_bytes_) {
+    Status status = CommitToFlash(data.first(flash_write_size_bytes_));
+    // TODO: switch to TRY once available.
+    if (!status.ok()) {
+      return Status::DATA_LOSS;
+    }
+
+    data = data.subspan(flash_write_size_bytes_);
+  }
+
+  // Only a multiple of flash_write_size_bytes_ are written in the flush. Any
+  // remainder is held until later for either a flush with
+  // flash_write_size_bytes buffered or the writer is closed.
+  if (!WriteBufferEmpty()) {
+    PW_DCHECK_UINT_EQ(data.size_bytes(), WriteBufferBytesUsed());
+    // For any leftover bytes less than the flash write size, move them to the
+    // start of the bufer.
+    std::memmove(write_buffer_.data(), data.data(), data.size_bytes());
+  } else {
+    PW_DCHECK_UINT_EQ(data.size_bytes(), 0);
+  }
+
+  return Status::OK;
+}
+
+Status BlobStore::FlushFinalPartialChunk() {
+  size_t bytes_in_buffer = WriteBufferBytesUsed();
+
+  PW_DCHECK_UINT_GT(bytes_in_buffer, 0);
+  PW_DCHECK_UINT_LE(bytes_in_buffer, flash_write_size_bytes_);
+  PW_DCHECK_UINT_LE(flash_write_size_bytes_, WriteBytesRemaining());
+
+  PW_LOG_DEBUG(
+      "  Remainder %u bytes in write buffer to zero-pad to flash write "
+      "size and commit",
+      static_cast<unsigned>(bytes_in_buffer));
+
+  // Zero out the remainder of the buffer.
+  auto zero_span = write_buffer_.subspan(bytes_in_buffer);
+  std::memset(zero_span.data(), 0, zero_span.size_bytes());
+  // TODO: look in to using flash erased value for fill, to possibly allow
+  // better resuming of writing.
+
+  ConstByteSpan remaining_bytes = write_buffer_.first(flash_write_size_bytes_);
+  return CommitToFlash(remaining_bytes, bytes_in_buffer);
+}
+
+Status BlobStore::CommitToFlash(ConstByteSpan source, size_t data_bytes) {
+  if (data_bytes == 0) {
+    data_bytes = source.size_bytes();
+  }
+  flash_erased_ = false;
+  valid_data_ = true;
+  StatusWithSize result = partition_.Write(flash_address_, source);
+  flash_address_ += data_bytes;
+  if (checksum_algo_ != nullptr) {
+    checksum_algo_->Update(source.first(data_bytes));
+  }
+
+  if (!result.status().ok()) {
+    valid_data_ = false;
+  }
+
+  return result.status();
+}
+
+// Needs to be in .cc file since PW_CHECK doesn't like being in .h files.
+size_t BlobStore::WriteBufferBytesUsed() const {
+  PW_CHECK_UINT_GE(write_address_, flash_address_);
+  return write_address_ - flash_address_;
+}
+
+// Needs to be in .cc file since PW_DCHECK doesn't like being in .h files.
+size_t BlobStore::WriteBufferBytesFree() const {
+  PW_DCHECK_UINT_GE(write_buffer_.size_bytes(), WriteBufferBytesUsed());
+  size_t buffer_remaining = write_buffer_.size_bytes() - WriteBufferBytesUsed();
+  return std::min(buffer_remaining, WriteBytesRemaining());
+}
+
+Status BlobStore::EraseIfNeeded() {
+  if (flash_address_ == 0) {
+    // Always just erase. Erase is smart enough to only erase if needed.
+    return Erase();
+  }
+  return Status::OK;
+}
+
 StatusWithSize BlobStore::Read(size_t offset, ByteSpan dest) {
   (void)offset;
   (void)dest;
@@ -235,7 +380,7 @@
 }
 
 Result<ByteSpan> BlobStore::GetMemoryMappedBlob() const {
-  if (write_address_ == 0 || !valid_data_) {
+  if (flash_address_ == 0 || !valid_data_) {
     return Status::FAILED_PRECONDITION;
   }
 
@@ -249,8 +394,6 @@
 size_t BlobStore::ReadableDataBytes() const { return flash_address_; }
 
 Status BlobStore::Erase() {
-  PW_DCHECK(writer_open_);
-
   // Can't erase if any readers are open, since this would erase flash right out
   // from under them.
   if (readers_open_ != 0) {
@@ -259,9 +402,10 @@
 
   // If already erased our work here is done.
   if (flash_erased_) {
-    PW_DCHECK_UINT_EQ(write_address_, 0);
+    // The write buffer might have bytes when the erase happens.
+    PW_DCHECK_UINT_LE(write_address_, write_buffer_.size_bytes());
     PW_DCHECK_UINT_EQ(flash_address_, 0);
-    PW_DCHECK(!valid_data_);
+    PW_DCHECK(valid_data_);
     return Status::OK;
   }
 
@@ -271,6 +415,7 @@
 
   if (status.ok()) {
     flash_erased_ = true;
+    valid_data_ = true;
   }
   return status;
 }
@@ -303,8 +448,8 @@
   }
 
   PW_LOG_DEBUG("Validate checksum of 0x%08x in flash for blob of %u bytes",
-               unsigned(metadata_.checksum),
-               unsigned(metadata_.data_size_bytes));
+               static_cast<unsigned>(metadata_.checksum),
+               static_cast<unsigned>(metadata_.data_size_bytes));
   auto result = CalculateChecksumFromFlash(metadata_.data_size_bytes);
   if (!result.ok()) {
     return result.status();
@@ -330,6 +475,7 @@
     const size_t read_size = std::min(size_t(end - address), buffer.size());
     StatusWithSize status =
         partition_.Read(address, std::span(buffer).first(read_size));
+    // TODO: switch to TRY once available.
     if (!status.ok()) {
       return status.status();
     }
diff --git a/pw_blob_store/blob_store_deferred_write_test.cc b/pw_blob_store/blob_store_deferred_write_test.cc
new file mode 100644
index 0000000..8100f2a
--- /dev/null
+++ b/pw_blob_store/blob_store_deferred_write_test.cc
@@ -0,0 +1,178 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <array>
+#include <cstddef>
+#include <cstring>
+#include <span>
+
+#include "gtest/gtest.h"
+#include "pw_blob_store/blob_store.h"
+#include "pw_kvs/crc16_checksum.h"
+#include "pw_kvs/fake_flash_memory.h"
+#include "pw_kvs/flash_memory.h"
+#include "pw_kvs/test_key_value_store.h"
+#include "pw_log/log.h"
+#include "pw_random/xor_shift.h"
+
+namespace pw::blob_store {
+namespace {
+
+class DeferredWriteTest : public ::testing::Test {
+ protected:
+  DeferredWriteTest() : flash_(kFlashAlignment), partition_(&flash_) {}
+
+  void InitFlashTo(std::span<const std::byte> contents) {
+    partition_.Erase();
+    std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
+  }
+
+  void InitBufferToRandom(uint64_t seed) {
+    partition_.Erase();
+    random::XorShiftStarRng64 rng(seed);
+    rng.Get(buffer_);
+  }
+
+  void InitBufferToFill(char fill) {
+    partition_.Erase();
+    std::memset(buffer_.data(), fill, buffer_.size());
+  }
+
+  // Fill the source buffer with random pattern based on given seed, written to
+  // BlobStore in specified chunk size.
+  void ChunkWriteTest(size_t chunk_size, size_t flush_interval) {
+    constexpr size_t kBufferSize = 256;
+    constexpr size_t kWriteSize = 64;
+    kvs::ChecksumCrc16 checksum;
+
+    size_t bytes_since_flush = 0;
+
+    char name[16] = {};
+    snprintf(name, sizeof(name), "Blob%u", static_cast<unsigned>(chunk_size));
+
+    BlobStoreBuffer<kBufferSize> blob(
+        name, &partition_, &checksum, kvs::TestKvs(), kWriteSize);
+    EXPECT_EQ(Status::OK, blob.Init());
+
+    BlobStore::DeferredWriter writer(blob);
+    EXPECT_EQ(Status::OK, writer.Open());
+
+    ByteSpan source = buffer_;
+    while (source.size_bytes() > 0) {
+      const size_t write_size = std::min(source.size_bytes(), chunk_size);
+
+      PW_LOG_DEBUG("Do write of %u bytes, %u bytes remain",
+                   static_cast<unsigned>(write_size),
+                   static_cast<unsigned>(source.size_bytes()));
+
+      EXPECT_EQ(Status::OK, writer.Write(source.first(write_size)));
+      // TODO: Add check that the write did not go to flash yet.
+
+      source = source.subspan(write_size);
+      bytes_since_flush += write_size;
+
+      if (bytes_since_flush >= flush_interval) {
+        bytes_since_flush = 0;
+        EXPECT_EQ(Status::OK, writer.Flush());
+      }
+    }
+
+    EXPECT_EQ(Status::OK, writer.Close());
+
+    // Use reader to check for valid data.
+    BlobStore::BlobReader reader(blob, 0);
+    EXPECT_EQ(Status::OK, reader.Open());
+    Result<ByteSpan> result = reader.GetMemoryMappedBlob();
+    ASSERT_TRUE(result.ok());
+    VerifyFlash(result.value());
+    EXPECT_EQ(Status::OK, reader.Close());
+  }
+
+  void VerifyFlash(ByteSpan verify_bytes) {
+    // Should be defined as same size.
+    EXPECT_EQ(buffer_.size(), flash_.buffer().size_bytes());
+
+    // Can't allow it to march off the end of buffer_.
+    ASSERT_LE(verify_bytes.size_bytes(), buffer_.size());
+
+    for (size_t i = 0; i < verify_bytes.size_bytes(); i++) {
+      EXPECT_EQ(buffer_[i], verify_bytes[i]);
+    }
+  }
+
+  static constexpr size_t kFlashAlignment = 16;
+  static constexpr size_t kSectorSize = 2048;
+  static constexpr size_t kSectorCount = 2;
+
+  kvs::FakeFlashMemoryBuffer<kSectorSize, kSectorCount> flash_;
+  kvs::FlashPartition partition_;
+  std::array<std::byte, kSectorCount * kSectorSize> buffer_;
+};
+
+TEST_F(DeferredWriteTest, ChunkWrite1) {
+  InitBufferToRandom(0x8675309);
+  ChunkWriteTest(1, 16);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite2) {
+  InitBufferToRandom(0x8675);
+  ChunkWriteTest(2, 16);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite3) {
+  InitBufferToFill(0);
+  ChunkWriteTest(3, 16);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite4) {
+  InitBufferToFill(1);
+  ChunkWriteTest(4, 64);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite5) {
+  InitBufferToFill(0xff);
+  ChunkWriteTest(5, 64);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite16) {
+  InitBufferToRandom(0x86);
+  ChunkWriteTest(16, 128);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite64) {
+  InitBufferToRandom(0x9);
+  ChunkWriteTest(64, 128);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite64FullBufferFill) {
+  InitBufferToRandom(0x9);
+  ChunkWriteTest(64, 256);
+}
+
+TEST_F(DeferredWriteTest, ChunkWrite256) {
+  InitBufferToRandom(0x12345678);
+  ChunkWriteTest(256, 256);
+}
+
+// TODO: test that has dirty flash, invalidated blob, open writer, invalidate
+// (not erase) and start writing (does the auto/implicit erase).
+
+// TODO: test that has dirty flash, invalidated blob, open writer, explicit
+// erase and start writing.
+
+// TODO: test start with dirty flash/invalid blob, open writer, write, close.
+// Verifies erase logic when write buffer has contents.
+
+}  // namespace
+}  // namespace pw::blob_store
diff --git a/pw_blob_store/blob_store_test.cc b/pw_blob_store/blob_store_test.cc
index 396a54b..97e8ce4 100644
--- a/pw_blob_store/blob_store_test.cc
+++ b/pw_blob_store/blob_store_test.cc
@@ -39,22 +39,25 @@
     std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
   }
 
-  void InitBufferToRandom(uint64_t seed) {
+  void InitSourceBufferToRandom(uint64_t seed) {
     partition_.Erase();
     random::XorShiftStarRng64 rng(seed);
-    rng.Get(buffer_);
+    rng.Get(source_buffer_);
+  }
+
+  void InitSourceBufferToFill(char fill) {
+    partition_.Erase();
+    std::memset(source_buffer_.data(), fill, source_buffer_.size());
   }
 
   // Fill the source buffer with random pattern based on given seed, written to
   // BlobStore in specified chunk size.
-  void ChunkWriteTest(size_t chunk_size, uint64_t seed) {
-    InitBufferToRandom(seed);
-
+  void ChunkWriteTest(size_t chunk_size) {
     constexpr size_t kBufferSize = 256;
     kvs::ChecksumCrc16 checksum;
 
     char name[16] = {};
-    snprintf(name, sizeof(name), "Blob%u", unsigned(chunk_size));
+    snprintf(name, sizeof(name), "Blob%u", static_cast<unsigned>(chunk_size));
 
     BlobStoreBuffer<kBufferSize> blob(
         name, &partition_, &checksum, kvs::TestKvs());
@@ -63,13 +66,13 @@
     BlobStore::BlobWriter writer(blob);
     EXPECT_EQ(Status::OK, writer.Open());
 
-    ByteSpan source = buffer_;
+    ByteSpan source = source_buffer_;
     while (source.size_bytes() > 0) {
       const size_t write_size = std::min(source.size_bytes(), chunk_size);
 
       PW_LOG_DEBUG("Do write of %u bytes, %u bytes remain",
-                   unsigned(write_size),
-                   unsigned(source.size_bytes()));
+                   static_cast<unsigned>(write_size),
+                   static_cast<unsigned>(source.size_bytes()));
 
       EXPECT_EQ(Status::OK, writer.Write(source.first(write_size)));
 
@@ -89,13 +92,13 @@
 
   void VerifyFlash(ByteSpan verify_bytes) {
     // Should be defined as same size.
-    EXPECT_EQ(buffer_.size(), flash_.buffer().size_bytes());
+    EXPECT_EQ(source_buffer_.size(), flash_.buffer().size_bytes());
 
-    // Can't allow it to march off the end of buffer_.
-    ASSERT_LE(verify_bytes.size_bytes(), buffer_.size());
+    // Can't allow it to march off the end of source_buffer_.
+    ASSERT_LE(verify_bytes.size_bytes(), source_buffer_.size());
 
     for (size_t i = 0; i < verify_bytes.size_bytes(); i++) {
-      EXPECT_EQ(buffer_[i], verify_bytes[i]);
+      EXPECT_EQ(source_buffer_[i], verify_bytes[i]);
     }
   }
 
@@ -105,7 +108,7 @@
 
   kvs::FakeFlashMemoryBuffer<kSectorSize, kSectorCount> flash_;
   kvs::FlashPartition partition_;
-  std::array<std::byte, kSectorCount * kSectorSize> buffer_;
+  std::array<std::byte, kSectorCount * kSectorSize> source_buffer_;
 };
 
 TEST_F(BlobStoreTest, Init_Ok) {
@@ -113,23 +116,86 @@
   EXPECT_EQ(Status::OK, blob.Init());
 }
 
-TEST_F(BlobStoreTest, ChunkWrite1) { ChunkWriteTest(1, 0x8675309); }
+TEST_F(BlobStoreTest, MultipleErase) {
+  BlobStoreBuffer<256> blob("Blob_OK", &partition_, nullptr, kvs::TestKvs());
+  EXPECT_EQ(Status::OK, blob.Init());
 
-TEST_F(BlobStoreTest, ChunkWrite2) { ChunkWriteTest(2, 0x8675); }
+  BlobStore::BlobWriter writer(blob);
+  EXPECT_EQ(Status::OK, writer.Open());
 
-TEST_F(BlobStoreTest, ChunkWrite16) { ChunkWriteTest(16, 0x86); }
+  EXPECT_EQ(Status::OK, writer.Erase());
+  EXPECT_EQ(Status::OK, writer.Erase());
+  EXPECT_EQ(Status::OK, writer.Erase());
+}
 
-TEST_F(BlobStoreTest, ChunkWrite64) { ChunkWriteTest(64, 0x9); }
+TEST_F(BlobStoreTest, ChunkWrite1) {
+  InitSourceBufferToRandom(0x8675309);
+  ChunkWriteTest(1);
+}
 
-TEST_F(BlobStoreTest, ChunkWrite256) { ChunkWriteTest(256, 0x12345678); }
+TEST_F(BlobStoreTest, ChunkWrite2) {
+  InitSourceBufferToRandom(0x8675);
+  ChunkWriteTest(2);
+}
 
-TEST_F(BlobStoreTest, ChunkWrite512) { ChunkWriteTest(512, 0x42); }
+TEST_F(BlobStoreTest, ChunkWrite3) {
+  InitSourceBufferToFill(0);
+  ChunkWriteTest(3);
+}
 
-TEST_F(BlobStoreTest, ChunkWrite4096) { ChunkWriteTest(4096, 0x89); }
+TEST_F(BlobStoreTest, ChunkWrite4) {
+  InitSourceBufferToFill(1);
+  ChunkWriteTest(4);
+}
+
+TEST_F(BlobStoreTest, ChunkWrite5) {
+  InitSourceBufferToFill(0xff);
+  ChunkWriteTest(5);
+}
+
+TEST_F(BlobStoreTest, ChunkWrite16) {
+  InitSourceBufferToRandom(0x86);
+  ChunkWriteTest(16);
+}
+
+TEST_F(BlobStoreTest, ChunkWrite64) {
+  InitSourceBufferToRandom(0x9);
+  ChunkWriteTest(64);
+}
+
+TEST_F(BlobStoreTest, ChunkWrite256) {
+  InitSourceBufferToRandom(0x12345678);
+  ChunkWriteTest(256);
+}
+
+TEST_F(BlobStoreTest, ChunkWrite512) {
+  InitSourceBufferToRandom(0x42);
+  ChunkWriteTest(512);
+}
+
+TEST_F(BlobStoreTest, ChunkWrite4096) {
+  InitSourceBufferToRandom(0x89);
+  ChunkWriteTest(4096);
+}
 
 TEST_F(BlobStoreTest, ChunkWriteSingleFull) {
-  ChunkWriteTest((kSectorCount * kSectorSize), 0x98765);
+  InitSourceBufferToRandom(0x98765);
+  ChunkWriteTest((kSectorCount * kSectorSize));
 }
 
+// TODO: test that does close with bytes still in the buffer to test zero fill
+// to alignment and write on close.
+
+// TODO: test to do write/close, write/close multiple times.
+
+// TODO: test start with old blob (with KVS entry), open, invalidate, no writes,
+// close. Verify the KVS entry is gone and blob is fully invalid.
+
+// TODO: test that checks doing read after bytes are in write buffer but before
+// any bytes are flushed to flash.
+
+// TODO: test mem mapped read when bytes in write buffer but nothing written to
+// flash.
+
 }  // namespace
 }  // namespace pw::blob_store
diff --git a/pw_blob_store/public/pw_blob_store/blob_store.h b/pw_blob_store/public/pw_blob_store/blob_store.h
index 87433f6..14a1461 100644
--- a/pw_blob_store/public/pw_blob_store/blob_store.h
+++ b/pw_blob_store/public/pw_blob_store/blob_store.h
@@ -49,12 +49,15 @@
  public:
   // Implement the stream::Writer and erase interface for a BlobStore. If not
   // already erased, the Write will do any needed erase.
-  class BlobWriter final : public stream::Writer {
+  //
+  // Only one writter (of either type) is allowed to be open at a time.
+  // Additionally, writters are unable to open if a reader is already open.
+  class BlobWriter : public stream::Writer {
    public:
     constexpr BlobWriter(BlobStore& store) : store_(store), open_(false) {}
     BlobWriter(const BlobWriter&) = delete;
     BlobWriter& operator=(const BlobWriter&) = delete;
-    ~BlobWriter() {
+    virtual ~BlobWriter() {
       if (open_) {
         Close();
       }
@@ -95,7 +98,7 @@
     // [error status] - flash erase failed.
     Status Erase() {
       PW_DCHECK(open_);
-      return Status::UNIMPLEMENTED;
+      return store_.Erase();
     }
 
     // Discard blob (in-progress or valid stored). Any written bytes are
@@ -109,7 +112,8 @@
     }
 
     // Probable (not guaranteed) minimum number of bytes at this time that can
-    // be written. Returns zero if, in the current state, Write would return
+    // be written. This is not necessarily the full number of bytes remaining in
+    // the blob. Returns zero if, in the current state, Write would return
     // status other than OK. See stream.h for additional details.
     size_t ConservativeWriteLimit() const override {
       PW_DCHECK(open_);
@@ -121,7 +125,7 @@
       return store_.write_address_;
     }
 
-   private:
+   protected:
     Status DoWrite(ConstByteSpan data) override {
       PW_DCHECK(open_);
       return store_.Write(data);
@@ -131,6 +135,43 @@
     bool open_;
   };
 
+  // Implement the stream::Writer and erase interface with deferred action for a
+  // BlobStore. If not already erased, the Flush will do any needed erase.
+  //
+  // Only one writter (of either type) is allowed to be open at a time.
+  // Additionally, writters are unable to open if a reader is already open.
+  class DeferredWriter final : public BlobWriter {
+   public:
+    constexpr DeferredWriter(BlobStore& store) : BlobWriter(store) {}
+    DeferredWriter(const DeferredWriter&) = delete;
+    DeferredWriter& operator=(const DeferredWriter&) = delete;
+    virtual ~DeferredWriter() {}
+
+    // Flush data in the write buffer. Only a multiple of flash_write_size_bytes
+    // are written in the flush. Any remainder is held until later for either
+    // a flush with flash_write_size_bytes buffered or the writer is closed.
+    Status Flush() {
+      PW_DCHECK(open_);
+      return store_.Flush();
+    }
+
+    // Probable (not guaranteed) minimum number of bytes at this time that can
+    // be written. This is not necessarily the full number of bytes remaining in
+    // the blob. Returns zero if, in the current state, Write would return
+    // status other than OK. See stream.h for additional details.
+    size_t ConservativeWriteLimit() const override {
+      PW_DCHECK(open_);
+      // Deferred writes need to fit in the write buffer.
+      return store_.WriteBufferBytesFree();
+    }
+
+   private:
+    Status DoWrite(ConstByteSpan data) override {
+      PW_DCHECK(open_);
+      return store_.AddToWriteBuffer(data);
+    }
+  };
+
   // Implement stream::Reader interface for BlobStore.
   class BlobReader final : public stream::Reader {
    public:
@@ -197,16 +238,31 @@
     size_t offset_;
   };
 
+  // BlobStore
+  // name - Name of blob store, used for metadata KVS key
+  // partition - Flash partiton to use for this blob. Blob uses the entire
+  //     partition.
+  // checksum_algo - Optional checksum for blob integrity checking. Use nullptr
+  //     for no check.
+  // kvs - KVS used for storing blob metadata.
+  // write_buffer - Used for buffering writes. Needs to be at least
+  //     flash_write_size_bytes.
+  // flash_write_size_bytes - Size in bytes to use for flash write operations.
+  //     This should be chosen to balance optimal write size and required buffer
+  //     size. Must be greater than or equal to flash write alignment, less than
+  //     or equal to flash sector size.
   BlobStore(std::string_view name,
             kvs::FlashPartition* partition,
             kvs::ChecksumAlgorithm* checksum_algo,
             kvs::KeyValueStore& kvs,
-            ByteSpan write_buffer)
+            ByteSpan write_buffer,
+            size_t flash_write_size_bytes)
       : name_(name),
         partition_(*partition),
         checksum_algo_(checksum_algo),
         kvs_(kvs),
         write_buffer_(write_buffer),
+        flash_write_size_bytes_(flash_write_size_bytes),
         initialized_(false),
         valid_data_(false),
         flash_erased_(false),
@@ -231,9 +287,6 @@
  private:
   typedef uint32_t ChecksumValue;
 
-  // Is the blob erased and ready to write.
-  bool erased() const { return flash_erased_; }
-
   // Open to do a blob write. Returns:
   //
   // OK - success.
@@ -251,7 +304,8 @@
   // store blob metadata. Returns:
   //
   // OK - success.
-  // FAILED_PRECONDITION - blob is not open.
+  // DATA_LOSS - Error during write (this close or previous write/flush). Blob
+  //     is closed and marked as invalid.
   Status CloseWrite();
   Status CloseRead();
 
@@ -260,33 +314,70 @@
   // not guaranteed to be fully written out to storage on Write return. Returns:
   //
   // OK - successful write/enqueue of data.
-  // FAILED_PRECONDITION - blob is not in an open (in-progress) write state.
   // RESOURCE_EXHAUSTED - unable to write all of requested data at this time. No
   //     data written.
   // OUT_OF_RANGE - Writer has been exhausted, similar to EOF. No data written,
   //     no more will be written.
+  // DATA_LOSS - Error during write (this write or previous write/flush). No
+  //     more will be written by following Write calls for current blob (until
+  //     erase/new blob started).
   Status Write(ConstByteSpan data);
 
+  // Similar to Write, but instead immediately writing out to flash, it only
+  // buffers the data. A flush or Close is reqired to get bytes writen out to
+  // flash
+  //
+  // OK - successful write/enqueue of data.
+  // RESOURCE_EXHAUSTED - unable to write all of requested data at this time. No
+  //     data written.
+  // OUT_OF_RANGE - Writer has been exhausted, similar to EOF. No data written,
+  //     no more will be written.
+  // DATA_LOSS - Error during a previous write/flush. No more will be written by
+  //     following Write calls for current blob (until erase/new blob started).
+  Status AddToWriteBuffer(ConstByteSpan data);
+
+  // Flush data in the write buffer. Only a multiple of flash_write_size_bytes
+  // are written in the flush. Any remainder is held until later for either a
+  // flush with flash_write_size_bytes buffered or the writer is closed.
+  //
+  // OK - successful write/enqueue of data.
+  // DATA_LOSS - Error during write (this flush or previous write/flush). No
+  //     more will be written by following Write calls for current blob (until
+  //     erase/new blob started).
+  Status Flush();
+
+  // Flush a chunk of data in the write buffer smaller than
+  // flash_write_size_bytes. This is only for the final flush as part of the
+  // CloseWrite. The partial chunk is padded to flash_write_size_bytes and a
+  // flash_write_size_bytes chunk is written to flash.
+  //
+  // OK - successful write/enqueue of data.
+  // DATA_LOSS - Error during write (this flush or previous write/flush). No
+  //     more will be written by following Write calls for current blob (until
+  //     erase/new blob started).
+  Status FlushFinalPartialChunk();
+
   // Commit data to flash and update flash_address_ with data bytes written. The
   // only time data_bytes should be manually specified is for a CloseWrite with
   // an unaligned-size chunk remaining in the buffer that has been zero padded
   // to alignment.
-  Status CommitToFlash(ConstByteSpan source, size_t data_bytes = 0) {
-    if (data_bytes == 0) {
-      data_bytes = source.size_bytes();
-    }
-    flash_erased_ = false;
-    valid_data_ = true;
-    StatusWithSize result = partition_.Write(flash_address_, source);
-    flash_address_ += data_bytes;
-    if (checksum_algo_ != nullptr) {
-      checksum_algo_->Update(source.first(data_bytes));
-    }
+  Status CommitToFlash(ConstByteSpan source, size_t data_bytes = 0);
 
-    return result.status();
-  }
+  // Blob is valid/OK to write to. Blob is considered valid to write if no data
+  // has been written due to the auto/implicit erase on write start.
+  //
+  // true - Blob is valid and OK to write to.
+  // false - Blob has previously had an error and not valid for writing new
+  //     data.
+  bool ValidToWrite() { return (valid_data_ == true) || (write_address_ == 0); }
 
-  bool WriteBufferEmpty() { return flash_address_ == write_address_; }
+  bool WriteBufferEmpty() const { return flash_address_ == write_address_; }
+
+  size_t WriteBufferBytesUsed() const;
+
+  size_t WriteBufferBytesFree() const;
+
+  Status EraseIfNeeded();
 
   // Read valid data. Attempts to read the lesser of output.size_bytes() or
   // available bytes worth of data. Returns:
@@ -352,6 +443,11 @@
   kvs::KeyValueStore& kvs_;
   ByteSpan write_buffer_;
 
+  // Size in bytes of flash write operations. This should be chosen to balance
+  // optimal write size and required buffer size. Must be GE flash write
+  // alignment, LE flash sector size.
+  const size_t flash_write_size_bytes_;
+
   //
   // Internal state for Blob store
   //
@@ -360,7 +456,8 @@
   // Initialization has been done.
   bool initialized_;
 
-  // Bytes stored are valid and good.
+  // Bytes stored are valid and good. Blob is OK to read and write to. Set as
+  // soon as is valid, even when bytes written is still 0.
   bool valid_data_;
 
   // Blob partition is currently erased and ready to write a new blob.
@@ -391,8 +488,14 @@
   explicit BlobStoreBuffer(std::string_view name,
                            kvs::FlashPartition* partition,
                            kvs::ChecksumAlgorithm* checksum_algo,
-                           kvs::KeyValueStore& kvs)
-      : BlobStore(name, partition, checksum_algo, kvs, buffer_) {}
+                           kvs::KeyValueStore& kvs,
+                           size_t flash_write_size_bytes = kBufferSizeBytes)
+      : BlobStore(name,
+                  partition,
+                  checksum_algo,
+                  kvs,
+                  buffer_,
+                  flash_write_size_bytes) {}
 
  private:
   std::array<std::byte, kBufferSizeBytes> buffer_;