Skip to content

History schema evolution#5470

Merged
bendk merged 4 commits into
mozilla:mainfrom
bendk:history-schema-evolution
Apr 20, 2023
Merged

History schema evolution#5470
bendk merged 4 commits into
mozilla:mainfrom
bendk:history-schema-evolution

Conversation

@bendk

@bendk bendk commented Apr 6, 2023

Copy link
Copy Markdown
Contributor

Pull Request checklist

  • Quality: This PR builds and tests run cleanly
    • Note:
      • For changes that need extra cross-platform testing, consider adding [ci full] to the PR title.
      • If this pull request includes a breaking change, consider cutting a new release after merging.
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry in CHANGES_UNRELEASED.md or an explanation of why it does not need one
    • Any breaking changes to Swift or Kotlin binding APIs are noted explicitly
  • Dependencies: This PR follows our dependency management guidelines
    • Any new dependencies are accompanied by a summary of the due dilligence applied in selecting them.

Branch builds: add [firefox-android: branch-name] to the PR title.

@bendk
bendk requested review from linabutler, mhammond and skhamis April 6, 2023 14:21
// handle that, but it would mean changing a lot of legacy code that assumes that visits are
// immutable and can be compared based on the timestamp + transition fields (or the other code that
// compares only the timestamps). If we want to address this case, let's do it as part of a
// general rework of the places sync merge code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What do others think about this one? The current code treats visits are currently considered immutable, which means if we add unknown fields, they can't change either.

Should we try to rework the code so it doesn't make that assumption? I kind of want to rework the merging code anyways and this could be a good excuse.

Another question is if we want to allow new fields to be part of the "key". Visits don't have a GUID, we treat two visits the same if they have the same timestamp + transition type (except sometimes it's just timestamp -- one more reason that we may want to rework this code). Would we ever want to allow new fields to be added to this key? Right now I don't think it's worth the complexity, but I wonder what others think.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One thing I'm trying to keep in mind of roundtripping this data, is really to prevent data loss until the clients that can "catch up" and actually understand the data in those fields.

Which means -- unknown_fields should eventually disappear as a client becomes "recent", having unknown_fields stick around even if every client can understand the schema -- seems bad (IMO). I do like the idea of reworking so that we avoid this pitfall but depends on our timelines.

To your second question, I agree given the added complexity and we don't have a spec we'd be trying to design the would-be new "keys" around -- might be better to de-prioritize that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Which means -- unknown_fields should eventually disappear as a client becomes "recent", having unknown_fields stick around even if every client can understand the schema -- seems bad (IMO). I do like the idea of reworking so that we avoid this pitfall but depends on our timelines.

I'm not understanding this in relation to this question. I think we will eventually null out unknown_fields once we upgrade no matter what. My question is if we should support changes to unknown_fields before that happens.

However, it's a really good point in general and it's making me rethink the pruning idea.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, this is tricky! I could see us wanting to add new fields to individual visits in the future, but I think it's definitely worth doing that refactor separately.

The fact that we keep a rolling set of just the last 20 visits around, and don't really support per-visit operations, makes me think that any fields we add won't be very durable—visits written by older devices won't have those new fields, and, since a device might have visits that occurred in between ones that are already on the server, every device's view of the history will be slightly different. They'll have records that you visited a page at least once, but the timestamps might be off.

Another question is if we want to allow new fields to be part of the "key". Visits don't have a GUID...

Oh gosh, yes, the fact that visits don't have an "identity" apart from their timestamps complicates other things, too—including deletions, which you found in #5472. I'm also not sure what kinds of realistic "key" fields (per-device, per-visit annotations?) we can add, though, and supporting that might be too invasive in the current Sync model.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think a visit is considered immutable because it's recording a historical event - so changing it in the future really doesn't make sense. I can't see how a different device would somehow say "this old visit I previously recorded now should have different attributes".

Similarly, I think the key question is roughly the same - it's a record of something happening at an instant - I don't see how other fields being part of the key makes sense - you'd never want to record 2 different visits of the same type at the same timestamp with different attributes - there can only really have been one visit at that instant.

If that all makes sense, maybe just update the comment to reflect that?

@bendk
bendk force-pushed the history-schema-evolution branch from 0f1e4c5 to 185c730 Compare April 6, 2023 14:45

@skhamis skhamis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like a great start! It seems close, but the pruning of unknown_fields separate from visits gives me a little pause. Feels we might need to understand the implications of that a bit more

"visits": [],
// Simulate 2 new features that nobody is asking for:
// - themes for specific URLs
// - prank the user when they try to open a URL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤣 this gave me a good chuckle

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I love this! 😹

Comment thread components/places/src/history_sync/plan.rs Outdated
// handle that, but it would mean changing a lot of legacy code that assumes that visits are
// immutable and can be compared based on the timestamp + transition fields (or the other code that
// compares only the timestamps). If we want to address this case, let's do it as part of a
// general rework of the places sync merge code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One thing I'm trying to keep in mind of roundtripping this data, is really to prevent data loss until the clients that can "catch up" and actually understand the data in those fields.

Which means -- unknown_fields should eventually disappear as a client becomes "recent", having unknown_fields stick around even if every client can understand the schema -- seems bad (IMO). I do like the idea of reworking so that we avoid this pitfall but depends on our timelines.

To your second question, I agree given the added complexity and we don't have a spec we'd be trying to design the would-be new "keys" around -- might be better to de-prioritize that.

Ok(())
}

fn prune_unknown_fields(conn: &PlacesDb, now: Timestamp) -> Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a nice solution! Would be nice to have some comments of why this exists and why we need to prune (I'm assuming it's related to the immutable visits discussion).

