Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
581 changes: 542 additions & 39 deletions category/async/storage_pool.cpp

Large diffs are not rendered by default.

211 changes: 208 additions & 3 deletions category/async/storage_pool.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@

#include <atomic>
#include <filesystem>
#include <optional>
#include <type_traits>
#include <variant>

MONAD_ASYNC_NAMESPACE_BEGIN

namespace test
{
struct StoragePoolTestAccess; // test-only access to the hash formulae
}

/* \brief Makes available the lowest possible latency zoned storage, if `zonefs`
is available. Otherwise falls back to an emulation which can use a file on a
filesystem, or a block device.
Expand Down Expand Up @@ -66,6 +72,8 @@ pathological i/o performance loss at usually the most inconvenient times.
*/
class storage_pool
{
friend struct test::StoragePoolTestAccess;

public:
//! \brief Type of chunk, conventional or sequential
enum chunk_type
Expand Down Expand Up @@ -144,21 +152,46 @@ class storage_pool
}
} *const metadata_;

// True if this open wrote the device's footer: the device was blank,
// or it was opened with mode::truncate.
bool const is_freshly_initialised_;

static_assert(sizeof(metadata_t) == 64);

// Base address and length of the mapping make_device_ established over
// this device's metadata. The base is CPU page aligned.
std::pair<void *, size_t> metadata_mapping_() const noexcept;

constexpr device_t(
int const readwritefd, type_t_ const type,
uint64_t const unique_hash, file_offset_t const size_of_file,
metadata_t *const metadata)
metadata_t *const metadata, bool const is_freshly_initialised)
: readwritefd_(readwritefd)
, type_(type)
, unique_hash_(unique_hash)
, size_of_file_(size_of_file)
, metadata_(metadata)
, is_freshly_initialised_(is_freshly_initialised)
{
}

public:
//! Returns whether this open wrote the device's footer, which it does
//! on a blank device and under mode::truncate
bool is_freshly_initialised() const noexcept
{
return is_freshly_initialised_;
}

//! The size of the device in bytes, as of when this pool opened it.
//! This is the quantity chunks() and the device's unique_hash are
//! derived from, so it is what has to be recorded to later recover
//! the geometry of a device grown in place.
file_offset_t size_bytes() const noexcept
{
return size_of_file_;
}

//! The current filesystem path of the device (it can change over time)
std::filesystem::path current_path() const;

Expand Down Expand Up @@ -295,11 +328,46 @@ class storage_pool
//! \brief What to do when opening the pool for use.
enum class mode
{
//! The source must already carry pool metadata; abort if it does
//! not.
open_existing,
//! Initialise the source if it does not, otherwise open it as it is.
create_if_needed,
truncate
//! Discard the source's contents and initialise it.
truncate,
//! Take up storage the source now offers but the pool does not yet
//! use, by relocating the metadata of a device extended in place.
//! Existing data is kept, and re-running resumes an interrupted run.
rescan
};

//! \brief How much space a database's metadata needs in the first half of
//! conventional chunk 0, supplied by the caller that owns that layout so
//! this layer needs no knowledge of it.
struct db_metadata_budget
{
//! Fixed header, ahead of the per-chunk array. Should be the largest
//! of any on-disk format the caller can still read, so a pool stays
//! migratable without remapping.
size_t header_bytes;
//! Cost of each sequential chunk in the per-chunk array.
size_t bytes_per_chunk;

//! A pool with no database on it, which has nothing to fit. Spelled
//! out so that skipping the check is a decision rather than an
//! omission.
static constexpr db_metadata_budget no_database() noexcept
{
return {.header_bytes = 0, .bytes_per_chunk = 0};
}
};

//! Smallest chunk capacity any pool carrying a database can have been
//! created with. This layer enforces no minimum of its own; the floor
//! comes from the database metadata having to fit in half of conventional
//! chunk 0, which the owning layer enforces on open.
static constexpr uint32_t min_chunk_capacity = 1u << 21;

