Skip to content

Conversation

abhijat
Copy link
Contributor

@abhijat abhijat commented Apr 1, 2025

When a search operation is performed on a hash set, expired fields are removed as a side effect.
If at the end of such an operation the hash set becomes empty, its key is removed from the database.

fixes #4856

@abhijat abhijat changed the title fix(hset_family): Ensure empty hash sets are removed [wip] fix(hset_family): Ensure empty hash sets are removed Apr 1, 2025
@abhijat abhijat force-pushed the abhijat/fix/delete-empty-hashset branch 12 times, most recently from f72ff33 to 1b3f3cd Compare April 2, 2025 07:12
@abhijat abhijat force-pushed the abhijat/fix/delete-empty-hashset branch 3 times, most recently from e860e4e to 8b3c8e7 Compare April 3, 2025 06:06
@abhijat abhijat requested a review from adiholden April 3, 2025 09:18
@abhijat abhijat changed the title [wip] fix(hset_family): Ensure empty hash sets are removed fix(hset_family): Ensure empty hash sets are removed Apr 3, 2025
}

// If the set is empty, FindWithCleanup will have removed the key
if (sm->Empty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If FindWithCleanup removed the key, is sm still valid or was it deallocated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, oddly enough in the debugger the pointer is still valid although its size is 0 and entries_ table is empty although the block of memory seems to have been freed. It doesn't seem safe though.

I will look into this further.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think in tests the pointer is valid because of UB and the OS just hasn't reused the memory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the latest version of the code, the map is not accessed after delete because delete is performed during scope exit of the function.


bool ContainsWithCleanup(HSetCleanupCtx ctx, const std::string_view field) {
const bool found = ctx.sm->Contains(field);
ctx.DelKeyIfEmpty();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a blocking function as RecordJournal can preempt

  1. Lets rename this function to have suffix Blocking
  2. If the key was deleted we should not access sm anymore

Copy link
Contributor

Choose a reason for hiding this comment

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

if I am not wrong with number 2, we need to find a way to enforce that callers will not access sm after calling this functions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now we set sm to nullptr if the key is deleted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another approach could be to consume the input pointer (by always setting to null) and return the string map pointer wrapped in a type such as variant or optional to signify that the string map is no longer valid, but nothing guarantees that the caller will not attempt to use the returned pointer without a check.

When a search operation is performed on a hash set, expired fields are
removed as a side effect.

If at the end of such an operation the hash set becomes empty, its key
is removed from the database.

Signed-off-by: Abhijat Malviya <[email protected]>
@abhijat abhijat force-pushed the abhijat/fix/delete-empty-hashset branch from 8b3c8e7 to c91cea7 Compare April 3, 2025 13:04
return OpStatus::OK;
}

struct HSetCleanupCtx {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this code needs more improvement.

  1. We pass too many variables into struct, some of them can be deduced from others
  2. The interface a bit awkward - why sm_ptr is double pointer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a double pointer to signal the user that the pointer may be invalidated as discussed in #4873 (comment)

If the key is deleted, then we set the pointer to null. As mentioned in #4873 (comment) though, it is tricky to avoid accidental usage.


if (it == sm->end())
return OpStatus::KEY_NOTFOUND;
if (const auto it = FindWithCleanupBlocking(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I appreciate the attempt to wrap everything into a single call but I think it makes things more confusing on the caller side.
If we found something - we for sure do not need to clean up.
if not - we should check if sm->Empty() and perform the deletion. So the deletion - can be a separate function
with its arguments but pushing this code down into FindWithCleanupBlocking makes the code here more confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially in this PR I had the approach to do an empty check + delete using RAII as in f72ff33 - but we discussed later and to make sure the key is always deleted for future commands I combined the check and deletion into one.

I will look into changing to make things simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to simplify it a bit, it can be made simpler by arming the delete object by hand but that leaves the possibility of new code not ensuring delete of empty keys cc @adiholden

@abhijat abhijat force-pushed the abhijat/fix/delete-empty-hashset branch 4 times, most recently from 1d93270 to b418619 Compare April 4, 2025 11:50
Signed-off-by: Abhijat Malviya <[email protected]>
@abhijat abhijat force-pushed the abhijat/fix/delete-empty-hashset branch from b418619 to 5381f48 Compare April 4, 2025 11:51
@abhijat abhijat requested review from romange and adiholden April 6, 2025 12:37

struct KeyCleanup {
using CleanupFuncT = std::function<void(std::string_view)>;
explicit KeyCleanup(CleanupFuncT func, const std::string_view key_view)
Copy link
Contributor

Choose a reason for hiding this comment

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

In general, string_view is not mutable so const is redundant here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I try to use const with views when I remember because even string_view can be made to point to some other string in the function body and const protects against that.

It's probably not something that happens often so I am fine to remove it, but I used const deliberately here.

Copy link
Contributor

Choose a reason for hiding this comment

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

ohhh yeah that makes sense! nvm

bool armed{false};
};

void DeleteKey(DbSlice& db_slice, const OpArgs& op_args, std::string_view key) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can move this function inside KeyCleanup. You only call it once as "freestanding" and I think we might even have a bug there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept it as a function because it is used twice.

auto it = db_slice.FindMutable(op_args.db_cntx, key).it;

db_slice.Del(op_args.db_cntx, it);
DeleteKey(db_slice, op_args, key);
Copy link
Contributor

Choose a reason for hiding this comment

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

DeleteKey also writes to the journal right ? Why didn't we do that before ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure but earlier in the PR discussion it was mentioned we should write to journal when we delete the key. Should we not do this in this case?

This is in the HGetGeneric -> OpGetAll code path. Will it be replicated by itself, considering it is a read command?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure but earlier in the PR discussion it was mentioned we should write to journal when we delete the key. Should we not do this in this case?

Yes we should but I am skeptical before those changes it seems that we did not write explicitly and we do now.

I will need to check at the flow and get back to you

Copy link
Contributor

Choose a reason for hiding this comment

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

Wow that was an actual bug. Mind if we also add a replication test for this ?

Copy link
Contributor

@kostasrim kostasrim left a comment

Choose a reason for hiding this comment

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

lgtm. Plz follow up with:

  1. Change the log for snapshot as per Adi's comment

@abhijat abhijat merged commit c129834 into main Apr 9, 2025
10 checks passed
@abhijat abhijat deleted the abhijat/fix/delete-empty-hashset branch April 9, 2025 06:55
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.

Save empty key fatal error on empty hash
4 participants