That being said, I'm not entirely sure how I feel of separating the maintenance of pruning unknown_fields -- wouldn't they get removed with the visits their associated with (in prune_older_visits) or is this used for something else? Comments here might also lessen the confusion of this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I meant to add a comment here and totally forgot about it. But now that I'm writing it down, I'm having second thoughts. Maybe we shouldn't special case the unknown_fields and just continue to use the existing pruning code.

The reasoning behind the prune_unknown_fields is that we only sync the most recent 20 visits for each URL, so storing unknown_fields for visits older than that seems like a waste of space. However, that's not really true:

  • As you point out, the client will upgrade and at that point it will want to migrate the unknown_fields data and start using it.
  • If you delete recent visits, then that will push older URLs into the most recent 20. (This is related to History syncing doesn't delete visits #5472).

Part of the motivation here is there's a lot of visits in the DB and unknown_fields is space inefficient. You want to add an enum column that could be stored in as an integer, you end up storing an entire JSON string. Maybe we can address this another way though:

  • Lets say that we are on nightly version v120 and want to add a new feature that requires a new field
  • Before starting work on that feature, we add a migration to store the new field in a column and round-trip the data.
  • The migration should be pretty trivial, so we uplift it into v119 and an ESR patch ahead of the v120 release.
  • That way users who are doing a reasonable job keeping up-to-date don't have to deal with the space penalty.

@bendk
bendk force-pushed the history-schema-evolution branch 2 times, most recently from 781013a to 5beccc2 Compare April 12, 2023 13:33
@bendk

bendk commented Apr 12, 2023

Copy link
Copy Markdown
Contributor Author

Based on the above thoughts and some conversations, I reworked this one to only add support for unknown fields and not implement pruning.

My feeling is that if we wanted to evolve the history visits table, we should use the workflow-based approach I mentioned above. But we can always revisit this one and decide to re-implement the pruning.

@skhamis how does this one look now?

@bendk
bendk force-pushed the history-schema-evolution branch 2 times, most recently from 0b4d951 to 5beccc2 Compare April 14, 2023 15:12

@skhamis skhamis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for making those changes. I think it looks good! I think we'll be able to better prune the unknown_fields when the time comes to "consume" said fields.

👍 from me! But I'd wait for Linas review before merging!

@mhammond mhammond left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks correct to me!

// handle that, but it would mean changing a lot of legacy code that assumes that visits are
// immutable and can be compared based on the timestamp + transition fields (or the other code that
// compares only the timestamps). If we want to address this case, let's do it as part of a
// general rework of the places sync merge code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think a visit is considered immutable because it's recording a historical event - so changing it in the future really doesn't make sense. I can't see how a different device would somehow say "this old visit I previously recorded now should have different attributes".

Similarly, I think the key question is roughly the same - it's a record of something happening at an instant - I don't see how other fields being part of the key makes sense - you'd never want to record 2 different visits of the same type at the same timestamp with different attributes - there can only really have been one visit at that instant.

If that all makes sense, maybe just update the comment to reflect that?

@linabutler linabutler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks great to me, too; thanks @bendk, and apologies for the delay!

My only question is how deduping will interact with this, but more about capturing the current semantics in a test, and not trying to define new semantics for it.

I also think your approach of adding unknown fields to just the page record, instead of individual visits, is great. If we do decide to look closer at visits, I think it's worth having that discussion in the context of other changes, like supporting deletions for them, instead of trying to solve all that here.

Thank you again! 🎊

Comment thread components/places/src/bookmark_sync/incoming.rs
Comment thread components/places/src/history_sync/payload_evolution_tests.rs
"visits": [],
// Simulate 2 new features that nobody is asking for:
// - themes for specific URLs
// - prank the user when they try to open a URL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I love this! 😹

// handle that, but it would mean changing a lot of legacy code that assumes that visits are
// immutable and can be compared based on the timestamp + transition fields (or the other code that
// compares only the timestamps). If we want to address this case, let's do it as part of a
// general rework of the places sync merge code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, this is tricky! I could see us wanting to add new fields to individual visits in the future, but I think it's definitely worth doing that refactor separately.

The fact that we keep a rolling set of just the last 20 visits around, and don't really support per-visit operations, makes me think that any fields we add won't be very durable—visits written by older devices won't have those new fields, and, since a device might have visits that occurred in between ones that are already on the server, every device's view of the history will be slightly different. They'll have records that you visited a page at least once, but the timestamps might be off.

Another question is if we want to allow new fields to be part of the "key". Visits don't have a GUID...

Oh gosh, yes, the fact that visits don't have an "identity" apart from their timestamps complicates other things, too—including deletions, which you found in #5472. I'm also not sure what kinds of realistic "key" fields (per-device, per-visit annotations?) we can add, though, and supporting that might be too invasive in the current Sync model.

Comment thread components/places/src/history_sync/payload_evolution_tests.rs
@bendk
bendk force-pushed the history-schema-evolution branch from 5beccc2 to 0b3e255 Compare April 18, 2023 15:47
bendk added 4 commits April 20, 2023 11:07
This was only used in tests, always returned a Some() value, and was
always unwrapped.  I think this was a artifact of how I write the tests
and I just never remembered to remove the option.
I'm happy to say that `test_all_upgrades()` caught the error before this
migration was in place.
@bendk
bendk force-pushed the history-schema-evolution branch from 0b3e255 to 00362b9 Compare April 20, 2023 15:08
@bendk
bendk merged commit 00362b9 into mozilla:main Apr 20, 2023
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