Skip to content

Commit 82eaa90

Browse files
authored
feat(hash): support HFE compaction filter (#3503)
Implement HFE-aware RocksDB compaction filtering so expired hash fields can be safely dropped, and whole hash metadata can be removed when all TTL fields are past the tracked upper bound. References the HFE proposal in #3432 and tracking issue #3436. Assisted-by: Codex/GPT5.5 xhigh
1 parent 5264d59 commit 82eaa90

2 files changed

Lines changed: 404 additions & 7 deletions

File tree

src/storage/compact_filter.cc

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,35 @@ namespace engine {
3636

3737
using rocksdb::Slice;
3838

39+
namespace {
40+
41+
bool IsExpiredHashFieldSubkey(const HashMetadata &metadata, Slice value, uint64_t now, bool *expired) {
42+
*expired = false;
43+
if (!metadata.IsFieldExpirationEncoding()) {
44+
return true;
45+
}
46+
47+
uint64_t expire = 0;
48+
auto s = metadata.DecodeSubkeyValue(&value, &expire);
49+
if (!s.ok()) {
50+
return false;
51+
}
52+
53+
*expired = expire != 0 && expire < now;
54+
return true;
55+
}
56+
57+
bool HasHashFieldExpirationCandidates(const HashMetadata &metadata) {
58+
return metadata.IsFieldExpirationEncoding() && metadata.size > metadata.persist;
59+
}
60+
61+
bool IsHashFieldExpirationMetadataExpired(const HashMetadata &metadata, uint64_t now) {
62+
return HasHashFieldExpirationCandidates(metadata) && metadata.persist == 0 && metadata.upper != 0 &&
63+
metadata.upper < now;
64+
}
65+
66+
} // namespace
67+
3968
bool MetadataFilter::Filter([[maybe_unused]] int level, const Slice &key, const Slice &value,
4069
[[maybe_unused]] std::string *new_value, [[maybe_unused]] bool *modified) const {
4170
Metadata metadata(kRedisNone, false);
@@ -45,9 +74,22 @@ bool MetadataFilter::Filter([[maybe_unused]] int level, const Slice &key, const
4574
WARN("[compact_filter/metadata] Failed to decode, namespace: {}, key: {}, err: {}", ns, user_key, s.ToString());
4675
return false;
4776
}
77+
78+
bool expired = metadata.Expired();
79+
if (!expired && metadata.Type() == kRedisHash) {
80+
HashMetadata hash_metadata(false);
81+
Slice input(value);
82+
if (s = hash_metadata.Decode(&input); !s.ok()) {
83+
WARN("[compact_filter/metadata] Failed to decode hash metadata, namespace: {}, key: {}, err: {}", ns, user_key,
84+
s.ToString());
85+
return false;
86+
}
87+
expired = IsHashFieldExpirationMetadataExpired(hash_metadata, util::GetTimeStampMS());
88+
}
89+
4890
DEBUG("[compact_filter/metadata] namespace: {}, key: {}, result: {}", ns, user_key,
49-
(metadata.Expired() ? "deleted" : "reserved"));
50-
return metadata.Expired();
91+
(expired ? "deleted" : "reserved"));
92+
return expired;
5193
}
5294

5395
Status SubKeyFilter::GetMetadata(const InternalKey &ikey, Metadata *metadata) const {
@@ -114,6 +156,26 @@ rocksdb::CompactionFilter::Decision SubKeyFilter::FilterBlobByKey([[maybe_unused
114156
if (metadata.Type() == kRedisBitmap || metadata.Type() == kRedisTimeSeries) {
115157
return rocksdb::CompactionFilter::Decision::kUndetermined;
116158
}
159+
if (metadata.Type() == kRedisHash) {
160+
if (IsMetadataExpired(ikey, metadata)) {
161+
return rocksdb::CompactionFilter::Decision::kRemove;
162+
}
163+
164+
HashMetadata hash_metadata(false);
165+
Slice input(cached_metadata_);
166+
if (auto s = hash_metadata.Decode(&input); !s.ok()) {
167+
ERROR("[compact_filter/subkey] Failed to decode hash metadata, namespace: {}, key: {}, err: {}",
168+
ikey.GetNamespace(), ikey.GetKey(), s.ToString());
169+
return rocksdb::CompactionFilter::Decision::kKeep;
170+
}
171+
if (IsHashFieldExpirationMetadataExpired(hash_metadata, util::GetTimeStampMS())) {
172+
return rocksdb::CompactionFilter::Decision::kRemove;
173+
}
174+
if (HasHashFieldExpirationCandidates(hash_metadata)) {
175+
return rocksdb::CompactionFilter::Decision::kUndetermined;
176+
}
177+
return rocksdb::CompactionFilter::Decision::kKeep;
178+
}
117179

118180
bool result = IsMetadataExpired(ikey, metadata);
119181
return result ? rocksdb::CompactionFilter::Decision::kRemove : rocksdb::CompactionFilter::Decision::kKeep;
@@ -154,7 +216,38 @@ bool SubKeyFilter::Filter([[maybe_unused]] int level, const Slice &key, const Sl
154216
return expired;
155217
}
156218

157-
return IsMetadataExpired(ikey, metadata) || (metadata.Type() == kRedisBitmap && redis::Bitmap::IsEmptySegment(value));
219+
bool metadata_expired = IsMetadataExpired(ikey, metadata);
220+
if (metadata_expired) {
221+
return true;
222+
}
223+
224+
if (metadata.Type() == kRedisHash) {
225+
HashMetadata hash_metadata(false);
226+
Slice input(cached_metadata_);
227+
auto s = hash_metadata.Decode(&input);
228+
if (!s.ok()) {
229+
ERROR("[compact_filter/subkey] Failed to decode hash metadata, namespace: {}, key: {}, err: {}",
230+
ikey.GetNamespace(), ikey.GetKey(), s.ToString());
231+
return false;
232+
}
233+
auto now = util::GetTimeStampMS();
234+
if (IsHashFieldExpirationMetadataExpired(hash_metadata, now)) {
235+
return true;
236+
}
237+
if (!HasHashFieldExpirationCandidates(hash_metadata)) {
238+
return false;
239+
}
240+
241+
bool field_expired = false;
242+
if (!IsExpiredHashFieldSubkey(hash_metadata, value, now, &field_expired)) {
243+
ERROR("[compact_filter/subkey] Failed to decode hash subkey value, namespace: {}, key: {}", ikey.GetNamespace(),
244+
ikey.GetKey());
245+
return false;
246+
}
247+
return field_expired;
248+
}
249+
250+
return metadata.Type() == kRedisBitmap && redis::Bitmap::IsEmptySegment(value);
158251
}
159252

160253
bool SearchFilter::Filter([[maybe_unused]] int level, const Slice &key, [[maybe_unused]] const Slice &value,

0 commit comments

Comments
 (0)