blob: 3f37f81a932497826ead7d9db3e81712372712f2 [file] [log] [blame]
// 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.
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include "pw_containers/vector.h"
#include "pw_kvs/checksum.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/format.h"
#include "pw_kvs/internal/entry.h"
#include "pw_kvs/internal/entry_cache.h"
#include "pw_kvs/internal/key_descriptor.h"
#include "pw_kvs/internal/sectors.h"
#include "pw_kvs/internal/span_traits.h"
#include "pw_kvs/key.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
namespace pw {
namespace kvs {
enum class GargbageCollectOnWrite {
// Disable all automatic garbage collection on write.
kDisabled,
// Allow up to a single sector, if needed, to be garbage collected on write.
kOneSector,
// Allow as many sectors as needed be garbage collected on write.
kAsManySectorsNeeded,
};
enum class ErrorRecovery {
// Immediately do full recovery of any errors that are detected.
kImmediate,
// Recover from errors, but delay time consuming recovery steps until later
// as part of other time consuming operations. Such as waiting to garbage
// collect sectors with corrupt entries until the next garbage collection.
kLazy,
// Only recover from errors when manually triggered as part of maintenance
// operations. This is not recommended for normal use, only for test or
// read-only use.
kManual,
};
struct Options {
// Perform garbage collection if necessary when writing. If not kDisabled,
// garbage collection is attempted if space for an entry cannot be found. This
// is a relatively lengthy operation. If kDisabled, Put calls that would
// require garbage collection fail with RESOURCE_EXHAUSTED.
GargbageCollectOnWrite gc_on_write =
GargbageCollectOnWrite::kAsManySectorsNeeded;
// When the KVS handles errors that are discovered, such as corrupt entries,
// not enough redundant copys of an entry, etc.
ErrorRecovery recovery = ErrorRecovery::kLazy;
// Verify an entry's checksum after reading it from flash.
bool verify_on_read = true;
// Verify an in-flash entry's checksum after writing it.
bool verify_on_write = true;
};
/// Flash-backed persistent key-value store (KVS) with integrated
/// wear-leveling.
///
/// Instances are declared as instances of
/// `pw::kvs::KeyValueStoreBuffer<MAX_ENTRIES, MAX_SECTORS>`, which allocates
/// buffers for tracking entries and flash sectors.
///
/// @code{.cpp}
/// #include "pw_kvs/key_value_store.h"
/// #include "pw_kvs/flash_test_partition.h"
///
/// constexpr size_t kMaxSectors = 6;
/// constexpr size_t kMaxEntries = 64;
/// static constexpr pw::kvs::EntryFormat kvs_format = {
/// .magic = 0xd253a8a9, // Prod apps should use a random number here
/// .checksum = nullptr
/// };
/// pw::kvs::KeyValueStoreBuffer<kMaxEntries, kMaxSectors> kvs(
/// &pw::kvs::FlashTestPartition(),
/// kvs_format
/// );
///
/// kvs.Init();
/// @endcode
class KeyValueStore {
public:
/// Initializes the KVS. Must be called before calling other functions.
///
/// @returns @rst
///
/// .. pw-status-codes::
///
/// OK: The KVS successfully initialized.
///
/// DATA_LOSS: The KVS initialized and is usable, but contains corrupt
/// data.
///
/// UNKNOWN: Unknown error. The KVS is not initialized.
///
/// @endrst
Status Init();
bool initialized() const {
return initialized_ == InitializationState::kReady;
}
/// Reads the value of an entry in the KVS. The value is read into the
/// provided buffer and the number of bytes read is returned. Reads can be
/// started at an offset.
///
/// @param[in] key The name of the key.
///
/// @param[out] value The buffer to read the key's value into.
///
/// @param[in] offset_bytes The byte offset to start the read at. Optional.
///
/// @returns @rst
///
/// .. pw-status-codes::
///
/// OK: The entry was successfully read.
///
/// NOT_FOUND: The key is not present in the KVS.
///
/// DATA_LOSS: Found the entry, but the data was corrupted.
///
/// RESOURCE_EXHAUSTED: The buffer could not fit the entire
/// value, but as many bytes as possible were written to it. The number of
/// of bytes read is returned. The remainder of the value can be read by
/// calling ``Get()`` again with an offset.
///
/// FAILED_PRECONDITION: The KVS is not initialized. Call ``Init()``
/// before calling this method.
///
/// INVALID_ARGUMENT: ``key`` is empty or too long, or ``value``
/// is too large.
///
/// @endrst
StatusWithSize Get(Key key,
span<std::byte> value,
size_t offset_bytes = 0) const;
/// Overload of `Get()` that accepts a pointer to a trivially copyable
/// object.
///
/// If `value` is an array, call `Get()` with
/// `as_writable_bytes(span(array))`, or pass a pointer to the array
/// instead of the array itself.
template <typename Pointer,
typename = std::enable_if_t<std::is_pointer<Pointer>::value>>
Status Get(const Key& key, const Pointer& pointer) const {
using T = std::remove_reference_t<std::remove_pointer_t<Pointer>>;
CheckThatObjectCanBePutOrGet<T>();
return FixedSizeGet(key, pointer, sizeof(T));
}
/// Adds a key-value entry to the KVS. If the key was already present, its
/// value is overwritten.
///
/// @param[in] key The name of the key. All keys in the KVS must have a
/// unique hash. If the hash of your key matches an existing key, nothing is
/// added and @pw_status{ALREADY_EXISTS} is returned.
///
/// @param[in] value The value for the key. This can be a span of bytes or a
/// trivially copyable object.
///
/// @returns @rst
///
/// .. pw-status-codes::
///
/// OK: The entry was successfully added or updated.
///
/// DATA_LOSS: Checksum validation failed after writing data.
///
/// RESOURCE_EXHAUSTED: Not enough space to add the entry.
///
/// ALREADY_EXISTS: The entry could not be added because a different
/// key with the same hash is already in the KVS.
///
/// FAILED_PRECONDITION: The KVS is not initialized. Call ``Init()``
/// before calling this method.
///
/// INVALID_ARGUMENT: ``key`` is empty or too long, or ``value``
/// is too large.
///
/// @endrst
template <typename T,
typename std::enable_if_t<ConvertsToSpan<T>::value>* = nullptr>
Status Put(const Key& key, const T& value) {
return PutBytes(key, as_bytes(internal::make_span(value)));
}
template <typename T,
typename std::enable_if_t<!ConvertsToSpan<T>::value>* = nullptr>
Status Put(const Key& key, const T& value) {
CheckThatObjectCanBePutOrGet<T>();
return PutBytes(key, as_bytes(span<const T>(&value, 1)));
}
/// Removes a key-value entry from the KVS.
///
/// @param[in] key - The name of the key-value entry to delete.
///
/// @returns @rst
///
/// .. pw-status-codes::
///
/// OK: The entry was successfully deleted.
///
/// NOT_FOUND: ``key`` is not present in the KVS.
///
/// DATA_LOSS: Checksum validation failed after recording the
/// erase.
///
/// RESOURCE_EXHAUSTED: Insufficient space to mark the entry as deleted.
///
/// FAILED_PRECONDITION: The KVS is not initialized. Call ``Init()``
/// before calling this method.
///
/// INVALID_ARGUMENT: ``key`` is empty or too long.
///
/// @endrst
Status Delete(Key key);
/// Returns the size of the value corresponding to the key.
///
/// @param[in] key - The name of the key.
///
/// @returns @rst
///
/// .. pw-status-codes::
///
/// OK: The size was returned successfully.
///
/// NOT_FOUND: ``key`` is not present in the KVS.
///
/// DATA_LOSS: Checksum validation failed after reading the entry.
///
/// FAILED_PRECONDITION: The KVS is not initialized. Call ``Init()``
/// before calling this method.
///
/// INVALID_ARGUMENT: ``key`` is empty or too long.
///
/// @endrst
StatusWithSize ValueSize(Key key) const;
/// Performs all maintenance possible, including all needed repairing of
/// corruption and garbage collection of reclaimable space in the KVS. When
/// configured for manual recovery, this (along with `FullMaintenance()`) is
/// the only way KVS repair is triggered.
///
/// @warning Performs heavy garbage collection of all reclaimable space,
/// regardless of whether there's other valid data in the sector. This
/// method may cause a significant amount of moving of valid entries.
Status HeavyMaintenance() {
return FullMaintenanceHelper(MaintenanceType::kHeavy);
}
/// Perform all maintenance possible, including all needed repairing of
/// corruption and garbage collection of reclaimable space in the KVS. When
/// configured for manual recovery, this (along with `HeavyMaintenance()`) is
/// the only way KVS repair is triggered.
///
/// Does not garbage collect sectors with valid data unless the KVS is mostly
/// full.
Status FullMaintenance() {
return FullMaintenanceHelper(MaintenanceType::kRegular);
}
/// Performs a portion of KVS maintenance. If configured for at least lazy
/// 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();
void LogDebugInfo() const;
// Classes and functions to support STL-style iteration.
class iterator;
/// Representation of a key-value entry during iteration.
class Item {
public:
/// @returns The key as a null-terminated string.
const char* key() const { return key_buffer_.data(); }
/// @returns The value referred to by this iterator. Equivalent to
/// `pw::kvs::KeyValueStore::Get()`.
StatusWithSize Get(span<std::byte> value_buffer,
size_t offset_bytes = 0) const {
return kvs_.Get(key(), *iterator_, value_buffer, offset_bytes);
}
template <typename Pointer,
typename = std::enable_if_t<std::is_pointer<Pointer>::value>>
Status Get(const Pointer& pointer) const {
using T = std::remove_reference_t<std::remove_pointer_t<Pointer>>;
CheckThatObjectCanBePutOrGet<T>();
return kvs_.FixedSizeGet(key(), *iterator_, pointer, sizeof(T));
}
// Reads the size of the value referred to by this iterator. Equivalent to
// KeyValueStore::ValueSize.
StatusWithSize ValueSize() const { return kvs_.ValueSize(*iterator_); }
private:
friend class iterator;
constexpr Item(const KeyValueStore& kvs,
const internal::EntryCache::const_iterator& item_iterator)
: kvs_(kvs), iterator_(item_iterator), key_buffer_{} {}
void ReadKey();
const KeyValueStore& kvs_;
internal::EntryCache::const_iterator iterator_;
// Buffer large enough for a null-terminated version of any valid key.
std::array<char, internal::Entry::kMaxKeyLength + 1> key_buffer_;
};
/// Supported iteration methods.
class iterator {
public:
/// Increments to the next key-value entry in the container.
iterator& operator++();
/// Increments to the next key-value entry in the container.
iterator operator++(int) {
const iterator original(item_.kvs_, item_.iterator_);
operator++();
return original;
}
/// Reads the entry's key from flash.
const Item& operator*() {
item_.ReadKey();
return item_;
}
/// Reads the entry into the `Item` object.
const Item* operator->() { return &operator*(); }
/// Equality comparison of two entries.
constexpr bool operator==(const iterator& rhs) const {
return item_.iterator_ == rhs.item_.iterator_;
}
/// Inequality comparison of two entries.
constexpr bool operator!=(const iterator& rhs) const {
return item_.iterator_ != rhs.item_.iterator_;
}
private:
friend class KeyValueStore;
constexpr iterator(
const KeyValueStore& kvs,
const internal::EntryCache::const_iterator& item_iterator)
: item_(kvs, item_iterator) {}
Item item_;
};
using const_iterator = iterator; // Standard alias for iterable types.
/// @returns The first key-value entry in the container. Used for iteration.
iterator begin() const;
/// @returns The last key-value entry in the container. Used for iteration.
iterator end() const { return iterator(*this, entry_cache_.end()); }
/// @returns The number of valid entries in the KVS.
size_t size() const { return entry_cache_.present_entries(); }
/// @returns The number of valid entries and deleted entries yet to be
/// collected.
size_t total_entries_with_deleted() const {
return entry_cache_.total_entries();
}
/// @returns The maximum number of KV entries that's possible in the KVS.
size_t max_size() const { return entry_cache_.max_entries(); }
/// @returns `true` if the KVS is empty.
size_t empty() const { return size() == 0u; }
/// @returns The number of transactions that have occurred since the KVS was
/// first used. This value is retained across initializations, but is reset
/// if the underlying flash is erased.
uint32_t transaction_count() const { return last_transaction_id_; }
/// Statistics about the KVS.
///
/// Statistics are since the KVS init. They're not retained across reboots.
struct StorageStats {
/// The number of writeable bytes remaining in the KVS. This number doesn't
/// include the one empty sector required for KVS garbage collection.
size_t writable_bytes;
/// The number of bytes in the KVS that are already in use.
size_t in_use_bytes;
/// The maximum number of bytes possible to reclaim by garbage collection.
/// The number of bytes actually reclaimed by maintenance depends on the
/// type of maintenance that's performed.
size_t reclaimable_bytes;
/// The total count of individual sector erases that have been performed.
size_t sector_erase_count;
/// The number of corrupt sectors that have been recovered.
size_t corrupt_sectors_recovered;
/// The number of missing redundant copies of entries that have been
/// recovered.
size_t missing_redundant_entries_recovered;
};
/// @returns A `StorageStats` struct with details about the current and past
/// state of the KVS.
StorageStats GetStorageStats() const;
/// @returns The number of identical copies written for each entry. A
/// redundancy of 1 means that only a single copy is written for each entry.
size_t redundancy() const { return entry_cache_.redundancy(); }
/// @returns `true` if the KVS has any unrepaired errors.
bool error_detected() const { return error_detected_; }
/// @returns The maximum number of bytes allowed for a key-value combination.
size_t max_key_value_size_bytes() const {
return max_key_value_size_bytes(partition_.sector_size_bytes());
}
/// @returns The maximum number of bytes allowed for a given sector size for
/// a key-value combination.
static constexpr size_t max_key_value_size_bytes(
size_t partition_sector_size_bytes) {
return partition_sector_size_bytes - Entry::entry_overhead();
}
/// Checks the KVS for any error conditions and returns `true` if any errors
/// are present. Primarily intended for test and internal use.
bool CheckForErrors();
protected:
using Address = FlashPartition::Address;
using Entry = internal::Entry;
using KeyDescriptor = internal::KeyDescriptor;
using SectorDescriptor = internal::SectorDescriptor;
// In the future, will be able to provide additional EntryFormats for
// backwards compatibility.
KeyValueStore(FlashPartition* partition,
span<const EntryFormat> formats,
const Options& options,
size_t redundancy,
Vector<SectorDescriptor>& sector_descriptor_list,
const SectorDescriptor** temp_sectors_to_skip,
Vector<KeyDescriptor>& key_descriptor_list,
Address* addresses);
private:
using EntryMetadata = internal::EntryMetadata;
using EntryState = internal::EntryState;
template <typename T>
static constexpr void CheckThatObjectCanBePutOrGet() {
static_assert(
std::is_trivially_copyable<T>::value && !std::is_pointer<T>::value,
"Only trivially copyable, non-pointer objects may be Put and Get by "
"value. Any value may be stored by converting it to a byte span "
"with as_bytes(span(&value, 1)) or "
"as_writable_bytes(span(&value, 1)).");
}
Status InitializeMetadata();
Status LoadEntry(Address entry_address, Address* next_entry_address);
Status ScanForEntry(const SectorDescriptor& sector,
Address start_address,
Address* next_entry_address);
// Remove deleted keys from the entry cache, including freeing sector bytes
// used by those keys. This must only be done directly after a full garbage
// collection, otherwise the current deleted entry could be garbage
// collected before the older stale entry producing a window for an
// invalid/corrupted KVS state if there was a power-fault, crash or other
// interruption.
Status RemoveDeletedKeyEntries();
Status PutBytes(Key key, span<const std::byte> value);
StatusWithSize ValueSize(const EntryMetadata& metadata) const;
Status ReadEntry(const EntryMetadata& metadata, Entry& entry) const;
// Finds the metadata for an entry matching a particular key. Searches for a
// KeyDescriptor that matches this key and sets *metadata_out to point to it
// if one is found.
//
// OK: there is a matching descriptor and *metadata is set
// NOT_FOUND: there is no descriptor that matches this key, but this key
// has a unique hash (and could potentially be added to the
// KVS)
// ALREADY_EXISTS: there is no descriptor that matches this key, but the
// key's hash collides with the hash for an existing
// descriptor
//
Status FindEntry(Key key, EntryMetadata* metadata_out) const;
// Searches for a KeyDescriptor that matches this key and sets *metadata_out
// to point to it if one is found.
//
// OK: there is a matching descriptor and *metadata_out is set
// NOT_FOUND: there is no descriptor that matches this key
//
Status FindExisting(Key key, EntryMetadata* metadata_out) const;
StatusWithSize Get(Key key,
const EntryMetadata& metadata,
span<std::byte> value_buffer,
size_t offset_bytes) const;
Status FixedSizeGet(Key key, void* value, size_t size_bytes) const;
Status FixedSizeGet(Key key,
const EntryMetadata& metadata,
void* value,
size_t size_bytes) const;
Status CheckWriteOperation(Key key) const;
Status CheckReadOperation(Key key) const;
Status WriteEntryForExistingKey(EntryMetadata& metadata,
EntryState new_state,
Key key,
span<const std::byte> value);
Status WriteEntryForNewKey(Key key, span<const std::byte> value);
Status WriteEntry(Key key,
span<const std::byte> value,
EntryState new_state,
EntryMetadata* prior_metadata = nullptr,
const internal::Entry* prior_entry = nullptr);
EntryMetadata CreateOrUpdateKeyDescriptor(const Entry& new_entry,
Key key,
EntryMetadata* prior_metadata,
size_t prior_size);
EntryMetadata UpdateKeyDescriptor(const Entry& entry,
Address new_address,
EntryMetadata* prior_metadata,
size_t prior_size);
Status GetAddressesForWrite(Address* write_addresses, size_t write_size);
Status GetSectorForWrite(SectorDescriptor** sector,
size_t entry_size,
span<const Address> reserved_addresses);
Status MarkSectorCorruptIfNotOk(Status status, SectorDescriptor* sector);
Status AppendEntry(const Entry& entry, Key key, span<const std::byte> value);
StatusWithSize CopyEntryToSector(Entry& entry,
SectorDescriptor* new_sector,
Address new_address);
Status RelocateEntry(const EntryMetadata& metadata,
KeyValueStore::Address& address,
span<const Address> reserved_addresses);
// Perform all maintenance possible, including all neeeded repairing of
// corruption and garbage collection of reclaimable space in the KVS. When
// configured for manual recovery, this is the only way KVS repair is
// triggered.
//
// - Regular will not garbage collect sectors with valid data unless the KVS
// is mostly full.
// - Heavy will garbage collect all reclaimable space regardless of valid data
// in the sector.
enum class MaintenanceType {
kRegular,
kHeavy,
};
Status FullMaintenanceHelper(MaintenanceType maintenance_type);
// Find and garbage collect a singe sector that does not include a reserved
// address.
Status GarbageCollect(span<const Address> reserved_addresses);
Status RelocateKeyAddressesInSector(SectorDescriptor& sector_to_gc,
const EntryMetadata& metadata,
span<const Address> reserved_addresses);
Status GarbageCollectSector(SectorDescriptor& sector_to_gc,
span<const Address> reserved_addresses);
// Ensure that all entries are on the primary (first) format. Entries that are
// not on the primary format are rewritten.
//
// Return: status + number of entries updated.
StatusWithSize UpdateEntriesToPrimaryFormat();
Status AddRedundantEntries(EntryMetadata& metadata);
Status RepairCorruptSectors();
Status EnsureFreeSectorExists();
Status EnsureEntryRedundancy();
Status FixErrors();
Status Repair();
internal::Entry CreateEntry(Address address,
Key key,
span<const std::byte> value,
EntryState state);
void LogSectors() const;
void LogKeyDescriptor() const;
FlashPartition& partition_;
const internal::EntryFormats formats_;
// List of sectors used by this KVS.
internal::Sectors sectors_;
// Unordered list of KeyDescriptors. Finding a key requires scanning and
// verifying a match by reading the actual entry.
internal::EntryCache entry_cache_;
Options options_;
// Threshold value for when to garbage collect all stale data. Above the
// threshold, GC all reclaimable bytes regardless of if valid data is in
// sector. Below the threshold, only GC sectors with reclaimable bytes and no
// valid bytes. The threshold is based on the portion of KVS capacity used by
// valid bytes.
static constexpr size_t kGcUsageThresholdPercentage = 70;
enum class InitializationState {
// KVS Init() has not been called and KVS is not usable.
kNotInitialized,
// KVS Init() run but not all the needed invariants are met for an operating
// KVS. KVS is not writable, but read operaions are possible.
kNeedsMaintenance,
// KVS is fully ready for use.
kReady,
};
InitializationState initialized_;
// error_detected_ needs to be set from const KVS methods (such as Get), so
// make it mutable.
mutable bool error_detected_;
struct InternalStats {
size_t sector_erase_count;
size_t corrupt_sectors_recovered;
size_t missing_redundant_entries_recovered;
};
InternalStats internal_stats_;
uint32_t last_transaction_id_;
};
template <size_t kMaxEntries,
size_t kMaxUsableSectors,
size_t kRedundancy = 1,
size_t kEntryFormats = 1>
class KeyValueStoreBuffer : public KeyValueStore {
public:
// Constructs a KeyValueStore on the partition, with support for one
// EntryFormat (kEntryFormats must be 1).
KeyValueStoreBuffer(FlashPartition* partition,
const EntryFormat& format,
const Options& options = {})
: KeyValueStoreBuffer(
partition,
span<const EntryFormat, kEntryFormats>(
reinterpret_cast<const EntryFormat (&)[1]>(format)),
options) {
static_assert(kEntryFormats == 1,
"kEntryFormats EntryFormats must be specified");
}
// Constructs a KeyValueStore on the partition. Supports multiple entry
// formats. The first EntryFormat is used for new entries.
KeyValueStoreBuffer(FlashPartition* partition,
span<const EntryFormat, kEntryFormats> formats,
const Options& options = {})
: KeyValueStore(partition,
formats_,
options,
kRedundancy,
sectors_,
temp_sectors_to_skip_,
key_descriptors_,
addresses_),
sectors_(),
key_descriptors_(),
formats_() {
std::copy(formats.begin(), formats.end(), formats_.begin());
}
private:
static_assert(kMaxEntries > 0u);
static_assert(kMaxUsableSectors > 0u);
static_assert(kRedundancy > 0u);
static_assert(kEntryFormats > 0u);
Vector<SectorDescriptor, kMaxUsableSectors> sectors_;
// Allocate space for an list of SectorDescriptors to avoid while searching
// for space to write entries. This is used to avoid changing sectors that are
// reserved for a new entry or marked as having other redundant copies of an
// entry. Up to kRedundancy sectors are reserved for a new entry, and up to
// kRedundancy - 1 sectors can have redundant copies of an entry, giving a
// maximum of 2 * kRedundancy - 1 sectors to avoid.
const SectorDescriptor* temp_sectors_to_skip_[2 * kRedundancy - 1];
// KeyDescriptors for use by the KVS's EntryCache.
Vector<KeyDescriptor, kMaxEntries> key_descriptors_;
// An array of addresses associated with the KeyDescriptors for use with the
// EntryCache. To support having KeyValueStores with different redundancies,
// the addresses are stored in a parallel array instead of inside
// KeyDescriptors.
internal::EntryCache::AddressList<kRedundancy, kMaxEntries> addresses_;
// EntryFormats that can be read by this KeyValueStore.
std::array<EntryFormat, kEntryFormats> formats_;
};
} // namespace kvs
} // namespace pw