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

Only use in-memory cache for database settings, set ttl=5 #12166

Merged
merged 6 commits into from
May 11, 2022

Conversation

AlanCoding
Copy link
Member

@AlanCoding AlanCoding commented May 4, 2022

SUMMARY

This is a speculative replacement for #12095, and maybe #5765 for good measure.

EDIT: due to unexpected developments, this does not replace 12095, but it does resolve 5765

Made a writeup on this here:

https://gist.github.com/AlanCoding/3bff66c63db36ae7cb8c326df2c38e29

ISSUE TYPE
  • Feature Pull Request
  • Bugfix Pull Request
COMPONENT NAME
  • API
ADDITIONAL INFORMATION

The __getattr__ method here will be called for any Django setting. The _get_local is only called for settings registered via our database settings system. For terminology, we call settings which are not registered "file-based settings".

It doesn't make any sense to cache file-based settings. They are already static values. This in-memory cache is not only worthless for file-based settings, it actively throws errors. This will even happen when the ttl value is 0, so someone probably thought that this was equivalent to turning the caching off, but it's not.

I didn't 100% need to introduce a new method here, but doing this avoids including the validate keyword argument in the cache key, which is my preference. It might also reduce the test churn from the change.

@AlanCoding AlanCoding force-pushed the thread_hot branch 2 times, most recently from d3f75a4 to 3156c49 Compare May 4, 2022 16:29
@AlanCoding
Copy link
Member Author

This is blocked on a particularly nasty migration error on initial migration.

Traceback (most recent call last):
File "/bin/awx-manage", line 33, in <module>
  sys.exit(load_entry_point('awx==4.3.0', 'console_scripts', 'awx-manage')())
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/__init__.py", line 170, in manage
  execute_from_command_line(sys.argv)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
  utility.execute()
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/management/__init__.py", line 413, in execute
  self.fetch_command(subcommand).run_from_argv(self.argv)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/management/base.py", line 354, in run_from_argv
  self.execute(*args, **cmd_options)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/management/base.py", line 393, in execute
  self.check()
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/management/base.py", line 419, in check
  all_issues = checks.run_checks(
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/checks/registry.py", line 76, in run_checks
  new_errors = check(app_configs=app_configs, databases=databases)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/checks/urls.py", line 40, in check_url_namespaces_unique
  all_namespaces = _load_all_namespaces(resolver)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/core/checks/urls.py", line 57, in _load_all_namespaces
  url_patterns = getattr(resolver, 'url_patterns', [])
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/utils/functional.py", line 48, in __get__
  res = instance.__dict__[self.name] = self.func(instance)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/urls/resolvers.py", line 602, in url_patterns
  patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/utils/functional.py", line 48, in __get__
  res = instance.__dict__[self.name] = self.func(instance)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/urls/resolvers.py", line 595, in urlconf_module
  return import_module(self.urlconf_name)
File "/usr/lib64/python3.9/importlib/__init__.py", line 127, in import_module
  return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 850, in exec_module
File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/urls.py", line 12, in <module>
  re_path(r'^api/', include('awx.api.urls', namespace='api')),
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/django/urls/conf.py", line 34, in include
  urlconf_module = import_module(urlconf_module)
File "/usr/lib64/python3.9/importlib/__init__.py", line 127, in import_module
  return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 850, in exec_module
File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/api/urls/__init__.py", line 5, in <module>
  from .urls import urlpatterns
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/api/urls/urls.py", line 71, in <module>
  from .oauth2_root import urls as oauth2_root_urls
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/api/urls/oauth2_root.py", line 10, in <module>
  from oauth2_provider import views
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/oauth2_provider/views/__init__.py", line 2, in <module>
  from .base import AuthorizationView, TokenView, RevokeTokenView  # isort:skip
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/oauth2_provider/views/base.py", line 69, in <module>
  class AuthorizationView(BaseAuthorizationView, FormView):
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/oauth2_provider/views/base.py", line 93, in AuthorizationView
  server_class = oauth2_settings.OAUTH2_SERVER_CLASS
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/oauth2_provider/settings.py", line 150, in __getattr__
  if attr not in self.defaults:
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/oauth2_provider/settings.py", line 150, in __getattr__
  if attr not in self.defaults:
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/oauth2_provider/settings.py", line 150, in __getattr__
  if attr not in self.defaults:
[Previous line repeated 922 more times]
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/__init__.py", line 89, in oauth2_getattribute
  val = settings.OAUTH2_PROVIDER.get(attr)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/conf/settings.py", line 491, in __getattr_without_cache__
  return getattr(self._wrapped, name)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/conf/settings.py", line 406, in __getattr__
  value = self._get_local_with_cache(name)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/cachetools/decorators.py", line 19, in wrapper
  return cache[k]
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/cachetools/ttl.py", line 76, in __getitem__
  link = self.__getlink(key)
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/cachetools/ttl.py", line 206, in __getlink
  value = self.__links[key]
File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/cachetools/keys.py", line 19, in __hash__
  self.__hashvalue = hashvalue = hash(self)
RecursionError: maximum recursion depth exceeded while calling a Python object

@AlanCoding
Copy link
Member Author

That recursion error looks like it's been fixed, running more tests now.

@AlanCoding
Copy link
Member Author

I believe this is still relevant, but needs to be rebased and re-tested on top of #11373

@AlanCoding
Copy link
Member Author

Clarifying #12084

Make necessary adjustments to monkeypatch
  as it is very vunerable to recursion

Clear cache for each request

Clear cache if a setting is changed
@AlanCoding AlanCoding marked this pull request as ready for review May 10, 2022 00:28
@AlanCoding
Copy link
Member Author

Testing is looking good, now I just plan to write a short knowledge base article on general cache knowledge.

@AlanCoding AlanCoding changed the title Only use in-memory cache for database settings Only use in-memory cache for database settings, set ttl=5 May 10, 2022

val = None
if 'migrate' not in sys.argv:
if (isinstance(attr, str)) and (attr in DEFAULTS) and (not attr.startswith('_')):
Copy link
Member Author

Choose a reason for hiding this comment

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

@jbradberry I'm removing the exception for migrations here because it is handled elsewhere now - by not initializing the settings wrapper when this condition is met. The other changes are to be more protective in __getattribute__ code, which is scary.

@@ -26,6 +26,11 @@
perf_logger = logging.getLogger('awx.analytics.performance')


class SettingsCacheMiddleware(MiddlewareMixin):
def process_request(self, request):
settings._awx_conf_memoizedcache.clear()
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't get the reasoning of doing this here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Any given automated script could do this:

  • make a PATCH to /api/v2/settings/jobs/, applying new values
  • right after that, make a GET to /api/v2/settings/jobs/

Regardless of what uWSGI worker processes the second (GET) request, it will be under the 5 second ttl threshold. That means that it would give the values from before the PATCH. The automated script will then conclude that the PATCH it made did not take effect.

Now replace the generalized "automated script" with out integration tests, and you see the immediate reason this was needed.

Copy link
Member

@rebeccahhh rebeccahhh left a comment

Choose a reason for hiding this comment

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

I think the performance improvements here are pretty excellent.

@AlanCoding AlanCoding merged commit aaad634 into ansible:devel May 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants