• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2020 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14 #pragma once
15 
16 #include <array>
17 #include <cstddef>
18 #include <cstdint>
19 #include <span>
20 #include <type_traits>
21 
22 #include "pw_containers/vector.h"
23 #include "pw_kvs/checksum.h"
24 #include "pw_kvs/flash_memory.h"
25 #include "pw_kvs/format.h"
26 #include "pw_kvs/internal/entry.h"
27 #include "pw_kvs/internal/entry_cache.h"
28 #include "pw_kvs/internal/key_descriptor.h"
29 #include "pw_kvs/internal/sectors.h"
30 #include "pw_kvs/internal/span_traits.h"
31 #include "pw_kvs/key.h"
32 #include "pw_status/status.h"
33 #include "pw_status/status_with_size.h"
34 
35 namespace pw {
36 namespace kvs {
37 
38 enum class GargbageCollectOnWrite {
39   // Disable all automatic garbage collection on write.
40   kDisabled,
41 
42   // Allow up to a single sector, if needed, to be garbage collected on write.
43   kOneSector,
44 
45   // Allow as many sectors as needed be garbage collected on write.
46   kAsManySectorsNeeded,
47 };
48 
49 enum class ErrorRecovery {
50   // Immediately do full recovery of any errors that are detected.
51   kImmediate,
52 
53   // Recover from errors, but delay time consuming recovery steps until later
54   // as part of other time consuming operations. Such as waiting to garbage
55   // collect sectors with corrupt entries until the next garbage collection.
56   kLazy,
57 
58   // Only recover from errors when manually triggered as part of maintenance
59   // operations. This is not recommended for normal use, only for test or
60   // read-only use.
61   kManual,
62 };
63 
64 struct Options {
65   // Perform garbage collection if necessary when writing. If not kDisabled,
66   // garbage collection is attempted if space for an entry cannot be found. This
67   // is a relatively lengthy operation. If kDisabled, Put calls that would
68   // require garbage collection fail with RESOURCE_EXHAUSTED.
69   GargbageCollectOnWrite gc_on_write = GargbageCollectOnWrite::kOneSector;
70 
71   // When the KVS handles errors that are discovered, such as corrupt entries,
72   // not enough redundant copys of an entry, etc.
73   ErrorRecovery recovery = ErrorRecovery::kLazy;
74 
75   // Verify an entry's checksum after reading it from flash.
76   bool verify_on_read = true;
77 
78   // Verify an in-flash entry's checksum after writing it.
79   bool verify_on_write = true;
80 };
81 
82 class KeyValueStore {
83  public:
84   // KeyValueStores are declared as instances of
85   // KeyValueStoreBuffer<MAX_ENTRIES, MAX_SECTORS>, which allocates buffers for
86   // tracking entries and flash sectors.
87 
88   // Initializes the key-value store. Must be called before calling other
89   // functions.
90   //
91   // Return values:
92   //
93   //          OK: KVS successfully initialized.
94   //   DATA_LOSS: KVS initialized and is usable, but contains corrupt data.
95   //     UNKNOWN: Unknown error. KVS is not initialized.
96   //
97   Status Init();
98 
initialized()99   bool initialized() const {
100     return initialized_ == InitializationState::kReady;
101   }
102 
103   // Reads the value of an entry in the KVS. The value is read into the provided
104   // buffer and the number of bytes read is returned. If desired, the read can
105   // be started at an offset.
106   //
107   // If the output buffer is too small for the value, Get returns
108   // RESOURCE_EXHAUSTED with the number of bytes read. The remainder of the
109   // value can be read by calling get with an offset.
110   //
111   //                    OK: the entry was successfully read
112   //             NOT_FOUND: the key is not present in the KVS
113   //             DATA_LOSS: found the entry, but the data was corrupted
114   //    RESOURCE_EXHAUSTED: the buffer could not fit the entire value, but as
115   //                        many bytes as possible were written to it
116   //   FAILED_PRECONDITION: the KVS is not initialized
117   //      INVALID_ARGUMENT: key is empty or too long or value is too large
118   //
119   StatusWithSize Get(Key key,
120                      std::span<std::byte> value,
121                      size_t offset_bytes = 0) const;
122 
123   // This overload of Get accepts a pointer to a trivially copyable object.
124   // If the value is an array, call Get with
125   // std::as_writable_bytes(std::span(array)), or pass a pointer to the array
126   // instead of the array itself.
127   template <typename Pointer,
128             typename = std::enable_if_t<std::is_pointer<Pointer>::value>>
Get(const Key & key,const Pointer & pointer)129   Status Get(const Key& key, const Pointer& pointer) const {
130     using T = std::remove_reference_t<std::remove_pointer_t<Pointer>>;
131     CheckThatObjectCanBePutOrGet<T>();
132     return FixedSizeGet(key, pointer, sizeof(T));
133   }
134 
135   // Adds a key-value entry to the KVS. If the key was already present, its
136   // value is overwritten.
137   //
138   // The value may be a std::span of bytes or a trivially copyable object.
139   //
140   // In the current implementation, all keys in the KVS must have a unique hash.
141   // If Put is called with a key whose hash matches an existing key, nothing
142   // is added and ALREADY_EXISTS is returned.
143   //
144   //                    OK: the entry was successfully added or updated
145   //             DATA_LOSS: checksum validation failed after writing the data
146   //    RESOURCE_EXHAUSTED: there is not enough space to add the entry
147   //        ALREADY_EXISTS: the entry could not be added because a different key
148   //                        with the same hash is already in the KVS
149   //   FAILED_PRECONDITION: the KVS is not initialized
150   //      INVALID_ARGUMENT: key is empty or too long or value is too large
151   //
152   template <typename T,
153             typename std::enable_if_t<ConvertsToSpan<T>::value>* = nullptr>
Put(const Key & key,const T & value)154   Status Put(const Key& key, const T& value) {
155     return PutBytes(key, std::as_bytes(internal::make_span(value)));
156   }
157 
158   template <typename T,
159             typename std::enable_if_t<!ConvertsToSpan<T>::value>* = nullptr>
Put(const Key & key,const T & value)160   Status Put(const Key& key, const T& value) {
161     CheckThatObjectCanBePutOrGet<T>();
162     return PutBytes(key, std::as_bytes(std::span<const T>(&value, 1)));
163   }
164 
165   // Removes a key-value entry from the KVS.
166   //
167   //                    OK: the entry was successfully added or updated
168   //             NOT_FOUND: the key is not present in the KVS
169   //             DATA_LOSS: checksum validation failed after recording the erase
170   //    RESOURCE_EXHAUSTED: insufficient space to mark the entry as deleted
171   //   FAILED_PRECONDITION: the KVS is not initialized
172   //      INVALID_ARGUMENT: key is empty or too long
173   //
174   Status Delete(Key key);
175 
176   // Returns the size of the value corresponding to the key.
177   //
178   //                    OK: the size was returned successfully
179   //             NOT_FOUND: the key is not present in the KVS
180   //             DATA_LOSS: checksum validation failed after reading the entry
181   //   FAILED_PRECONDITION: the KVS is not initialized
182   //      INVALID_ARGUMENT: key is empty or too long
183   //
184   StatusWithSize ValueSize(Key key) const;
185 
186   // Perform all maintenance possible, including all neeeded repairing of
187   // corruption and garbage collection of reclaimable space in the KVS. When
188   // configured for manual recovery, this (along with FullMaintenance) is the
189   // only way KVS repair is triggered.
190   //
191   // - Heavy garbage collection of all reclaimable space, regardless of valid
192   //   data in the sector.
HeavyMaintenance()193   Status HeavyMaintenance() {
194     return FullMaintenanceHelper(MaintenanceType::kHeavy);
195   }
196 
197   // Perform all maintenance possible, including all neeeded repairing of
198   // corruption and garbage collection of reclaimable space in the KVS. When
199   // configured for manual recovery, this (along with HeavyMaintenance) is the
200   // only way KVS repair is triggered.
201   //
202   // - Regular will not garbage collect sectors with valid data unless the KVS
203   //   is mostly full.
FullMaintenance()204   Status FullMaintenance() {
205     return FullMaintenanceHelper(MaintenanceType::kRegular);
206   }
207 
208   // Perform a portion of KVS maintenance. If configured for at least lazy
209   // recovery, will do any needed repairing of corruption. Does garbage
210   // collection of part of the KVS, typically a single sector or similar unit
211   // that makes sense for the KVS implementation.
212   Status PartialMaintenance();
213 
214   void LogDebugInfo() const;
215 
216   // Classes and functions to support STL-style iteration.
217   class iterator;
218 
219   class Item {
220    public:
221     // The key as a null-terminated string.
key()222     const char* key() const { return key_buffer_.data(); }
223 
224     // Gets the value referred to by this iterator. Equivalent to
225     // KeyValueStore::Get.
226     StatusWithSize Get(std::span<std::byte> value_buffer,
227                        size_t offset_bytes = 0) const {
228       return kvs_.Get(key(), *iterator_, value_buffer, offset_bytes);
229     }
230 
231     template <typename Pointer,
232               typename = std::enable_if_t<std::is_pointer<Pointer>::value>>
Get(const Pointer & pointer)233     Status Get(const Pointer& pointer) const {
234       using T = std::remove_reference_t<std::remove_pointer_t<Pointer>>;
235       CheckThatObjectCanBePutOrGet<T>();
236       return kvs_.FixedSizeGet(key(), *iterator_, pointer, sizeof(T));
237     }
238 
239     // Reads the size of the value referred to by this iterator. Equivalent to
240     // KeyValueStore::ValueSize.
ValueSize()241     StatusWithSize ValueSize() const { return kvs_.ValueSize(*iterator_); }
242 
243    private:
244     friend class iterator;
245 
Item(const KeyValueStore & kvs,const internal::EntryCache::const_iterator & item_iterator)246     constexpr Item(const KeyValueStore& kvs,
247                    const internal::EntryCache::const_iterator& item_iterator)
248         : kvs_(kvs), iterator_(item_iterator), key_buffer_{} {}
249 
250     void ReadKey();
251 
252     const KeyValueStore& kvs_;
253     internal::EntryCache::const_iterator iterator_;
254 
255     // Buffer large enough for a null-terminated version of any valid key.
256     std::array<char, internal::Entry::kMaxKeyLength + 1> key_buffer_;
257   };
258 
259   class iterator {
260    public:
261     iterator& operator++();
262 
263     iterator& operator++(int) { return operator++(); }
264 
265     // Reads the entry's key from flash.
266     const Item& operator*() {
267       item_.ReadKey();
268       return item_;
269     }
270 
271     const Item* operator->() {
272       return &operator*();  // Read the key into the Item object.
273     }
274 
275     constexpr bool operator==(const iterator& rhs) const {
276       return item_.iterator_ == rhs.item_.iterator_;
277     }
278 
279     constexpr bool operator!=(const iterator& rhs) const {
280       return item_.iterator_ != rhs.item_.iterator_;
281     }
282 
283    private:
284     friend class KeyValueStore;
285 
iterator(const KeyValueStore & kvs,const internal::EntryCache::const_iterator & item_iterator)286     constexpr iterator(
287         const KeyValueStore& kvs,
288         const internal::EntryCache::const_iterator& item_iterator)
289         : item_(kvs, item_iterator) {}
290 
291     Item item_;
292   };
293 
294   using const_iterator = iterator;  // Standard alias for iterable types.
295 
296   iterator begin() const;
end()297   iterator end() const { return iterator(*this, entry_cache_.end()); }
298 
299   // Returns the number of valid entries in the KeyValueStore.
size()300   size_t size() const { return entry_cache_.present_entries(); }
301 
max_size()302   size_t max_size() const { return entry_cache_.max_entries(); }
303 
empty()304   size_t empty() const { return size() == 0u; }
305 
306   // Returns the number of transactions that have occurred since the KVS was
307   // first used. This value is retained across initializations, but is reset if
308   // the underlying flash is erased.
transaction_count()309   uint32_t transaction_count() const { return last_transaction_id_; }
310 
311   struct StorageStats {
312     size_t writable_bytes;
313     size_t in_use_bytes;
314     size_t reclaimable_bytes;
315     size_t sector_erase_count;
316     size_t corrupt_sectors_recovered;
317     size_t missing_redundant_entries_recovered;
318   };
319 
320   StorageStats GetStorageStats() const;
321 
322   // Level of redundancy to use for writing entries.
redundancy()323   size_t redundancy() const { return entry_cache_.redundancy(); }
324 
error_detected()325   bool error_detected() const { return error_detected_; }
326 
327   // Maximum number of bytes allowed for a key-value combination.
max_key_value_size_bytes()328   size_t max_key_value_size_bytes() const {
329     return max_key_value_size_bytes(partition_.sector_size_bytes());
330   }
331 
332   // Maximum number of bytes allowed for a given sector size for a key-value
333   // combination.
max_key_value_size_bytes(size_t partition_sector_size_bytes)334   static constexpr size_t max_key_value_size_bytes(
335       size_t partition_sector_size_bytes) {
336     return partition_sector_size_bytes - Entry::entry_overhead();
337   }
338 
339   // Check KVS for any error conditions. Primarily intended for test and
340   // internal use.
341   bool CheckForErrors();
342 
343  protected:
344   using Address = FlashPartition::Address;
345   using Entry = internal::Entry;
346   using KeyDescriptor = internal::KeyDescriptor;
347   using SectorDescriptor = internal::SectorDescriptor;
348 
349   // In the future, will be able to provide additional EntryFormats for
350   // backwards compatibility.
351   KeyValueStore(FlashPartition* partition,
352                 std::span<const EntryFormat> formats,
353                 const Options& options,
354                 size_t redundancy,
355                 Vector<SectorDescriptor>& sector_descriptor_list,
356                 const SectorDescriptor** temp_sectors_to_skip,
357                 Vector<KeyDescriptor>& key_descriptor_list,
358                 Address* addresses);
359 
360  private:
361   using EntryMetadata = internal::EntryMetadata;
362   using EntryState = internal::EntryState;
363 
364   template <typename T>
CheckThatObjectCanBePutOrGet()365   static constexpr void CheckThatObjectCanBePutOrGet() {
366     static_assert(
367         std::is_trivially_copyable<T>::value && !std::is_pointer<T>::value,
368         "Only trivially copyable, non-pointer objects may be Put and Get by "
369         "value. Any value may be stored by converting it to a byte std::span "
370         "with std::as_bytes(std::span(&value, 1)) or "
371         "std::as_writable_bytes(std::span(&value, 1)).");
372   }
373 
374   Status InitializeMetadata();
375   Status LoadEntry(Address entry_address, Address* next_entry_address);
376   Status ScanForEntry(const SectorDescriptor& sector,
377                       Address start_address,
378                       Address* next_entry_address);
379 
380   Status PutBytes(Key key, std::span<const std::byte> value);
381 
382   StatusWithSize ValueSize(const EntryMetadata& metadata) const;
383 
384   Status ReadEntry(const EntryMetadata& metadata, Entry& entry) const;
385 
386   // Finds the metadata for an entry matching a particular key. Searches for a
387   // KeyDescriptor that matches this key and sets *metadata to point to it if
388   // one is found.
389   //
390   //             OK: there is a matching descriptor and *metadata is set
391   //      NOT_FOUND: there is no descriptor that matches this key, but this key
392   //                 has a unique hash (and could potentially be added to the
393   //                 KVS)
394   // ALREADY_EXISTS: there is no descriptor that matches this key, but the
395   //                 key's hash collides with the hash for an existing
396   //                 descriptor
397   //
398   Status FindEntry(Key key, EntryMetadata* metadata) const;
399 
400   // Searches for a KeyDescriptor that matches this key and sets *metadata to
401   // point to it if one is found.
402   //
403   //          OK: there is a matching descriptor and *metadata is set
404   //   NOT_FOUND: there is no descriptor that matches this key
405   //
406   Status FindExisting(Key key, EntryMetadata* metadata) const;
407 
408   StatusWithSize Get(Key key,
409                      const EntryMetadata& metadata,
410                      std::span<std::byte> value_buffer,
411                      size_t offset_bytes) const;
412 
413   Status FixedSizeGet(Key key, void* value, size_t size_bytes) const;
414 
415   Status FixedSizeGet(Key key,
416                       const EntryMetadata& descriptor,
417                       void* value,
418                       size_t size_bytes) const;
419 
420   Status CheckWriteOperation(Key key) const;
421   Status CheckReadOperation(Key key) const;
422 
423   Status WriteEntryForExistingKey(EntryMetadata& metadata,
424                                   EntryState new_state,
425                                   Key key,
426                                   std::span<const std::byte> value);
427 
428   Status WriteEntryForNewKey(Key key, std::span<const std::byte> value);
429 
430   Status WriteEntry(Key key,
431                     std::span<const std::byte> value,
432                     EntryState new_state,
433                     EntryMetadata* prior_metadata = nullptr,
434                     const internal::Entry* prior_entry = nullptr);
435 
436   EntryMetadata CreateOrUpdateKeyDescriptor(const Entry& new_entry,
437                                             Key key,
438                                             EntryMetadata* prior_metadata,
439                                             size_t prior_size);
440 
441   EntryMetadata UpdateKeyDescriptor(const Entry& entry,
442                                     Address new_address,
443                                     EntryMetadata* prior_metadata,
444                                     size_t prior_size);
445 
446   Status GetAddressesForWrite(Address* write_addresses, size_t write_size);
447 
448   Status GetSectorForWrite(SectorDescriptor** sector,
449                            size_t entry_size,
450                            std::span<const Address> addresses_to_skip);
451 
452   Status MarkSectorCorruptIfNotOk(Status status, SectorDescriptor* sector);
453 
454   Status AppendEntry(const Entry& entry,
455                      Key key,
456                      std::span<const std::byte> value);
457 
458   StatusWithSize CopyEntryToSector(Entry& entry,
459                                    SectorDescriptor* new_sector,
460                                    Address new_address);
461 
462   Status RelocateEntry(const EntryMetadata& metadata,
463                        KeyValueStore::Address& address,
464                        std::span<const Address> addresses_to_skip);
465 
466   // Perform all maintenance possible, including all neeeded repairing of
467   // corruption and garbage collection of reclaimable space in the KVS. When
468   // configured for manual recovery, this is the only way KVS repair is
469   // triggered.
470   //
471   // - Regular will not garbage collect sectors with valid data unless the KVS
472   //   is mostly full.
473   // - Heavy will garbage collect all reclaimable space regardless of valid data
474   //   in the sector.
475   enum class MaintenanceType {
476     kRegular,
477     kHeavy,
478   };
479   Status FullMaintenanceHelper(MaintenanceType maintenance_type);
480 
481   // Find and garbage collect a singe sector that does not include an address to
482   // skip.
483   Status GarbageCollect(std::span<const Address> addresses_to_skip);
484 
485   Status RelocateKeyAddressesInSector(
486       SectorDescriptor& sector_to_gc,
487       const EntryMetadata& descriptor,
488       std::span<const Address> addresses_to_skip);
489 
490   Status GarbageCollectSector(SectorDescriptor& sector_to_gc,
491                               std::span<const Address> addresses_to_skip);
492 
493   // Ensure that all entries are on the primary (first) format. Entries that are
494   // not on the primary format are rewritten.
495   //
496   // Return: status + number of entries updated.
497   StatusWithSize UpdateEntriesToPrimaryFormat();
498 
499   Status AddRedundantEntries(EntryMetadata& metadata);
500 
501   Status RepairCorruptSectors();
502 
503   Status EnsureFreeSectorExists();
504 
505   Status EnsureEntryRedundancy();
506 
507   Status FixErrors();
508 
509   Status Repair();
510 
511   internal::Entry CreateEntry(Address address,
512                               Key key,
513                               std::span<const std::byte> value,
514                               EntryState state);
515 
516   void LogSectors() const;
517   void LogKeyDescriptor() const;
518 
519   FlashPartition& partition_;
520   const internal::EntryFormats formats_;
521 
522   // List of sectors used by this KVS.
523   internal::Sectors sectors_;
524 
525   // Unordered list of KeyDescriptors. Finding a key requires scanning and
526   // verifying a match by reading the actual entry.
527   internal::EntryCache entry_cache_;
528 
529   Options options_;
530 
531   // Threshold value for when to garbage collect all stale data. Above the
532   // threshold, GC all reclaimable bytes regardless of if valid data is in
533   // sector. Below the threshold, only GC sectors with reclaimable bytes and no
534   // valid bytes. The threshold is based on the portion of KVS capacity used by
535   // valid bytes.
536   static constexpr size_t kGcUsageThresholdPercentage = 70;
537 
538   enum class InitializationState {
539     // KVS Init() has not been called and KVS is not usable.
540     kNotInitialized,
541 
542     // KVS Init() run but not all the needed invariants are met for an operating
543     // KVS. KVS is not writable, but read operaions are possible.
544     kNeedsMaintenance,
545 
546     // KVS is fully ready for use.
547     kReady,
548   };
549   InitializationState initialized_;
550 
551   // error_detected_ needs to be set from const KVS methods (such as Get), so
552   // make it mutable.
553   mutable bool error_detected_;
554 
555   struct InternalStats {
556     size_t sector_erase_count;
557     size_t corrupt_sectors_recovered;
558     size_t missing_redundant_entries_recovered;
559   };
560   InternalStats internal_stats_;
561 
562   uint32_t last_transaction_id_;
563 };
564 
565 template <size_t kMaxEntries,
566           size_t kMaxUsableSectors,
567           size_t kRedundancy = 1,
568           size_t kEntryFormats = 1>
569 class KeyValueStoreBuffer : public KeyValueStore {
570  public:
571   // Constructs a KeyValueStore on the partition, with support for one
572   // EntryFormat (kEntryFormats must be 1).
573   KeyValueStoreBuffer(FlashPartition* partition,
574                       const EntryFormat& format,
575                       const Options& options = {})
KeyValueStoreBuffer(partition,std::span<const EntryFormat,kEntryFormats> (reinterpret_cast<const EntryFormat (&)[1]> (format)),options)576       : KeyValueStoreBuffer(
577             partition,
578             std::span<const EntryFormat, kEntryFormats>(
579                 reinterpret_cast<const EntryFormat (&)[1]>(format)),
580             options) {
581     static_assert(kEntryFormats == 1,
582                   "kEntryFormats EntryFormats must be specified");
583   }
584 
585   // Constructs a KeyValueStore on the partition. Supports multiple entry
586   // formats. The first EntryFormat is used for new entries.
587   KeyValueStoreBuffer(FlashPartition* partition,
588                       std::span<const EntryFormat, kEntryFormats> formats,
589                       const Options& options = {})
KeyValueStore(partition,formats_,options,kRedundancy,sectors_,temp_sectors_to_skip_,key_descriptors_,addresses_)590       : KeyValueStore(partition,
591                       formats_,
592                       options,
593                       kRedundancy,
594                       sectors_,
595                       temp_sectors_to_skip_,
596                       key_descriptors_,
597                       addresses_) {
598     std::copy(formats.begin(), formats.end(), formats_.begin());
599   }
600 
601  private:
602   static_assert(kMaxEntries > 0u);
603   static_assert(kMaxUsableSectors > 0u);
604   static_assert(kRedundancy > 0u);
605   static_assert(kEntryFormats > 0u);
606 
607   Vector<SectorDescriptor, kMaxUsableSectors> sectors_;
608 
609   // Allocate space for an list of SectorDescriptors to avoid while searching
610   // for space to write entries. This is used to avoid changing sectors that are
611   // reserved for a new entry or marked as having other redundant copies of an
612   // entry. Up to kRedundancy sectors are reserved for a new entry, and up to
613   // kRedundancy - 1 sectors can have redundant copies of an entry, giving a
614   // maximum of 2 * kRedundancy - 1 sectors to avoid.
615   const SectorDescriptor* temp_sectors_to_skip_[2 * kRedundancy - 1];
616 
617   // KeyDescriptors for use by the KVS's EntryCache.
618   Vector<KeyDescriptor, kMaxEntries> key_descriptors_;
619 
620   // An array of addresses associated with the KeyDescriptors for use with the
621   // EntryCache. To support having KeyValueStores with different redundancies,
622   // the addresses are stored in a parallel array instead of inside
623   // KeyDescriptors.
624   internal::EntryCache::AddressList<kRedundancy, kMaxEntries> addresses_;
625 
626   // EntryFormats that can be read by this KeyValueStore.
627   std::array<EntryFormat, kEntryFormats> formats_;
628 };
629 
630 }  // namespace kvs
631 }  // namespace pw
632