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

Throw if a table being written to has no subscriptions and FLX sync is enabled #5488

Merged
merged 20 commits into from
Jun 1, 2022

Conversation

jbreams
Copy link
Contributor

@jbreams jbreams commented May 16, 2022

What, How & Why?

This is one of the things we committed to for World in the FLX sync adaptive changes scope.

It seems an easy footgun for users is to do writes to a table where they haven't set any permissions yet. This adds a validation hook to the ClientReplication class where we can just check the latest subscription set for whether it has a subscription for the table being written to. It doesn't validate anything about the query - compensating writes on the server will do stricter validation than on the client.

☑️ ToDos

  • 📝 Changelog update
  • 🚦 Tests (or not relevant)

@cla-bot cla-bot bot added the cla: yes label May 16, 2022
@jbreams jbreams force-pushed the jbr/throw_on_writes_without_a_subscription branch from b5b387c to 2c58d3a Compare May 27, 2022 19:46
@jbreams jbreams marked this pull request as ready for review May 27, 2022 20:23
@tgoyne
Copy link
Member

tgoyne commented May 27, 2022

Wouldn't it be both simpler and faster to have something like std::unordered_set<std::string> SubscriptionStore::get_subscribed_tables(Transaction& tr)? Checking if the table has subscriptions via SubscriptionSet would make sense if that's something that SubscriptionSet could easily answer, but it appears to be something that has zero overlap with what SubscriptionSet is currently doing.

@jbreams
Copy link
Contributor Author

jbreams commented May 27, 2022

@tgoyne - not sure I follow - SubscriptionSet is the thing that knows what tables have which subscriptions. That's like the whole point of that type.

@tgoyne
Copy link
Member

tgoyne commented May 27, 2022

I don't understand how entirely new functionality can possibly be the point of a pre-existing type. The code which uses m_tables does not access any other functionality from SubscriptionSet, and the code which uses the other functionality from SubscriptionSet doesn't access m_tables.

@jbreams
Copy link
Contributor Author

jbreams commented May 27, 2022

The point of the SubscriptionSet type is to hold and make it so you can query all the info about subscriptions, and by extension which tables they belong to. SubscriptionStore doesn't actually know/do anything with the data about subscriptions or tables or queries or anything, it just produces SubscriptionSets.

Is your suggestion here that SubscriptionStore should have some special side-logic to lookup just the table names for the latest SubscriptionSet without actually creating a SubscriptionSet and returning that as a set?

@jbreams
Copy link
Contributor Author

jbreams commented May 27, 2022

@tgoyne I've put up what I think you're suggesting. Is this what you had in mind?

@tgoyne
Copy link
Member

tgoyne commented May 27, 2022

Yes. I think the primary advantage of that is it avoids introducing mutable state with additional invariants which have to be maintained, particularly when the mutable version isn't directly relevant (since we don't allow mixing updates to subscriptions and other writes).

It'll also be faster, but you'd probably need an absurd number of subscriptions for that to be a consideration even with it being something that has to happen on every write transaction, so that's not a real consideration.

(The "for_latest" part in get_tables_for_latest() is misleading, but I assume you just haven't made a final pass over the names)

Copy link
Contributor

@ironage ironage left a comment

Choose a reason for hiding this comment

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

I wasn't involved in the discussion around this, but it seems like a very disruptive change in functionality. If the server processes changes, the objects modified outside subscriptions are automatically reverted, but with this check in place on the client side, all changes in the same commit are lost. It seems like it could be a good feature to have on during development though, what do you think about making this configurable so devs can turn it off for production apps?

return table_name.substr(g_class_name_prefix_len);
}

static bool table_name_is_class_name(StringData table_name)
Copy link
Contributor

Choose a reason for hiding this comment

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

my preference would be to overload table_is_public since they do the same thing but for different types

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

