Dark launch: Fix 1+N queries in third party auth config fetching - #23824
Conversation
f8cf82e to
fa1bd03
Compare
| provider = OAuth2ProviderConfig.current(oauth2_backend_name) | ||
| oauth2_backends = OAuth2ProviderConfig.objects.prefetch_related('site').all() | ||
| for provider in unique_everseen(oauth2_backends, key=lambda p: p.backend_name): | ||
| if provider.enabled_for_current_site and provider.backend_name in _PSA_OAUTH2_BACKENDS: |
There was a problem hiding this comment.
Since we're only ever doing this for the site we're on, can we do the reverse and get all the providers where the site is the current site?
If I'm reading this right, it looks like a provider is mapped to at most one site:
https://github.com/edx/edx-platform/blob/0f5c474f8f49e7dbc1a7d60e15f5409a29dfe69f/common/djangoapps/third_party_auth/models.py#L138
There was a problem hiding this comment.
Agreed that that would make more sense. For that matter, why not add site, enabled, and backend_name to the query and skip the entire guard clause that's inside the for loop?
I was being cautious because I'm not sure why it wasn't written that way in the first place... but maybe there's not a good reason. :-)
There was a problem hiding this comment.
Yea, if we find one later, we can always undo the change and put in the good reason as a comment. Let's just make this a more sensible query.
There was a problem hiding this comment.
Curiously, most of the tests pass if I do the below, including one of the two that was failing before—but the other one (test_provider.py:38) is still failing as if the query is being cached.
site = Site.objects.get_current(get_current_request())
oauth2_backends = OAuth2ProviderConfig.objects.filter(enabled=True, site=site, backend_name__in=_PSA_OAUTH2_BACKENDS)
for provider in unique_everseen(oauth2_backends, key=lambda p: p.backend_name):
yield providerFailure:
def test_runtime_configuration(self):
self.configure_google_provider(enabled=True)
enabled_providers = provider.Registry.enabled()
self.assertEqual(len(enabled_providers), 1)
self.assertEqual(enabled_providers[0].name, "Google")
self.assertEqual(enabled_providers[0].get_setting("SECRET"), "opensesame")
self.configure_google_provider(enabled=False)
enabled_providers = provider.Registry.enabled()
> self.assertEqual(len(enabled_providers), 0)
E AssertionError: 1 != 0
common/djangoapps/third_party_auth/tests/test_provider.py:38: AssertionErrorThere was a problem hiding this comment.
OK, I think I see what's happening here. The test configures a provider that is enabled, then configures one that is disabled, both with the same backend name, so that we end up with two objects in the DB. The code on master is supposed to pick up only the most recent object, and then discard it if it is disabled. That's why it's not part of the query.
(But then I don't understand why site wasn't part of the query! Surely we'd want to partition that append-to-override behavior by site.)
There was a problem hiding this comment.
(And the reason my code was failing is because it was getting the first row with a given slug or backend or key, not the last.)
There was a problem hiding this comment.
So this is because it's backed by config models right? The config model table is essentially keeping a history of all the ways a setting has been set. So to get the correct setting, we want to get the latest setting for any given name and treat it as the source of truth. So if the latest version is disabled, then we treat the value as disabled. It seems like the test is valid.
There was a problem hiding this comment.
Yeah, and my iterator was skipping the disabled one. I think the existing code has a bug around handling of sites, though; if two providers are registered for the same backend_name but different sites, the existing code makes the more recent one shadow the other.
|
Curious: Is there a ticket for this work? I'm wondering if we are investing time in preemptive optimization here. |
|
@timmc-edx What is the context here? What problem did you see that prompt this change? The change is not really obvious to me it improves things. I read your PR description and comments and couldn't make good sense out of them. |
|
Sorry, yes, this is https://openedx.atlassian.net/browse/ARCHBOM-1139 -- we're seeing an inordinate number of SELECTs on |
|
@timmc-edx Great. With that ticket, now I know what you are trying to do. I'll need to review again. Thanks |
|
At this point, all I can say for sure is that there's something I'm missing around how (The Slack thread you were tagged in contains some of my stream of consciousness, so maybe that's already obvious by now.) |
The filtering of SAML provider configs is the most likely culprit for what is causing a large number of requests for django_site when loading the login page in production. (ARCHBOM-1139)
fa1bd03 to
894dc12
Compare
schenedx
left a comment
There was a problem hiding this comment.
How many queries would this change shrink down to?
My comments are minor in importance.
| self.assertEqual(prov.enabled_for_current_site, False) | ||
|
|
||
| @with_site_configuration(SITE_DOMAIN_A) | ||
| def test_providers_with_same_key_independent_across_sites(self): |
There was a problem hiding this comment.
I believe we can have another test case where each site will have both oauth_provider set and SAML_provider set. Then the return should be the latest oauth_provider and latest saml provider in a list.
| if config.enabled: | ||
| yield config | ||
|
|
||
| for provider in enabled_in_site(OAuth2ProviderConfig, {'backend_name__in': _PSA_OAUTH2_BACKENDS}): |
There was a problem hiding this comment.
Can you please add more comments for this loop? Like First, we should get all the ID providers using oauth2
- Prep for moving `current_partitioned` (now `current_all`) to django-config-models - Unroll `enabled_in_site` - Don't bother with `backend_name` in DB query: It's very unlikely to reduce the response size; it makes the queries harder to read; and most importantly increases the necessary complexity for `current_all` (since if `'site_id'` is added to `KEY_FIELDS` we will then not need any additional filters at all.) - Add comments - Add additional test
|
Addressed some feedback. I'm also going to see about:
|
|
I've changed it to be a dark launch for now, so that the old implementation is still used, but the new code is run under a try/catch and any failures or result mismatches are sent to New Relic. My intention is to open a separate PR to put |
Not moving `current_all` to django-config-models after all; it's enough of a hack that it doesn't really belong perfectly in either place. Some renaming and reworking of parameters.
ac09934 to
ce8a0b7
Compare
|
Your PR has finished running tests. There were no failures. |
|
EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production. |
|
EdX Release Notice: This PR has been deployed to the production environment. |
Dark launch comparison of a fix for the 1+N issue on the login page. Old implementation is still run and the results used, but if there are discrepancies with the new implementation, logs metrics and debugging info to New Relic.
This should also fix an issue with configs shadowing each other across sites, but we only have configs for one site in Production, so I'm not expecting the comparison to trip over that.