Skip to content
Merged
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
90 changes: 90 additions & 0 deletions mooncake-store/include/metadata/eviction_index.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once

// EvictionIndex: the order in which eviction visits one namespace's objects.
// The namespace owns one and tells it about every key the route hands out: a
// read as its read guard is released, a write as its write guard is. Eviction
// reads it through a cursor, oldest-ranked key first.
//
// An index may rank keys however it likes (by lease deadline, by access
// frequency, by size) as long as it keeps one rule: the rank a key is filed at
// is never later than the rank it really has. A read may raise a key's real
// rank without telling the index, which then catches up when the cursor's
// StillAt is asked about the key under its write lock: that release files the
// key again at its real rank. So an index only has to watch reads when a read
// can lower a rank, and says so at construction; the route skips the read hook
// for every other index.
//
// Ranks are comparable across the namespaces of one service, which all use
// the same kind of index, so eviction can merge their cursors.
//
// Lock order: whatever lock an index takes is a leaf under a key lock, and
// nothing takes a key lock while holding it.

#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>

#include "metadata/object_route.h"

namespace mooncake {
namespace metadata {

class EvictionIndex {
public:
using Clock = std::chrono::system_clock;
// Lower ranks are evicted first.
using Rank = int64_t;

// A key as the index filed it.
struct Entry {
std::string key;
Rank rank;
};

// One walk over the filed keys, lowest rank first. What it hands out is a
// snapshot: a key may be refiled or gone by the time it is acted on.
class Cursor {
public:
virtual ~Cursor() = default;
// The next filed key; nullopt once the walk is done.
virtual std::optional<Entry> Next() = 0;
// Whether `entry`, which this cursor handed out, still stands at its
// rank. Asked under the key's write lock, so a key that moved on is
// refiled at its real rank as that lock is let go.
[[nodiscard]] virtual bool StillAt(const route::WriteGuard& guard,
const Entry& entry) const = 0;
// How many keys were filed when the walk began.
[[nodiscard]] virtual size_t Total() const = 0;
};

explicit EvictionIndex(bool observes_reads)
: observes_reads_(observes_reads) {}
EvictionIndex(const EvictionIndex&) = delete;
EvictionIndex& operator=(const EvictionIndex&) = delete;
virtual ~EvictionIndex() = default;

// Called as a read guard on the key is released, with the key's lock held
// shared, possibly by several readers at once; only when the index
// observes reads. It must not touch the slot's owner state, which the
// shared lock does not guard.
virtual void OnRead(const route::ReadGuard& guard) = 0;
// Called as a write guard on the key is released, with the key's lock
// held exclusively: files, refiles or drops the key to match the object
// it holds now.
virtual void OnWrite(const route::WriteGuard& guard) = 0;
// A walk over the keys that may be due by `now`.
[[nodiscard]] virtual std::unique_ptr<Cursor> Scan(
Clock::time_point now) const = 0;

// Whether OnRead must be called.
[[nodiscard]] bool observes_reads() const { return observes_reads_; }

private:
const bool observes_reads_;
};

} // namespace metadata
} // namespace mooncake
168 changes: 168 additions & 0 deletions mooncake-store/include/metadata/lease_eviction_index.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#pragma once

// LeaseEvictionIndex: an EvictionIndex that ranks a key by when its object's
// lease runs out, so eviction visits the objects whose lease ran out first.
//
// A key is filed while it holds an object that is not hard pinned and has a
// completed memory replica. Only writes move a key: a read that extends a
// lease leaves it where it was, so a key is never filed later than its lease
// runs out, only earlier, and the index does not observe reads. A cursor tells
// such a key apart by its real deadline, and releasing its write lock files it
// there, which is the whole of the index's upkeep for reads.
//
// Keys are filed in buckets of kBucketWidth by deadline; a key's rank is its
// bucket. A walk at `now` stops after the bucket `now` falls in, which also
// holds keys whose lease has not run out yet: eviction judges each key's real
// lease anyway, and leaving the bucket out would hold back a lease that ran out
// a moment ago.

#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <utility>
#include <vector>