Comment on lines +111 to +114
auto sub_mgr = weak_sub_mgr.lock();
if (!sub_mgr) {
throw std::runtime_error("Subscription store was destroyed while user writes were on-going");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not move this weak -> strong pointer logic into the UniqueFunction? Then we can reduce the scope of strong pointers floating around.

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 turns out we don't need to capture the sub_mgr at all. I think we still want to lock it and get the info out of it here so that we can cache the result across all the writes in this transaction.

@jbreams
Copy link
Contributor Author

jbreams commented May 27, 2022

The scope of this work was defined and approved and signed off on in RPM-272. I think making this so you can control whether this validation is on/off within your Realm config would be a significant change in the agreed-upon scope. Also this apparently was enough of a footgun for developers during the beta period that just making it an error was decided to be worth it. cc @ianpward

@ironage
Copy link
Contributor

ironage commented May 27, 2022

Fair enough. But it does look like that decision was made when the consequence of this type of write was a client reset. I imagine it is much less of a problem with the advent of compensating writes.

@tgoyne
Copy link
Member

tgoyne commented May 27, 2022

I don't really have an opinion on if this needs an option, but adding an option would be trivial (just add bool strict_flx_sync_object_creation = true; to SyncConfig, change the check to m_config.sync_config && m_config.sync_config->flx_sync_requested && m_config.sync_config->strict_flx_sync_object_creation and add a test).

@jbreams
Copy link
Contributor Author

jbreams commented May 28, 2022

Compensating writes was planned and in-progress when this feature was scoped. The idea from the scoping discussion was that this behavior is always a result of a programmer error and that it was very confusing for new users who would start doing writes and nothing would happen or they'd get an error message back asynchronously. From what I recall, it came up during user interviews @ianpward did. It was decided to just make the case where you're writing to a table without any subscriptions always an error. I was not the person pushing for this, so maybe @ianpward or @jsflax can correct me if I'm wrong about the history of this feature.

@ianpward
Copy link
Contributor

Yes - we have had a clear and consistent feedback not only from our users, but shockingly our own realm engineers ran into this several times during the hackathon week.

@ironage
Copy link
Contributor

ironage commented May 30, 2022

Ok makes sense, I'm fine with this going ahead. We can always adjust later if needed. I will review again once the test failures are fixed.

@jbreams jbreams force-pushed the jbr/throw_on_writes_without_a_subscription branch 2 times, most recently from 9b05a5a to c56cfae Compare May 31, 2022 15:13
@jbreams
Copy link
Contributor Author

jbreams commented May 31, 2022

@ironage , I think the failures here are not caused by this work, can you review?

Copy link
Contributor

@ironage ironage left a comment

Choose a reason for hiding this comment

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

👍
Could use a note in the change log.


auto latest_sub_tables = sub_mgr->get_tables_for_latest(tr);
return [tables = std::move(latest_sub_tables)](const Table& table) {
if (table.is_embedded()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

heads up @danieltabacaru: we're going to need a similar early out for asymmetric tables depending on which PR gets merged first.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@jbreams since my pr went in first, you'll have to add the check

@@ -447,12 +447,21 @@ class ClientReplication final : public SyncReplication {
{
}

// A write validator factory takes a read-able transaction and returns a UniqueFunction containing a
// SyncReplication::WriteValidator. The factory will get called at the start of a write transaction
// and the WriteValidator it returns will be re-used for all mutations wit`hin the transaction.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// and the WriteValidator it returns will be re-used for all mutations wit`hin the transaction.
// and the WriteValidator it returns will be re-used for all mutations within the transaction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed.

@@ -102,6 +103,30 @@ void RealmCoordinator::create_sync_session()
return;

m_sync_session = m_config.sync_config->user->sync_manager()->get_session(m_db, *m_config.sync_config);
if (m_config.sync_config && m_config.sync_config->flx_sync_requested) {
std::weak_ptr<sync::SubscriptionStore> weak_sub_mgr(m_sync_session->get_flx_subscription_store());
sync::ClientReplication& history = static_cast<sync::ClientReplication&>(*m_db->get_replication());
Copy link
Collaborator

Choose a reason for hiding this comment

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

auto?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@@ -447,12 +447,21 @@ class ClientReplication final : public SyncReplication {
{
}

// A write validator factory takes a read-able transaction and returns a UniqueFunction containing a
Copy link
Collaborator

Choose a reason for hiding this comment

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

it sounds it is only used with a write transaction, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed this to say "write transaction"

Copy link
Collaborator

@danieltabacaru danieltabacaru left a comment

Choose a reason for hiding this comment

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

LGTM

@jbreams jbreams force-pushed the jbr/throw_on_writes_without_a_subscription branch from ccf398a to 88087e1 Compare June 1, 2022 13:01
@jbreams jbreams merged commit 02adb14 into master Jun 1, 2022
@jbreams jbreams deleted the jbr/throw_on_writes_without_a_subscription branch June 1, 2022 17:03
jedelbo added a commit that referenced this pull request Jun 2, 2022
jedelbo added a commit that referenced this pull request Jun 2, 2022
@stoneyMDB stoneyMDB linked an issue Jun 2, 2022 that may be closed by this pull request
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 22, 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.

Need error message for object creation before subscription
6 participants