//! \brief Flags for storage pool creation
struct creation_flags
{
Expand Down Expand Up @@ -327,13 +395,23 @@ class storage_pool
//! Number of conventional chunks to allocate. Default is 3.
uint32_t num_cnv_chunks;

//! What db_metadata recorded as the size of the device before the
//! extend.
std::optional<file_offset_t> recorded_size_of_grown_device;

//! Space the database's metadata needs, from the caller that owns that
//! layout. Nothing if there is no database.
std::optional<db_metadata_budget> metadata_budget;

constexpr creation_flags()
: chunk_capacity(28)
, open_read_only(false)
, open_read_only_allow_dirty(false)
, disable_mismatching_storage_pool_check(false)
, allow_migration(false)
, num_cnv_chunks(3)
, recorded_size_of_grown_device(std::nullopt)
, metadata_budget(std::nullopt)
{
}

Expand All @@ -348,12 +426,107 @@ class storage_pool

private:
bool const is_read_only_, is_read_only_allow_dirty_, is_migration_allowed_,
is_newly_truncated_;
is_newly_truncated_, is_rescanning_;
device_t device_;
// A chunk's whole geometry follows from its id, so these counts are all
// the pool keeps per chunk type.
uint32_t cnv_chunks_count_{0}, seq_chunks_count_{0};

// The pool metadata a device carries in its final sizeof(metadata_t)
// bytes, as read back. Absent on a blank device, and on one extended in
// place, which strands the footer mid-device.
struct device_pool_metadata_
{
uint32_t chunk_capacity;
uint32_t num_cnv_chunks;
uint32_t config_hash;
size_t chunks;
};

// Read-only description of the source, gathered before anything is
// written, so a refused device is never modified. unique_hash is stored
// already-computed rather than as its inputs, so this can equally describe
// a live device_t, which keeps only the finished hash.
struct device_info_
{
device_t::type_t_ type;
uint64_t unique_hash;
// Current size: BLKGETSIZE64 for a block device, st_size for a file.
file_offset_t size;
std::optional<device_pool_metadata_> pool_metadata;
// The device number compute_unique_hash_ was given, so that the hash
// can be recomputed at a different size -- which is what validating a
// grown device's previous size needs.
uint64_t hash_dev_no;
};

// Everything about `source`, including the pool metadata read back from
// its end.
static device_info_ read_device_info_(std::filesystem::path const &source);

// The hash formulae, each in one place so the validating pre-pass and
// adopt_device_ cannot drift apart. Members rather than file-local statics
// because device_t::type_t_ is private to device_t and only storage_pool
// is its friend.
static uint64_t compute_unique_hash_(
device_t::type_t_ type, uint64_t dev_no, file_offset_t size);

static uint32_t compute_config_hash_(device_info_ const &);

// A source which grew in place: it presents no footer at the end its
// current size gives it, because extending strands the footer mid-device,
// but carries a stranded one below which validates against the pool's own
// hash.
struct grown_device_
{
// The recorded size, once validated: the size this device had when it
// was last part of this pool, as validated against the
// stranded footer's own config_hash.
file_offset_t previous_size;
size_t previous_chunks; // chunks() at previous_size
uint32_t chunk_capacity; // read back from the stranded footer
uint32_t num_cnv_chunks;
};

static device_info_ device_info_of_(device_t const &);

// The device as it was before it grew, which is what the pool's pre-grow
// config_hash covers.
static device_info_ device_info_at_previous_size_(
device_info_ const &now, grown_device_ const &grown);

// Reads the sizeof(metadata_t) bytes a footer would occupy if the device
// were exactly `size` bytes long, and returns it if it carries the magic.
static std::optional<device_t::metadata_t>
read_footer_for_size_(int fd, file_offset_t size);

// Checks `recorded_size` against the footer stranded there by the extend
// which grew `source`, for a source presenting no footer at its end.
// Returns nothing unless that footer describes the pool as it was at
// `recorded_size`; `footer_found` then distinguishes a size with no footer
// at all from one whose footer belongs elsewhere. Reads only.
static std::optional<grown_device_> validate_grown_device_(
std::filesystem::path const &source, device_info_ const &current,
std::optional<file_offset_t> recorded_size, bool &footer_found);

// Aborts, having written nothing, if `info` is not a device this pool can
// take up: not blank and carrying no footer that `recorded_size` explains,
// or grown past what the chunk id space or `budget` allows.
static std::optional<grown_device_> validate_device_to_rescan_(
std::filesystem::path const &source, device_info_ const &info,
std::optional<file_offset_t> recorded_size,
std::optional<db_metadata_budget> const &budget);

// Writes the grown device's metadata region at the end its current size
// gives it: the bytes-used array carried over from the region stranded at
// `grown.previous_size` with the new chunks zeroed, then the footer. The
// footer is written and made durable last, so it is the commit record --
// and with one device it is the only one, so nothing else has to be
// durable first.
static void relocate_device_metadata_(
std::filesystem::path const &source, file_offset_t current_size,
grown_device_ const &grown, uint32_t new_config_hash);

static device_t make_device_(
mode op, device_t::type_t_ type, std::filesystem::path const &path,
int fd, std::variant<uint64_t, device_t const *> dev_no_or_dev,
Expand Down Expand Up @@ -427,12 +600,44 @@ class storage_pool
return is_newly_truncated_;
}

//! \brief True if the storage pool was opened with mode::rescan.
//! Consulted by DbMetadataContext to decide whether a pool reporting more
//! chunks than the metadata describes should be grown or rejected with a
//! "run monad-mpt --rescan-devices" message.
bool is_rescanning() const noexcept
{
return is_rescanning_;
}

//! \brief Returns the backing storage device
device_t const &device() const noexcept
{
return device_;
}

//! \brief What a mode::rescan open of `source` would do, decided without
//! writing anything.
struct rescan_preview
{
//! The validated previous size of the source: the size it had before
//! it was extended in place, zero if it was not. Its contents are kept
//! either way.
file_offset_t grown_previous_size;
//! Total chunks, cnv and seq, the source offered at that size.
size_t grown_previous_chunks;
};

//! \brief Classifies `source` as a mode::rescan open would, writing
//! nothing. It applies the same refusals, so a caller can put an accurate
//! confirmation prompt in front of the operation and know the operation
//! will not then refuse it. `recorded_size` is what db_metadata holds for
//! the source; the validated previous size it yields comes back in
//! `grown_previous_size`.
static rescan_preview preview_rescan(
Comment thread
maxkozlovsky marked this conversation as resolved.
std::filesystem::path const &source,
std::optional<file_offset_t> recorded_size,
std::optional<db_metadata_budget> const &budget);

//! \brief Returns the number of chunks for the specified type
size_t chunks(chunk_type const which) const noexcept
{
Expand Down
Loading
Loading