Skip to content

Commit

Permalink
Add null check to PackedPointer. (#176)
Browse files Browse the repository at this point in the history
Add ScenarioRunner (testing); more static checks for cache slots/refs.

Fix header double-inclusion guard name.

CR feedback.
  • Loading branch information
tonyastolfi authored Dec 6, 2024
1 parent 3c3f408 commit b1acd08
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 45 deletions.
2 changes: 1 addition & 1 deletion script
Submodule script updated 2 files
+52 −1 batt.py
+5 −0 conan-targets.mk
4 changes: 4 additions & 0 deletions src/llfs/api_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ BATT_STRONG_TYPEDEF(bool, HasOutgoingRefs);
*/
BATT_STRONG_TYPEDEF(usize, ItemOffset);

/** \brief A pseudo-random number generator seed.
*/
BATT_STRONG_TYPEDEF(u32, RandomSeed);

} // namespace llfs

#endif // LLFS_API_TYPES_HPP
31 changes: 6 additions & 25 deletions src/llfs/ioring_log_device2.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include <llfs/ioring_log_device.test.hpp>
#include <llfs/storage_simulation.hpp>

#include <llfs/testing/scenario_runner.hpp>

namespace {

using namespace llfs::constants;
Expand Down Expand Up @@ -511,9 +513,6 @@ class IoRingLogDevice2SimTest : public ::testing::Test

TEST_F(IoRingLogDevice2SimTest, Simulation)
{
const usize kNumThreads = std::thread::hardware_concurrency();
const usize kUpdateInterval = kNumSeeds / 20;

llfs::testing::TestConfig test_config;

LLFS_LOG_INFO() << BATT_INSPECT(kNumSeeds);
Expand All @@ -527,28 +526,10 @@ TEST_F(IoRingLogDevice2SimTest, Simulation)
EXPECT_FALSE(goals.tail_collision);
EXPECT_FALSE(goals.tail_rewrite);

std::vector<std::thread> threads;
usize start_seed = test_config.get_random_seed();
usize seeds_remaining = kNumSeeds;
for (usize i = 0; i < kNumThreads; ++i) {
usize n_seeds = seeds_remaining / (kNumThreads - i);
usize end_seed = start_seed + n_seeds;
threads.emplace_back([&, start_seed, end_seed] {
for (usize seed = start_seed; seed < end_seed; seed += 1) {
LLFS_LOG_INFO_EVERY_N(kUpdateInterval)
<< "progress=" << (seed - start_seed) * 100 / (end_seed - start_seed) << "%";

Scenario scenario{seed, goals};
ASSERT_NO_FATAL_FAILURE(scenario.run());
}
});
start_seed += n_seeds;
seeds_remaining -= n_seeds;
}

for (std::thread& t : threads) {
t.join();
}
llfs::testing::ScenarioRunner{} //
.n_seeds(kNumSeeds)
.n_updates(20)
.run(batt::StaticType<Scenario>{}, goals);

EXPECT_TRUE(goals.concurrent_writes);
EXPECT_TRUE(goals.burst_mode_checked);
Expand Down
1 change: 1 addition & 0 deletions src/llfs/packed_pointer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct PackedPointer {
const T* get() const
{
BATT_STATIC_ASSERT_EQ(sizeof(PackedPointer), sizeof(Offset));
BATT_CHECK_NE(this->offset, 0);

return reinterpret_cast<const T*>(reinterpret_cast<const u8*>(this) + this->offset);
}
Expand Down
22 changes: 11 additions & 11 deletions src/llfs/page_cache_slot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ bool PageCacheSlot::is_pinned() const noexcept

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
//
u32 PageCacheSlot::pin_count() const noexcept
u64 PageCacheSlot::pin_count() const noexcept
{
return Self::get_pin_count(this->state_.load(std::memory_order_acquire));
}
Expand Down Expand Up @@ -116,11 +116,11 @@ auto PageCacheSlot::acquire_pin(PageId key, bool ignore_key) noexcept -> PinnedR
{
const auto old_state = this->state_.fetch_add(kPinCountDelta, std::memory_order_acquire);
const auto new_state = old_state + kPinCountDelta;
const bool newly_pinned = !this->is_pinned(old_state);
const bool newly_pinned = !Self::is_pinned(old_state);

BATT_CHECK_EQ(new_state & Self::kOverflowMask, 0);

BATT_CHECK(this->is_pinned(new_state));
BATT_CHECK(Self::is_pinned(new_state));

BATT_SUPPRESS_IF_GCC("-Wmaybe-uninitialized")

Expand All @@ -134,7 +134,7 @@ auto PageCacheSlot::acquire_pin(PageId key, bool ignore_key) noexcept -> PinnedR
// If the pin_count > 1 (because of the fetch_add above) and the slot is valid, it is safe to read
// the key. If the key doesn't match, release the ref and return failure.
//
if (!this->is_valid(old_state) ||
if (!Self::is_valid(old_state) ||
(!ignore_key && (!this->key_.is_valid() || this->key_ != key))) {
this->release_pin();
return PinnedRef{};
Expand All @@ -149,7 +149,7 @@ auto PageCacheSlot::acquire_pin(PageId key, bool ignore_key) noexcept -> PinnedR
BATT_CHECK(this->value_);
}

return PinnedRef{this};
return PinnedRef{this, CallerPromisesTheyAcquiredPinCount{}};
}

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
Expand Down Expand Up @@ -199,7 +199,7 @@ void PageCacheSlot::release_pin() noexcept
<< BATT_INSPECT(old_state);

const auto new_state = old_state - kPinCountDelta;
const bool newly_unpinned = !this->is_pinned(new_state);
const bool newly_unpinned = !Self::is_pinned(new_state);

BATT_CHECK_EQ(new_state & Self::kOverflowMask, 0);

Expand All @@ -221,7 +221,7 @@ bool PageCacheSlot::evict() noexcept
//
auto observed_state = this->state_.load(std::memory_order_acquire);
for (;;) {
if (Self::is_pinned(observed_state) || !this->is_valid(observed_state)) {
if (Self::is_pinned(observed_state) || !Self::is_valid(observed_state)) {
return false;
}

Expand All @@ -244,7 +244,7 @@ bool PageCacheSlot::evict_if_key_equals(PageId key) noexcept
const auto old_state = this->state_.fetch_add(kPinCountDelta, std::memory_order_acquire);
auto observed_state = old_state + kPinCountDelta;

const bool newly_pinned = !this->is_pinned(old_state);
const bool newly_pinned = !Self::is_pinned(old_state);
if (newly_pinned) {
this->add_ref();
}
Expand All @@ -257,7 +257,7 @@ bool PageCacheSlot::evict_if_key_equals(PageId key) noexcept
for (;;) {
// To succeed, we must be holding the only pin, the slot must be valid, and the key must match.
//
if (!(Self::get_pin_count(observed_state) == 1 && this->is_valid(observed_state) &&
if (!(Self::get_pin_count(observed_state) == 1 && Self::is_valid(observed_state) &&
this->key_ == key)) {
this->release_pin();
return false;
Expand Down Expand Up @@ -300,7 +300,7 @@ auto PageCacheSlot::fill(PageId key) noexcept -> PinnedRef
this->add_ref();
this->set_valid();

return PinnedRef{this};
return PinnedRef{this, CallerPromisesTheyAcquiredPinCount{}};
}

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
Expand Down Expand Up @@ -333,7 +333,7 @@ void PageCacheSlot::notify_last_ref_released()
void PageCacheSlot::set_valid()
{
const auto observed_state = this->state_.fetch_or(kValidMask, std::memory_order_release);
BATT_CHECK(!this->is_valid(observed_state)) << "Must go from an invalid state to valid!";
BATT_CHECK(!Self::is_valid(observed_state)) << "Must go from an invalid state to valid!";
}

} //namespace llfs
4 changes: 2 additions & 2 deletions src/llfs/page_cache_slot.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class PageCacheSlot

/** \brief Returns the pin count bit field within the passed state integer.
*/
static constexpr u32 get_pin_count(u64 state)
static constexpr u64 get_pin_count(u64 state)
{
return state >> kPinCountShift;
}
Expand Down Expand Up @@ -202,7 +202,7 @@ class PageCacheSlot

/** \brief Returns the current pin count of the slot; if this is 0, the slot is not pinned.
*/
u32 pin_count() const noexcept;
u64 pin_count() const noexcept;

/** \brief Conditionally pins the slot so it can't be evicted.
*
Expand Down
13 changes: 8 additions & 5 deletions src/llfs/page_cache_slot.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class PageCacheSlotTest : public ::testing::Test
// 1. Create slots with different index values in a Pool; verify index()
// - also verify initial is_valid state
//
TEST_F(PageCacheSlotTest, CreateSlotsDeath)
TEST_F(PageCacheSlotTest, CreateSlots)
{
for (usize i = 0; i < kNumTestSlots; ++i) {
llfs::PageCacheSlot* slot = this->pool_->allocate();
Expand Down Expand Up @@ -152,7 +152,8 @@ TEST_F(PageCacheSlotTest, StateTransitions)
// i. Valid + Cleared --(acquire_pin)--> Valid + Cleared + Pinned
//
{
llfs::PageCacheSlot::PinnedRef valid_cleared_ref = slot->acquire_pin(llfs::PageId{}, /*ignore_key=*/true);
llfs::PageCacheSlot::PinnedRef valid_cleared_ref =
slot->acquire_pin(llfs::PageId{}, /*ignore_key=*/true);
EXPECT_EQ(valid_cleared_ref.value(), nullptr);
EXPECT_EQ(slot->pin_count(), 1u);
EXPECT_EQ(slot->ref_count(), 1u);
Expand Down Expand Up @@ -421,7 +422,8 @@ TEST_F(PageCacheSlotTest, FillFailureClearedDeath)
slot->clear();

{
llfs::PageCacheSlot::PinnedRef valid_cleared_ref = slot->acquire_pin(llfs::PageId{}, /*ignore_key=*/true);
llfs::PageCacheSlot::PinnedRef valid_cleared_ref =
slot->acquire_pin(llfs::PageId{}, /*ignore_key=*/true);
EXPECT_TRUE(slot->is_valid());
EXPECT_DEATH(slot->fill(llfs::PageId{2}), "Assert.*fail.*is.*valid");
}
Expand Down Expand Up @@ -473,12 +475,13 @@ TEST_F(PageCacheSlotTest, RefCounting)
continue;
}

// Split the workload: let half the threads perform acquire_pin calls and
// Split the workload: let half the threads perform acquire_pin calls and
// let the other half perform evictions.
//
if (i % 2) {
{
llfs::PageCacheSlot::PinnedRef pinned = slot->acquire_pin(llfs::PageId{}, /*ignore_key=*/true);
llfs::PageCacheSlot::PinnedRef pinned =
slot->acquire_pin(llfs::PageId{}, /*ignore_key=*/true);
}
} else {
slot->evict_if_key_equals(llfs::PageId{1});
Expand Down
22 changes: 21 additions & 1 deletion src/llfs/page_cache_slot_pinned_ref.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@

namespace llfs {

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
//
class CallerPromisesTheyAcquiredPinCount
{
private:
CallerPromisesTheyAcquiredPinCount() = default;

//+++++++++++-+-+--+----- --- -- - - - -
// The following declared "friend" functions are the only places that should be acquiring a pin on
// a cache slot and using it to create a PinnedRef!
//+++++++++++-+-+--+----- --- -- - - - -

friend auto PageCacheSlot::acquire_pin(PageId key,
bool ignore_key) noexcept -> PageCacheSlot::PinnedRef;

friend auto PageCacheSlot::fill(PageId key) noexcept -> PageCacheSlot::PinnedRef;
};

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
//
class PageCacheSlot::PinnedRef : public boost::equality_comparable<PageCacheSlot::PinnedRef>
{
public:
Expand All @@ -22,7 +42,7 @@ class PageCacheSlot::PinnedRef : public boost::equality_comparable<PageCacheSlot
PinnedRef() = default;

private:
explicit PinnedRef(PageCacheSlot* slot) noexcept
explicit PinnedRef(PageCacheSlot* slot, CallerPromisesTheyAcquiredPinCount) noexcept
: slot_{slot}
, value_{slot ? slot->value() : nullptr}
{
Expand Down
22 changes: 22 additions & 0 deletions src/llfs/storage_simulation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ class StorageSimulation::TaskSchedulerImpl : public batt::TaskScheduler

//=#=#==#==#===============+=+=+=+=++=++++++++++++++-++-+--+-+----+---------------

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
//
/*explicit*/ StorageSimulation::StorageSimulation(RandomSeed seed) noexcept
: StorageSimulation{batt::StateMachineEntropySource{
/*entropy_fn=*/[rng = std::make_shared<std::default_random_engine>(seed)](
usize min_value, usize max_value) -> usize {
std::uniform_int_distribution<usize> pick_value{min_value, max_value};
return pick_value(*rng);
}}}
{
}

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
//
/*explicit*/ StorageSimulation::StorageSimulation(
Expand Down Expand Up @@ -423,6 +435,16 @@ const batt::SharedPtr<PageCache>& StorageSimulation::init_cache() noexcept
return this->cache_;
}

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
//
std::unique_ptr<LogDeviceFactory> StorageSimulation::get_log_device_factory(const std::string& name,
Optional<u64> capacity)
{
return std::make_unique<BasicLogDeviceFactory>([this, name, capacity] {
return this->get_log_device(name, capacity);
});
}

//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
//
StatusOr<std::unique_ptr<Volume>> StorageSimulation::get_volume(
Expand Down
56 changes: 56 additions & 0 deletions src/llfs/storage_simulation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ class StorageSimulation

//+++++++++++-+-+--+----- --- -- - - - -

/** \brief Creates a new StorageSimulation using a default random engine seeded with the passed
* value.
*/
explicit StorageSimulation(RandomSeed seed) noexcept;

/** \brief Creates a new StorageSimulation context that draws from the passed entropy source.
*/
explicit StorageSimulation(batt::StateMachineEntropySource&& entropy_source) noexcept;
Expand Down Expand Up @@ -121,13 +126,42 @@ class StorageSimulation
*/
batt::TaskScheduler& task_scheduler() noexcept;

/** \brief Convenience: invokes this->task_scheduler().schedule_task() and returns the resulting
* executor.
*/
boost::asio::any_io_executor schedule_task() noexcept
{
return this->task_scheduler().schedule_task();
}

/** \brief Convenience: invokes this->entropy_source().pick_int(min_value, max_value).
*/
usize pick_int(usize min_value, usize max_value) const
{
return this->entropy_source().pick_int(min_value, max_value);
}

/** \brief Convenience: invokes this->entropy_source().pick_branch().
*/
bool pick_branch() const
{
return this->entropy_source().pick_branch();
}

/** \brief Returns a reference to the entropy source passed in at construction time.
*/
batt::StateMachineEntropySource& entropy_source() noexcept
{
return this->entropy_source_;
}

/** \brief Returns a reference to the entropy source passed in at construction time.
*/
const batt::StateMachineEntropySource& entropy_source() const noexcept
{
return this->entropy_source_;
}

/** \brief Returns whether the simulation is currently injecting failures randomly into system
* components.
*
Expand Down Expand Up @@ -203,6 +237,15 @@ class StorageSimulation
*/
std::unique_ptr<LogDevice> get_log_device(const std::string& name, Optional<u64> capacity = None);

/** \brief Creates/accesses a simulated LogDevice via the LogDeviceFactory interface.
*
* \param name A unique name used to identify the LogDevice in the context of this simulation
* \param capacity The maximum size in bytes of the log; must be specified the first time this
* function is called for a given name (i.e., when the log is initially created)
*/
std::unique_ptr<LogDeviceFactory> get_log_device_factory(const std::string& name,
Optional<u64> capacity = None);

/** \brief Creates/accesses a simulated PageDevice.
*
* The args `page_count` and `page_size` must be specified the first time this
Expand Down Expand Up @@ -253,6 +296,19 @@ class StorageSimulation
return this->step_.get_value();
}

/** \brief Blocks the calling task until the simulation step has reached or exceeded the specified
* value.
*
* \return the new step value, or error code if the simulation was interrupted/ended before
* the target step was reached.
*/
StatusOr<u64> await_step(u64 minimum_step) noexcept
{
return this->step_.await_true([minimum_step](u64 observed_step) {
return observed_step >= minimum_step;
});
}

/** \brief Returns true if all blocks for a given page are present in a simulated PageDevice,
* false if they are all absent, error status code otherwise.
*/
Expand Down
Loading

0 comments on commit b1acd08

Please sign in to comment.