#include "metadata/eviction_index.h"
#include "metadata/object_route.h"
#include "object_metadata.h"

namespace mooncake {
namespace metadata {

class LeaseEvictionIndex final : public EvictionIndex {
public:
static constexpr Clock::duration kBucketWidth =
std::chrono::milliseconds(100);

LeaseEvictionIndex() : EvictionIndex(/*observes_reads=*/false) {}

// Whether eviction may ever take memory from `metadata`.
[[nodiscard]] static bool Qualifies(const ObjectMetadata& metadata) {
return !metadata.IsHardPinned() &&
metadata.HasReplica([](const Replica& replica) {
return replica.is_memory_replica() && replica.is_completed();
});
}

void OnRead(const route::ReadGuard&) override {}

void OnWrite(const route::WriteGuard& guard) override {
route::SlotOwnerState& owner = guard.owner_state();
const bool file = guard.has_object() && Qualifies(guard.metadata());
const Rank rank = file ? RankOf(guard.metadata()) : 0;
if (file == owner.eviction_filed &&
(!file || rank == owner.eviction_rank)) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (owner.eviction_filed) {
Unfile(guard.slot(), owner.eviction_rank);
}
if (file) {
buckets_[rank].push_back(guard.slot());
++size_;
}
owner.eviction_filed = file;
owner.eviction_rank = rank;
}

[[nodiscard]] std::unique_ptr<Cursor> Scan(
Clock::time_point now) const override {
return std::make_unique<LeaseCursor>(*this, BucketOf(now));
}

private:
using SlotList = route::KeySlot::EvictionList;

// Reads the index one bucket at a time, so it never holds the index lock
// for longer than one bucket takes to copy.
class LeaseCursor final : public Cursor {
public:
LeaseCursor(const LeaseEvictionIndex& index, Rank last)
: index_(index),
last_(last),
total_(index.size_.load(std::memory_order_relaxed)) {}

std::optional<Entry> Next() override {
if (pos_ == buffer_.size()) {
buffer_.clear();
pos_ = 0;
next_ = index_.ReadBucket(next_, last_, buffer_);
}
if (pos_ == buffer_.size()) {
return std::nullopt;
}
return std::move(buffer_[pos_++]);
}

bool StillAt(const route::WriteGuard& guard,
const Entry& entry) const override {
return guard.has_object() && RankOf(guard.metadata()) <= entry.rank;
}

size_t Total() const override { return total_; }

private:
const LeaseEvictionIndex& index_;
// The last bucket the walk reads.
const Rank last_;
const size_t total_;
// The first bucket the walk has not read yet.
Rank next_ = std::numeric_limits<Rank>::min();
std::vector<Entry> buffer_;
size_t pos_ = 0;
};

static Rank RankOf(const ObjectMetadata& metadata) {
return BucketOf(metadata.EvictionDeadline());
}

static Rank BucketOf(Clock::time_point deadline) {
const auto since_epoch = deadline.time_since_epoch();
Rank bucket = since_epoch / kBucketWidth;
// Round toward the earlier bucket for a deadline before the epoch.
if (since_epoch < Clock::duration::zero() &&
since_epoch % kBucketWidth != Clock::duration::zero()) {
--bucket;
}
return bucket;
}

// Copies the keys of the first non-empty bucket in [from, last] into
// `out` and returns the bucket after it; returns `last + 1` when there is
// none.
Rank ReadBucket(Rank from, Rank last, std::vector<Entry>& out) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = buckets_.lower_bound(from);
if (it == buckets_.end() || it->first > last) {
return last + 1;
}
for (const route::KeySlot& slot : it->second) {
out.push_back({slot.key(), it->first});
}
return it->first + 1;
}

void Unfile(route::KeySlot& slot, Rank rank) {
const auto it = buckets_.find(rank);
it->second.erase(it->second.iterator_to(slot));
if (it->second.empty()) {
buckets_.erase(it);
}
--size_;
}

mutable std::mutex mutex_;
// A node map, so a bucket's list stays put while others come and go.
std::map<Rank, SlotList> buckets_;
std::atomic<size_t> size_{0};
};

} // namespace metadata
} // namespace mooncake
Loading
Loading