Skip to content

Conversation

@allenss-amazon
Copy link
Member

@allenss-amazon allenss-amazon commented Oct 1, 2025

Revises the save/restore logic to support saving of all index data and pending mutation queue as discussed earlier.

This new logic is fully backward and forward compatible. RDB files written using the previous binaries (aka V1) will still be loaded and subject to the same stale data correction and forced backfill logic as existed before this PR. In other words, for RDB files written with the old code, the new code handles them exactly the same.

New format files (aka V2) contain additional data that will be ignored by old code. In other words, V2 RDB files can still be loaded into prior code which will ignore the extra data, i.e., they are treated like V1 files and be subjected to stale data correction and forced backfill.

V2 files which are loaded into the new code are not subjected to stale data correction and forced backfill. Once a V2 file is loaded it can "open for business" immediately.

The V2 format contains an additional section for the contents of non-vector indexes (Tags, Numeric). These sections contain the key and attribute-value for each entry in the index. This external format differs from the internal format, allowing the internal data structures to be freely optimized in the future.

The V2 format also has a section for the mutation queue and index backfill status. This additional section records the keys that were in the mutation queue at the time of the save as well as the index backfill status (complete or incomplete). This allows the recreation of the mutation queue and backfill status (backfill needed or not) to match the state at the time of the save. (One small quirk is that the saved mutation queue doesn't bother to record whether the saved keys are in the mutation queue due to a backfill or not. This behavior should not affect the client-visible semantics, but would affect internal counters for the ingestion pipeline as they would be mislead by the source of the mutation.

Two controls are provided to override the normal behavior of this code.

The configurable: search.rdb_write_v2 (default "yes") controls whether a generated RDB file is in the V1 or V2 format.

The configurable: search.rdb_read_v2 (default "yes") controls whether the V2 data is used. Setting this to "no" will force the code to treat a V2 file as a V1 file, i.e., to ignore the extra V1 data.

FIxes #41

@allenss-amazon allenss-amazon changed the title Initial wiring Revise Save/Restore for true pit snapshot. Oct 4, 2025
@allenss-amazon allenss-amazon marked this pull request as ready for review October 4, 2025 18:50
absl::Status SaveString(const std::string_view s) {
return SaveChunk(s.data(), s.size());
}
template <typename T, std::enable_if_t<std::is_trivial<T>::value &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need enable_if_t ? I saw it limits this function only to certain types. What types does it limit the template to. In future are we expected to serialize any complex information ?

Copy link
Member Author

Choose a reason for hiding this comment

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

This implementation won't handle complex information. If the need for a more sophisticated serialization framework comes up we'll tackle it then. The enable_if_t is seat belts for developers.

: vmsdk::ThreadPool(name, num_threads) {
ON_CALL(*this, Schedule(testing::_, testing::_))
.WillByDefault(testing::Invoke(
.WillByDefault(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I had seen this was causing build warnings , glad this is being fixed, what was the actual issue?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure why this wasn't caught earlier. I believe that testing::Invoke predates lambda functions and isn't needed for them.

<< tracked_mutated_records_.size();
VMSDK_RETURN_IF_ERROR(out.SaveObject(tracked_mutated_records_.size()));
for (const auto &[key, value] : tracked_mutated_records_) {
VMSDK_RETURN_IF_ERROR(out.SaveString(key->Str()));
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't save the value of records in the mutation queue?

Copy link
Member Author

Choose a reason for hiding this comment

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

The values are not needed because they are "by definition" duplicates of what is in the main database.

//
if (tracked_keys_.contains(key_ptr)) {
DCHECK(false);
return absl::InternalError("Numeric field save duplicate key");
Copy link
Collaborator

Choose a reason for hiding this comment

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

what happens when tracked/ untracked keys overlap between loading different indexes. I assume it would be handled automatically while backfilling?

Copy link
Member Author

Choose a reason for hiding this comment

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

In theory tracked/untracked are disjoint sets. In the save/restore world it's better to crash the save than the restore. In other words, let's not write an invalid RDB file.

import threading
from ft_info_parser import FTInfoParser

index = Index("index", [Vector("v", 3, type="HNSW", m=2, efc=1), Numeric("n"), Tag("t")])
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe we could add a flat vector field as well just to make testing more extensive. also maybe adding some deletes, updates and then saving restoring would also remove any doubts

Copy link
Member Author

Choose a reason for hiding this comment

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

The PR doesn't change the save/restore of a vector index. It does suppress the post load scan for stale keys. But this logic isn't affected by HNSW vs FLAT.

supplemental_content->mutation_queue_header().backfilling();
if (!backfilling) {
VMSDK_LOG(DEBUG, ctx) << "Backfill suppressed.";
index_schema->backfill_job_.Get() = std::nullopt;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I imagine some consistency check here, but i guess that is a separate task

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, in a perfect world we'd have additional checking code that could be enabled/disabled.

Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
Signed-off-by: Allen Samuels <[email protected]>
absl::StatusOr<T> LoadObject() {
VMSDK_ASSIGN_OR_RETURN(auto buffer, LoadChunk());
if (buffer->size() != sizeof(T)) {
return absl::InternalError("Mismatched size protocol error");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider asserting using CHECK or DCHECK

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think crashing is the correct response to an invalid RDB file.

auto idx = attr.GetIndex();
size_t cnt = idx->GetTrackedKeyCount() + idx->GetUnTrackedKeyCount();
if (cnt != oracle_key_count) {
if (IsVectorIndex(idx) && cnt < oracle_key_count) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't this be:

if ((IsVectorIndex(idx) && cnt <= oracle_key_count) || (!IsVectorIndex(idx) && cnt == oracle_key_count)) {
  continue;

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, they are equivalent. How about this?

    if (IsVectorIndex(idx) ? cnt <= oracle_key_count
                           : cnt == oracle_key_count) {
      continue;
    }

if (index_schema) {
VMSDK_RETURN_IF_ERROR(index_schema->LoadIndexExtension(
ctx, RDBChunkInputStream(supplemental_iter.IterateChunks())));
bool backfilling =
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: there is no point in defining a variable if used only once.

Copy link
Member Author

Choose a reason for hiding this comment

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

Which variable? backfilling is used in the next line.

break;
}
}
size_t oracle_key_count =
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't we break if non-vector index if found?

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't that exactly what this does?

}
auto status2 = larger_index->ForEachUnTrackedKey(key_check);
if (!status2.ok()) {
status = status1;
Copy link
Collaborator

Choose a reason for hiding this comment

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

should this be:
status = status2; ?

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch.

//
// Need to find an attribute index that has the right tracked/untracked
// keys. Any non-vector index will do. But it there are only vector
// indexes we will use that.
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is the value of doing so if there are no non-vector indexes?

Copy link
Member Author

@allenss-amazon allenss-amazon Oct 29, 2025

Choose a reason for hiding this comment

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

Hmm, good point. Worse, the list will be wrong if there are multiple vector indexes across disjoint sets of keys. I think you're correct that it can be skipped.

@yairgott
Copy link
Collaborator

I might had missed it, but I haven't seen that the queued multi/exec are being serialized as well.

@allenss-amazon
Copy link
Member Author

I might had missed it, but I haven't seen that the queued multi/exec are being serialized as well.

Good catch.

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.

Avoid Backfill After RDB is Loaded

3 participants