##// END OF EJS Templates
fix(caching): fixed problems with Cache query for users....
fix(caching): fixed problems with Cache query for users. The old way of querying caused the user get query to be always cached, and returning old results even in 2fa forms. The new limited query doesn't cache the user object resolving issues

File last commit:

r5095:aa627a5f default
r5365:ae8a165b default
Show More
caching_query.py
250 lines | 8.0 KiB | text/x-python | PythonLexer
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""caching_query.py
Represent functions and classes
which allow the usage of Dogpile caching with SQLAlchemy.
Introduces a query option called FromCache.
.. versionchanged:: 1.4 the caching approach has been altered to work
based on a session event.
The three new concepts introduced here are:
* ORMCache - an extension for an ORM :class:`.Session`
retrieves results in/from dogpile.cache.
* FromCache - a query option that establishes caching
parameters on a Query
* RelationshipCache - a variant of FromCache which is specific
to a query invoked during a lazy load.
The rest of what's here are standard SQLAlchemy and
dogpile.cache constructs.
"""
from dogpile.cache.api import NO_VALUE
from sqlalchemy import event
from sqlalchemy.orm import loading
from sqlalchemy.orm.interfaces import UserDefinedOption
DEFAULT_REGION = "sql_cache_short"
class ORMCache:
"""An add-on for an ORM :class:`.Session` optionally loads full results
from a dogpile cache region.
cache = ORMCache(regions={})
cache.listen_on_session(Session)
"""
def __init__(self, regions):
self.cache_regions = regions or self._get_region()
self._statement_cache = {}
@classmethod
def _get_region(cls):
from rhodecode.lib.rc_cache import region_meta
return region_meta.dogpile_cache_regions
def listen_on_session(self, session_factory):
event.listen(session_factory, "do_orm_execute", self._do_orm_execute)
def _do_orm_execute(self, orm_context):
for opt in orm_context.user_defined_options:
if isinstance(opt, RelationshipCache):
opt = opt._process_orm_context(orm_context)
if opt is None:
continue
if isinstance(opt, FromCache):
dogpile_region = self.cache_regions[opt.region]
if dogpile_region.expiration_time <= 0:
# don't cache 0 time expiration cache
continue
if opt.cache_key:
our_cache_key = f'SQL_CACHE_{opt.cache_key}'
else:
our_cache_key = opt._generate_cache_key(
orm_context.statement, orm_context.parameters, self
)
if opt.ignore_expiration:
cached_value = dogpile_region.get(
our_cache_key,
expiration_time=opt.expiration_time,
ignore_expiration=opt.ignore_expiration,
)
else:
def createfunc():
return orm_context.invoke_statement().freeze()
cached_value = dogpile_region.get_or_create(
our_cache_key,
createfunc,
expiration_time=opt.expiration_time,
)
if cached_value is NO_VALUE:
# keyerror? this is bigger than a keyerror...
raise KeyError()
orm_result = loading.merge_frozen_result(
orm_context.session,
orm_context.statement,
cached_value,
load=False,
)
return orm_result()
else:
return None
def invalidate(self, statement, parameters, opt):
"""Invalidate the cache value represented by a statement."""
statement = statement.__clause_element__()
dogpile_region = self.cache_regions[opt.region]
cache_key = opt._generate_cache_key(statement, parameters, self)
dogpile_region.delete(cache_key)
class FromCache(UserDefinedOption):
"""Specifies that a Query should load results from a cache."""
propagate_to_loaders = False
def __init__(
self,
region=DEFAULT_REGION,
cache_key=None,
expiration_time=None,
ignore_expiration=False,
):
"""Construct a new FromCache.
:param region: the cache region. Should be a
region configured in the dictionary of dogpile
regions.
:param cache_key: optional. A string cache key
that will serve as the key to the query. Use this
if your query has a huge amount of parameters (such
as when using in_()) which correspond more simply to
some other identifier.
"""
self.region = region
self.cache_key = cache_key
self.expiration_time = expiration_time
self.ignore_expiration = ignore_expiration
# this is not needed as of SQLAlchemy 1.4.28;
# UserDefinedOption classes no longer participate in the SQL
# compilation cache key
def _gen_cache_key(self, anon_map, bindparams):
return None
def _generate_cache_key(self, statement, parameters, orm_cache):
"""generate a cache key with which to key the results of a statement.
This leverages the use of the SQL compilation cache key which is
repurposed as a SQL results key.
"""
statement_cache_key = statement._generate_cache_key()
key = statement_cache_key.to_offline_string(
orm_cache._statement_cache, statement, parameters
) + repr(self.cache_key)
# print("here's our key...%s" % key)
return key
class RelationshipCache(FromCache):
"""Specifies that a Query as called within a "lazy load"
should load results from a cache."""
propagate_to_loaders = True
def __init__(
self,
attribute,
region=DEFAULT_REGION,
cache_key=None,
expiration_time=None,
ignore_expiration=False,
):
"""Construct a new RelationshipCache.
:param attribute: A Class.attribute which
indicates a particular class relationship() whose
lazy loader should be pulled from the cache.
:param region: name of the cache region.
:param cache_key: optional. A string cache key
that will serve as the key to the query, bypassing
the usual means of forming a key from the Query itself.
"""
self.region = region
self.cache_key = cache_key
self.expiration_time = expiration_time
self.ignore_expiration = ignore_expiration
self._relationship_options = {
(attribute.property.parent.class_, attribute.property.key): self
}
def _process_orm_context(self, orm_context):
current_path = orm_context.loader_strategy_path
if current_path:
mapper, prop = current_path[-2:]
key = prop.key
for cls in mapper.class_.__mro__:
if (cls, key) in self._relationship_options:
relationship_option = self._relationship_options[
(cls, key)
]
return relationship_option
def and_(self, option):
"""Chain another RelationshipCache option to this one.
While many RelationshipCache objects can be specified on a single
Query separately, chaining them together allows for a more efficient
lookup during load.
"""
self._relationship_options.update(option._relationship_options)
return self