-
Notifications
You must be signed in to change notification settings - Fork 723
[Store] Add hard pin mechanism for eviction-protected objects #1728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
152fcf4
d50b37b
724af76
0bdc2a7
6acbd56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -766,7 +766,7 @@ auto MasterService::PutStart(const UUID& client_id, const std::string& key, | |||||||||||||
| shard->metadata.emplace( | ||||||||||||||
| std::piecewise_construct, std::forward_as_tuple(key), | ||||||||||||||
| std::forward_as_tuple(client_id, now, total_length, std::move(replicas), | ||||||||||||||
| config.with_soft_pin)); | ||||||||||||||
| config.with_soft_pin, config.with_hard_pin)); | ||||||||||||||
| // Also insert the metadata into processing set for monitoring. | ||||||||||||||
| shard->processing_keys.insert(key); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -2886,6 +2886,10 @@ void MasterService::BatchEvict(double evict_ratio_target, | |||||||||||||
| candidates; // can be removed | ||||||||||||||
| for (auto it = shard->metadata.begin(); it != shard->metadata.end(); | ||||||||||||||
| it++) { | ||||||||||||||
| // Hard-pinned objects are never evicted | ||||||||||||||
| if (it->second.IsHardPinned()) { | ||||||||||||||
| continue; | ||||||||||||||
| } | ||||||||||||||
| // Skip objects that are not expired or have incomplete replicas | ||||||||||||||
| if (!it->second.IsLeaseExpired(now) || | ||||||||||||||
| !can_evict_replicas(it->second)) { | ||||||||||||||
|
|
@@ -2920,7 +2924,8 @@ void MasterService::BatchEvict(double evict_ratio_target, | |||||||||||||
| while (it != shard->metadata.end()) { | ||||||||||||||
| // Skip objects that are not allowed to be evicted in the first | ||||||||||||||
| // pass | ||||||||||||||
| if (!it->second.IsLeaseExpired(now) || | ||||||||||||||
| if (it->second.IsHardPinned() || | ||||||||||||||
| !it->second.IsLeaseExpired(now) || | ||||||||||||||
| it->second.IsSoftPinned(now) || | ||||||||||||||
| !can_evict_replicas(it->second)) { | ||||||||||||||
| ++it; | ||||||||||||||
|
|
@@ -2983,7 +2988,8 @@ void MasterService::BatchEvict(double evict_ratio_target, | |||||||||||||
| (start_idx + i) % kNumShards); | ||||||||||||||
| auto it = shard->metadata.begin(); | ||||||||||||||
| while (it != shard->metadata.end() && target_evict_num > 0) { | ||||||||||||||
| if (it->second.lease_timeout <= target_timeout && | ||||||||||||||
| if (!it->second.IsHardPinned() && | ||||||||||||||
| it->second.lease_timeout <= target_timeout && | ||||||||||||||
| !it->second.IsSoftPinned(now) && | ||||||||||||||
| can_evict_replicas(it->second)) { | ||||||||||||||
| // Evict this object | ||||||||||||||
|
|
@@ -3025,9 +3031,9 @@ void MasterService::BatchEvict(double evict_ratio_target, | |||||||||||||
|
|
||||||||||||||
| auto it = shard->metadata.begin(); | ||||||||||||||
| while (it != shard->metadata.end() && target_evict_num > 0) { | ||||||||||||||
| // Skip objects that are not expired or have incomplete | ||||||||||||||
| // replicas | ||||||||||||||
| if (!it->second.IsLeaseExpired(now) || | ||||||||||||||
| // Skip hard-pinned or not-yet-expired objects | ||||||||||||||
| if (it->second.IsHardPinned() || | ||||||||||||||
| !it->second.IsLeaseExpired(now) || | ||||||||||||||
| !can_evict_replicas(it->second)) { | ||||||||||||||
| ++it; | ||||||||||||||
| continue; | ||||||||||||||
|
|
@@ -3500,10 +3506,12 @@ MasterService::MetadataSerializer::SerializeMetadata( | |||||||||||||
| MsgpackPacker& packer) const { | ||||||||||||||
| // Pack ObjectMetadata using array structure for efficiency | ||||||||||||||
| // Format: [client_id, put_start_time, size, lease_timeout, | ||||||||||||||
| // has_soft_pin_timeout, soft_pin_timeout, replicas_count, replicas...] | ||||||||||||||
| // has_soft_pin_timeout, soft_pin_timeout, replicas_count, replicas..., | ||||||||||||||
| // hard_pinned] | ||||||||||||||
|
|
||||||||||||||
| size_t array_size = 7; // size, lease_timeout, has_soft_pin_timeout, | ||||||||||||||
| // soft_pin_timeout, replicas_count | ||||||||||||||
| size_t array_size = 8; // client_id, put_start_time, size, lease_timeout, | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we avoid hardcoding here? Use |
||||||||||||||
| // has_soft_pin_timeout, soft_pin_timeout, | ||||||||||||||
| // replicas_count + hard_pinned | ||||||||||||||
|
Comment on lines
+3513
to
+3515
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment for
Suggested change
|
||||||||||||||
| array_size += metadata.CountReplicas(); // One element per replica | ||||||||||||||
| packer.pack_array(array_size); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -3552,6 +3560,9 @@ MasterService::MetadataSerializer::SerializeMetadata( | |||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Serialize hard_pinned flag (appended for backward compatibility) | ||||||||||||||
| packer.pack(metadata.hard_pinned); | ||||||||||||||
|
||||||||||||||
| packer.pack(metadata.hard_pinned); | |
| packer.pack(metadata.IsHardPinned()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider this comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use IsHardPinned instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the comment, and switched to IsHardPinned() as you suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done — also caught another direct access in DeserializeShard (L3495) and fixed that too.
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DeserializeMetadata() correctly parses the optional hard_pinned flag and passes it into the temporary ObjectMetadata instance, but the restore path in DeserializeShard() reconstructs shard metadata by emplacing a new ObjectMetadata from metadata_ptr without forwarding hard_pinned, so hard-pinned objects will come back as not hard pinned after restoring a snapshot. Please ensure the shard-level reconstruction preserves hard_pinned (either pass it into the constructor or set the field under lock after emplace).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4272,6 +4272,170 @@ TEST_F(MasterServiceTest, ForceRemoveAllLeasedObjects) { | |
| ASSERT_FALSE(exist_result.value()); | ||
| } | ||
| } | ||
| TEST_F(MasterServiceTest, HardPinObjectNotEvicted) { | ||
| // Hard-pinned objects must survive eviction under memory pressure, | ||
| // even after lease expires and all non-pinned objects are gone. | ||
| const uint64_t kv_lease_ttl = 200; | ||
| auto service_config = MasterServiceConfig::builder() | ||
| .set_default_kv_lease_ttl(kv_lease_ttl) | ||
| .build(); | ||
| std::unique_ptr<MasterService> service_(new MasterService(service_config)); | ||
| const UUID client_id = generate_uuid(); | ||
|
|
||
| constexpr size_t buffer = 0x300000000; | ||
| constexpr size_t segment_size = 1024 * 1024 * 16; | ||
| constexpr size_t value_size = 1024 * 1024; | ||
| [[maybe_unused]] const auto context = | ||
| PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size); | ||
|
|
||
| // Put a hard-pinned object | ||
| { | ||
| ReplicateConfig config; | ||
| config.replica_num = 1; | ||
| config.with_hard_pin = true; | ||
| auto result = | ||
| service_->PutStart(client_id, "pinned_model", value_size, config); | ||
| ASSERT_TRUE(result.has_value()); | ||
| ASSERT_TRUE( | ||
| service_->PutEnd(client_id, "pinned_model", ReplicaType::MEMORY) | ||
| .has_value()); | ||
| } | ||
|
|
||
| // Fill remaining space with normal objects to trigger eviction | ||
| for (int i = 0; i < 20; i++) { | ||
| std::string key = "filler_" + std::to_string(i); | ||
| ReplicateConfig config; | ||
| config.replica_num = 1; | ||
| auto result = service_->PutStart(client_id, key, value_size, config); | ||
| if (result.has_value()) { | ||
| service_->PutEnd(client_id, key, ReplicaType::MEMORY); | ||
| } | ||
| } | ||
|
|
||
| // Wait for leases to expire and eviction to kick in | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 500)); | ||
|
|
||
| // Hard-pinned object must still be there | ||
| auto get_result = service_->GetReplicaList("pinned_model"); | ||
| ASSERT_TRUE(get_result.has_value()) | ||
| << "Hard-pinned object was evicted, but it should never be"; | ||
|
|
||
|
Comment on lines
+4275
to
+4322
|
||
| // Explicit Remove should still work on hard-pinned objects | ||
| auto remove_result = service_->Remove("pinned_model"); | ||
| ASSERT_TRUE(remove_result.has_value()); | ||
| auto exist_result = service_->ExistKey("pinned_model"); | ||
| ASSERT_TRUE(exist_result.has_value()); | ||
| ASSERT_FALSE(exist_result.value()); | ||
|
|
||
| service_->RemoveAll(); | ||
| } | ||
|
|
||
| TEST_F(MasterServiceTest, HardPinWithSoftPinEvictionOrder) { | ||
| // Verify eviction priority: non-pinned first, then soft-pinned, | ||
| // and hard-pinned objects are never evicted even under extreme pressure. | ||
| const uint64_t kv_lease_ttl = 200; | ||
| const uint64_t kv_soft_pin_ttl = 10000; | ||
| const bool allow_evict_soft_pinned_objects = true; | ||
| auto service_config = MasterServiceConfig::builder() | ||
| .set_default_kv_lease_ttl(kv_lease_ttl) | ||
| .set_default_kv_soft_pin_ttl(kv_soft_pin_ttl) | ||
| .set_allow_evict_soft_pinned_objects( | ||
| allow_evict_soft_pinned_objects) | ||
| .set_eviction_ratio(0.5) | ||
| .build(); | ||
| std::unique_ptr<MasterService> service_(new MasterService(service_config)); | ||
| const UUID client_id = generate_uuid(); | ||
|
|
||
| constexpr size_t buffer = 0x300000000; | ||
| constexpr size_t segment_size = 1024 * 1024 * 16; | ||
| constexpr size_t value_size = 1024 * 1024; | ||
| [[maybe_unused]] const auto context = | ||
| PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size); | ||
|
|
||
| // Put a hard-pinned object | ||
| { | ||
| ReplicateConfig config; | ||
| config.replica_num = 1; | ||
| config.with_hard_pin = true; | ||
| ASSERT_TRUE( | ||
| service_->PutStart(client_id, "hard_pinned", value_size, config) | ||
| .has_value()); | ||
| ASSERT_TRUE( | ||
| service_->PutEnd(client_id, "hard_pinned", ReplicaType::MEMORY) | ||
| .has_value()); | ||
| } | ||
|
|
||
| // Put a soft-pinned object | ||
| { | ||
| ReplicateConfig config; | ||
| config.replica_num = 1; | ||
| config.with_soft_pin = true; | ||
| ASSERT_TRUE( | ||
| service_->PutStart(client_id, "soft_pinned", value_size, config) | ||
| .has_value()); | ||
| ASSERT_TRUE( | ||
| service_->PutEnd(client_id, "soft_pinned", ReplicaType::MEMORY) | ||
| .has_value()); | ||
| } | ||
|
|
||
| // Fill the rest | ||
| for (int i = 0; i < 20; i++) { | ||
| std::string key = "normal_" + std::to_string(i); | ||
| ReplicateConfig config; | ||
| config.replica_num = 1; | ||
| auto result = service_->PutStart(client_id, key, value_size, config); | ||
| if (result.has_value()) { | ||
| service_->PutEnd(client_id, key, ReplicaType::MEMORY); | ||
| } | ||
| } | ||
|
|
||
| // Let leases expire, trigger eviction | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 500)); | ||
|
|
||
| // Hard-pinned always survives | ||
| ASSERT_TRUE(service_->GetReplicaList("hard_pinned").has_value()) | ||
| << "Hard-pinned object was evicted"; | ||
|
|
||
| std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl)); | ||
|
Comment on lines
+4333
to
+4399
|
||
| service_->RemoveAll(); | ||
| } | ||
|
|
||
| TEST_F(MasterServiceTest, HardPinDefaultIsFalse) { | ||
| // Objects created without with_hard_pin should not be hard-pinned | ||
| auto service_config = | ||
| MasterServiceConfig::builder().set_default_kv_lease_ttl(5000).build(); | ||
| std::unique_ptr<MasterService> service_(new MasterService(service_config)); | ||
| const UUID client_id = generate_uuid(); | ||
|
|
||
| constexpr size_t buffer = 0x300000000; | ||
| constexpr size_t segment_size = 1024 * 1024 * 16; | ||
| [[maybe_unused]] const auto context = | ||
| PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size); | ||
|
|
||
| // Put without hard_pin (default) | ||
| ReplicateConfig config; | ||
| config.replica_num = 1; | ||
| ASSERT_TRUE( | ||
| service_->PutStart(client_id, "normal_key", 1024, config).has_value()); | ||
| ASSERT_TRUE(service_->PutEnd(client_id, "normal_key", ReplicaType::MEMORY) | ||
| .has_value()); | ||
|
|
||
| // Put with hard_pin | ||
| ReplicateConfig hp_config; | ||
| hp_config.replica_num = 1; | ||
| hp_config.with_hard_pin = true; | ||
| ASSERT_TRUE( | ||
| service_->PutStart(client_id, "hp_key", 1024, hp_config).has_value()); | ||
| ASSERT_TRUE( | ||
| service_->PutEnd(client_id, "hp_key", ReplicaType::MEMORY).has_value()); | ||
|
|
||
| // Both should exist | ||
| ASSERT_TRUE(service_->GetReplicaList("normal_key").has_value()); | ||
| ASSERT_TRUE(service_->GetReplicaList("hp_key").has_value()); | ||
|
|
||
| service_->RemoveAll(); | ||
| } | ||
|
|
||
| } // namespace mooncake::test | ||
|
|
||
| int main(int argc, char** argv) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hard_pinnedis set only at creation time and never mutated elsewhere (no assignments beyond the constructor). Keeping itGUARDED_BY(lock)forcesIsHardPinned()to take the spinlock on every eviction scan. Consider makinghard_pinnedimmutable (e.g.,const bool) and not guarded, so eviction and serialization can read it without lock overhead and without thread-safety annotation violations.