Skip to content

Conversation

@glevkovich
Copy link
Contributor

Previously, HEXPIRE could leave an empty hash in the database if all fields were expired immediately. This "zombie" key caused the RDB serializer to crash during SAVE/BGSAVE operations.

This change detects when the hash becomes empty inside OpHExpire and removes the key using DelMutable. DelMutable is required here to transfer ownership of the iterator and safely handle the AutoUpdater lifecycle.

Copilot AI review requested due to automatic review settings January 29, 2026 15:13
Previously, HEXPIRE could leave an empty hash in the database if all
fields were expired immediately. This "zombie" key caused the RDB
serializer to crash during SAVE/BGSAVE operations.

This change detects when the hash becomes empty inside OpHExpire and
removes the key using DelMutable. DelMutable is required here to
transfer ownership of the iterator and safely handle the AutoUpdater
lifecycle.

Signed-off-by: Gil Levkovich <[email protected]>
@glevkovich glevkovich force-pushed the glevkovich/fix_save_entry_fatal_error_on_empty_hash branch from f27908a to 6e92baf Compare January 29, 2026 15:16
@augmentcode
Copy link

augmentcode bot commented Jan 29, 2026

🤖 Augment PR Summary

Summary: Prevents HEXPIRE from leaving an empty ("zombie") hash key behind, which could crash RDB SAVE/BGSAVE.
Changes: Deletes the hash key via DbSlice::DelMutable when the last field is expired with TTL=0, and adds a regression test covering EXISTS and SAVE behavior.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a bug where HEXPIRE with an immediate expiration could leave an empty (“zombie”) hash key behind, which could crash RDB serialization during SAVE/BGSAVE.

Changes:

  • Update OpHExpire to delete the hash key when expiring fields makes it empty (using DbSlice::DelMutable).
  • Add a regression test covering HEXPIRE ... 0 ... ensuring the key is removed and an RDB save succeeds.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/server/hset_family.cc Deletes the hash key when HEXPIRE removes the last remaining field, preventing empty-hash serialization crashes.
src/server/hset_family_test.cc Adds a test verifying HEXPIRE with TTL 0 deletes the key and that SAVE RDB succeeds.

Comment on lines +755 to +756
// if the hash becomes empty. If the key remains (zombie), saving the RDB will crash
// and EXISTS
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is incomplete/unclear (line ends with “and EXISTS”). Please reword or remove this fragment so the intent is unambiguous.

Suggested change
// if the hash becomes empty. If the key remains (zombie), saving the RDB will crash
// and EXISTS
// if the hash becomes empty. If the key remains (zombie), saving the RDB or running
// commands like EXISTS against it may lead to crashes or other incorrect behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +545 to +552
// If it is a hash which became empty after expiring fields, we must delete the key safely.
// We use DelMutable which consumes the iterator/updater to prevent the crash.
if (pv->Encoding() == kEncodingStrMap2) {
auto* sm = static_cast<StringMap*>(pv->RObjPtr());
if (sm->UpperBoundSize() == 0) {
db_slice.DelMutable(op_args.db_cntx, std::move(*op_res));
}
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation in this newly added block doesn’t match the surrounding style in this file (2-space indent). Please reformat this block (e.g., clang-format) so the inner statements align consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +758 to +763
Run({"HSET", "zombie", "f", "v"});
auto resp = Run({"HEXPIRE", "zombie", "0", "FIELDS", "1", "f"});
EXPECT_THAT(resp, IntArg(2));
EXPECT_EQ(0, CheckedInt({"EXISTS", "zombie"}));
EXPECT_EQ(Run({"SAVE", "RDB", "zombie_test.rdb"}), "OK");
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test writes an RDB snapshot to a hard-coded filename ("zombie_test.rdb"). In this fixture, FLAGS_dbfilename is empty and BaseFamilyTest::CleanupSnapshots() won’t remove this file, so it can leak artifacts into the build/test working directory and cause conflicts across runs. Consider calling InitWithDbFilename() and using SAVE RDB without an explicit basename (or use a basename that starts with the configured dbfilename), or explicitly delete the file at the end of the test.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@romange romange left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm but please address the comments

auto resp = Run({"HEXPIRE", "zombie", "0", "FIELDS", "1", "f"});
EXPECT_THAT(resp, IntArg(2));
EXPECT_EQ(0, CheckedInt({"EXISTS", "zombie"}));
EXPECT_EQ(Run({"SAVE", "RDB", "zombie_test.rdb"}), "OK");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save is unnecessary imho

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants