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
23 changes: 16 additions & 7 deletions mooncake-store/include/replica_selection.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,16 @@ inline const Replica::Descriptor *PickBestRemoteMemory(

// Select the best replica from a list: prefer local MEMORY, local NOF_SSD,
// remote MEMORY, remote NOF_SSD, LOCAL_DISK, DFS, then DISK. Master may return
// replicas in any order, so we always scan. When scoring is enabled and there
// are multiple remote MEMORY replicas, the best-scoring one is chosen instead
// of the first encountered.
// replicas in any order, so we always scan all replicas before deciding — this
// ensures local MEMORY is always preferred over local NOF regardless of the
// order replicas appear in the list. When scoring is enabled and there are
// multiple remote MEMORY replicas, the best-scoring one is chosen instead of
// the first encountered.
inline const Replica::Descriptor *SelectBestReplica(
const std::vector<Replica::Descriptor> &replicas,
const std::unordered_set<std::string> &local_endpoints) {
const Replica::Descriptor *local_memory = nullptr;
const Replica::Descriptor *local_nof = nullptr;
const Replica::Descriptor *first_memory = nullptr;
const Replica::Descriptor *first_nof = nullptr;
for (const auto &r : replicas) {
Expand All @@ -128,18 +132,23 @@ inline const Replica::Descriptor *SelectBestReplica(
if (local_endpoints.count(
r.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_)) {
return &r; // local MEMORY — best case
if (!local_memory) local_memory = &r;
} else {
if (!first_memory) first_memory = &r;
}
if (!first_memory) first_memory = &r;
} else if (r.is_nof_replica()) {
if (local_endpoints.count(
r.get_nof_descriptor()
.buffer_descriptor.transport_endpoint_)) {
return &r; // local NOF_SSD — also good
if (!local_nof) local_nof = &r;
} else {
if (!first_nof) first_nof = &r;
}
if (!first_nof) first_nof = &r;
}
}
// Local MEMORY always beats local NOF regardless of list order.
if (local_memory) return local_memory;
if (local_nof) return local_nof;
// No local replica. Among remote MEMORY replicas, optionally pick the
// best-scoring one instead of the first encountered (issue #2516).
if (first_memory && RemoteReplicaScoringEnabled()) {
Expand Down
59 changes: 59 additions & 0 deletions mooncake-store/tests/replica_selection_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,65 @@ TEST_F(ReplicaSelectionTest, LocalMemoryAlwaysWins) {
"nodeB"); // locality beats protocol
}

// Regression for issue #3658: a complete local MEMORY replica must win over a
// complete local NOF replica regardless of the order they appear in the list.
// ObjectMetadata preserves insertion order, and promotion appends a new MEMORY
// replica to existing metadata, so a MEMORY replica may legally appear after a
// NOF replica. The session planner supports MEMORY and DFS, but not NOF, so
// selecting NOF here would regress a session that has a usable MEMORY replica.
TEST_F(ReplicaSelectionTest, LocalMemoryBeatsLocalNoFInBothOrders) {
std::unordered_set<std::string> local = {"memB", "nofB"};

// local NOF listed before local MEMORY
std::vector<Replica::Descriptor> nof_first = {
MakeNoF("nofB"),
MakeMemory("memB", "tcp"),
};
const auto* sel = SelectBestReplica(nof_first, local);
ASSERT_NE(sel, nullptr);
EXPECT_TRUE(sel->is_memory_replica());
EXPECT_EQ(
sel->get_memory_descriptor().buffer_descriptor.transport_endpoint_,
"memB");

// local MEMORY listed before local NOF (the previously-correct order)
std::vector<Replica::Descriptor> mem_first = {
MakeMemory("memB", "tcp"),
MakeNoF("nofB"),
};
sel = SelectBestReplica(mem_first, local);
ASSERT_NE(sel, nullptr);
EXPECT_TRUE(sel->is_memory_replica());
EXPECT_EQ(
sel->get_memory_descriptor().buffer_descriptor.transport_endpoint_,
"memB");
}

TEST_F(ReplicaSelectionTest, IncompleteLocalMemoryFallsBackToLocalNoF) {
// The local MEMORY replica is not readable yet, so the local NOF_SSD one
// remains the best choice.
std::unordered_set<std::string> local = {"nodeA"};
std::vector<Replica::Descriptor> reps = {
MakeNoF("nodeA"),
MakeMemory("nodeA", "rdma", ReplicaStatus::PROCESSING),
};
const auto* sel = SelectBestReplica(reps, local);
ASSERT_NE(sel, nullptr);
EXPECT_TRUE(sel->is_nof_replica());
}

TEST_F(ReplicaSelectionTest, LocalNoFPrecedesRemoteMemory) {
// Locality still outranks tier for remote MEMORY replicas.
std::unordered_set<std::string> local = {"nodeA"};
std::vector<Replica::Descriptor> reps = {
MakeNoF("nodeA"),
MakeMemory("nodeB", "rdma"),
};
const auto* sel = SelectBestReplica(reps, local);
ASSERT_NE(sel, nullptr);
EXPECT_TRUE(sel->is_nof_replica());
}

TEST_F(ReplicaSelectionTest, ScoringOffKeepsFirstRemoteMemory) {
// No scorer injected and env not set -> must return the FIRST remote
// MEMORY.
Expand Down
Loading