Skip to content
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

RCORE-1977 Empty reciprocal changesets lead to crashes or data divergence #7955

Merged
merged 4 commits into from
Aug 8, 2024

Conversation

danieltabacaru
Copy link
Collaborator

@danieltabacaru danieltabacaru commented Aug 6, 2024

What, How & Why?

This PR fixes two bugs related to storing and retrieving reciprocal changesets:

  1. Storing empty reciprocal changesets (after non-empty reciprocal changesets) in the realm file lead to BadChangeset exceptions or data divergence and consistency errors (RCORE-1977)
  2. Empty reciprocal changesets retrieved from internal cache lead to the same issues

Fixes #7893.

☑️ ToDos

  • 📝 Changelog update
  • 🚦 Tests (or not relevant)
  • [ ] C-API, if public C++ API changed
  • [ ] bindgen/spec.yml, if public C++ API changed

Copy link

coveralls-official bot commented Aug 6, 2024

Pull Request Test Coverage Report for Build daniel.tabacaru_884

Details

  • 286 of 289 (98.96%) changed or added relevant lines in 3 files are covered.
  • 107 unchanged lines in 16 files lost coverage.
  • Overall coverage increased (+0.01%) to 91.118%

Changes Missing Coverage Covered Lines Changed/Added Lines %
test/test_sync.cpp 281 284 98.94%
Files with Coverage Reduction New Missed Lines %
src/realm/dictionary.cpp 1 85.16%
src/realm/object-store/sync/sync_session.cpp 1 92.01%
src/realm/util/serializer.cpp 1 90.43%
src/realm/list.cpp 2 87.37%
src/realm/mixed.cpp 2 86.61%
src/realm/sync/network/http.hpp 2 82.76%
test/object-store/sync/flx_sync.cpp 2 98.35%
src/realm/sync/noinst/client_reset.cpp 3 94.5%
src/realm/sync/noinst/protocol_codec.hpp 3 76.27%
src/realm/query_expression.hpp 4 93.81%
Totals Coverage Status
Change from base Build 2543: 0.01%
Covered Lines: 217112
Relevant Lines: 238276

💛 - Coveralls

@danieltabacaru danieltabacaru marked this pull request as ready for review August 6, 2024 15:11
sync::parse_changeset(in, reciprocal_changeset); // Throws
}
// The only instruction in the reciprocal changeset was discarded during OT.
CHECK(reciprocal_changeset.empty());
Copy link
Contributor

Choose a reason for hiding this comment

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

the comment above says there should be two reciprocal changesets?

return ++timestamp;
});

auto latest_local_version = [&] {
Copy link
Contributor

Choose a reason for hiding this comment

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

it looks like this gets used by all three of these tests, maybe move it into its own static function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's not the exact setup for all three tests, but I can look into refactoring it (or perhaps split it into two part: schema creation and adding the initial data)

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, got it. I will leave whether to refactor this to your judgement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

decided it's not worth it. I'd've normally implemented this using sections. maybe we should switch to using catch as well.

auto transact = db->start_read();
history.integrate_server_changesets(progress, downloadable_bytes, server_changesets_encoded, version_info,
DownloadBatchState::SteadyState, *test_context.logger, transact);

Copy link
Contributor

Choose a reason for hiding this comment

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

should we check that the reciprocal changesets are what we expect here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we can do that

history.integrate_server_changesets(progress, downloadable_bytes, server_changesets_encoded, version_info,
DownloadBatchState::SteadyState, *test_context.logger, transact);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

same q here, do we need to check that the reciprocal changesets are what we expect?

@@ -2591,7 +2591,8 @@ Changeset& Transformer::get_reciprocal_transform(TransformHistory& history, file
version_type version, const HistoryEntry& history_entry)
{
auto& changeset = m_reciprocal_transform_cache[version]; // Throws
if (changeset.empty()) {
// There can be empty changesets in the cache, so check the version too.
if (changeset.empty() && changeset.version == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

remind me, how do you end up with a changeset in the cache where the version is zero?

Copy link
Collaborator Author

@danieltabacaru danieltabacaru Aug 6, 2024

Choose a reason for hiding this comment

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

the instruction above it inserts an empty changeset (with version zero) if the key is not found. subtle, but one the culprits. We used emplace() before, but it was changed/refactored with a bunch of other changes some time ago and so the bug was introduced.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind updating the comment to explain this? Thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

interesting. is there a reason why we can't use cache.insert right above rather than the subscript operator so we actually get the inserted flag rather than checking if the version is zero?

Copy link
Contributor

@michael-wb michael-wb left a comment

Choose a reason for hiding this comment

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

LGTM - I agree with Jonathan's update recommendations for the tests.

@@ -2591,7 +2591,8 @@ Changeset& Transformer::get_reciprocal_transform(TransformHistory& history, file
version_type version, const HistoryEntry& history_entry)
{
auto& changeset = m_reciprocal_transform_cache[version]; // Throws
if (changeset.empty()) {
// There can be empty changesets in the cache, so check the version too.
if (changeset.empty() && changeset.version == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind updating the comment to explain this? Thanks

@danieltabacaru danieltabacaru requested a review from jbreams August 8, 2024 16:14
@danieltabacaru danieltabacaru merged commit 13d8264 into master Aug 8, 2024
4 of 5 checks passed
@danieltabacaru danieltabacaru deleted the dt/fix_empty_reciprocal_changesets branch August 8, 2024 18:52
@github-actions github-actions bot mentioned this pull request Aug 9, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bad changeset due to untouched reciprocal history entry
3 participants