##// END OF EJS Templates
chore(release): merged default into stable branch
chore(release): merged default into stable branch

File last commit:

r5095:aa627a5f default
r5558:77c2b736 merge v5.3.0 stable
Show More
caching_query.py
250 lines | 8.0 KiB | text/x-python | PythonLexer
copyrights: updated for 2023
r5088 # Copyright (C) 2010-2023 RhodeCode GmbH
project: added all source files and assets
r1 #
# 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/
caches: use dogpile for sql_cache_short region.
r2883 """caching_query.py
project: added all source files and assets
r1
caches: use dogpile for sql_cache_short region.
r2883 Represent functions and classes
which allow the usage of Dogpile caching with SQLAlchemy.
Introduces a query option called FromCache.
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 .. versionchanged:: 1.4 the caching approach has been altered to work
based on a session event.
project: added all source files and assets
r1 The three new concepts introduced here are:
caches: use new sqlalchemy 1.4 caching query approach
r5000 * ORMCache - an extension for an ORM :class:`.Session`
caches: use dogpile for sql_cache_short region.
r2883 retrieves results in/from dogpile.cache.
project: added all source files and assets
r1 * 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
caches: use dogpile for sql_cache_short region.
r2883 dogpile.cache constructs.
project: added all source files and assets
r1
"""
caches: use dogpile for sql_cache_short region.
r2883 from dogpile.cache.api import NO_VALUE
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 from sqlalchemy import event
from sqlalchemy.orm import loading
from sqlalchemy.orm.interfaces import UserDefinedOption
DEFAULT_REGION = "sql_cache_short"
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 class ORMCache:
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 """An add-on for an ORM :class:`.Session` optionally loads full results
from a dogpile cache region.
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 cache = ORMCache(regions={})
cache.listen_on_session(Session)
project: added all source files and assets
r1
"""
caches: use new sqlalchemy 1.4 caching query approach
r5000
def __init__(self, regions):
self.cache_regions = regions or self._get_region()
self._statement_cache = {}
@classmethod
def _get_region(cls):
caches: use dogpile for sql_cache_short region.
r2883 from rhodecode.lib.rc_cache import region_meta
return region_meta.dogpile_cache_regions
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 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]
project: added all source files and assets
r1
libs: major refactor for python3
r5085 if dogpile_region.expiration_time <= 0:
# don't cache 0 time expiration cache
continue
caches: use new sqlalchemy 1.4 caching query approach
r5000 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
)
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 if opt.ignore_expiration:
cached_value = dogpile_region.get(
our_cache_key,
expiration_time=opt.expiration_time,
ignore_expiration=opt.ignore_expiration,
)
else:
caches: use dogpile for sql_cache_short region.
r2883
caches: use new sqlalchemy 1.4 caching query approach
r5000 def createfunc():
return orm_context.invoke_statement().freeze()
cached_value = dogpile_region.get_or_create(
our_cache_key,
createfunc,
expiration_time=opt.expiration_time,
)
caches: don't use lambda for caching query for easier cache debugging.
r2671
caches: use new sqlalchemy 1.4 caching query approach
r5000 if cached_value is NO_VALUE:
# keyerror? this is bigger than a keyerror...
raise KeyError()
caches: don't use lambda for caching query for easier cache debugging.
r2671
caches: use new sqlalchemy 1.4 caching query approach
r5000 orm_result = loading.merge_frozen_result(
orm_context.session,
orm_context.statement,
cached_value,
load=False,
caches: use dogpile for sql_cache_short region.
r2883 )
caches: use new sqlalchemy 1.4 caching query approach
r5000 return orm_result()
project: added all source files and assets
r1 else:
caches: use new sqlalchemy 1.4 caching query approach
r5000 return None
caches: use dogpile for sql_cache_short region.
r2883
caches: use new sqlalchemy 1.4 caching query approach
r5000 def invalidate(self, statement, parameters, opt):
"""Invalidate the cache value represented by a statement."""
statement = statement.__clause_element__()
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 dogpile_region = self.cache_regions[opt.region]
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 cache_key = opt._generate_cache_key(statement, parameters, self)
caches: use dogpile for sql_cache_short region.
r2883 dogpile_region.delete(cache_key)
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 class FromCache(UserDefinedOption):
project: added all source files and assets
r1 """Specifies that a Query should load results from a cache."""
propagate_to_loaders = False
caches: use new sqlalchemy 1.4 caching query approach
r5000 def __init__(
self,
region=DEFAULT_REGION,
cache_key=None,
expiration_time=None,
ignore_expiration=False,
):
project: added all source files and assets
r1 """Construct a new FromCache.
:param region: the cache region. Should be a
caches: use new sqlalchemy 1.4 caching query approach
r5000 region configured in the dictionary of dogpile
regions.
project: added all source files and assets
r1
:param cache_key: optional. A string cache key
caches: use new sqlalchemy 1.4 caching query approach
r5000 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.
project: added all source files and assets
r1
"""
self.region = region
self.cache_key = cache_key
caches: use new sqlalchemy 1.4 caching query approach
r5000 self.expiration_time = expiration_time
self.ignore_expiration = ignore_expiration
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 # 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
project: added all source files and assets
r1
caches: use new sqlalchemy 1.4 caching query approach
r5000 class RelationshipCache(FromCache):
project: added all source files and assets
r1 """Specifies that a Query as called within a "lazy load"
caches: use new sqlalchemy 1.4 caching query approach
r5000 should load results from a cache."""
project: added all source files and assets
r1
propagate_to_loaders = True
caches: use new sqlalchemy 1.4 caching query approach
r5000 def __init__(
self,
attribute,
region=DEFAULT_REGION,
cache_key=None,
expiration_time=None,
ignore_expiration=False,
):
project: added all source files and assets
r1 """Construct a new RelationshipCache.
:param attribute: A Class.attribute which
caches: use new sqlalchemy 1.4 caching query approach
r5000 indicates a particular class relationship() whose
lazy loader should be pulled from the cache.
project: added all source files and assets
r1
caches: use dogpile for sql_cache_short region.
r2883 :param region: name of the cache region.
:param cache_key: optional. A string cache key
caches: use new sqlalchemy 1.4 caching query approach
r5000 that will serve as the key to the query, bypassing
the usual means of forming a key from the Query itself.
caches: use dogpile for sql_cache_short region.
r2883
project: added all source files and assets
r1 """
self.region = region
caches: use dogpile for sql_cache_short region.
r2883 self.cache_key = cache_key
caches: use new sqlalchemy 1.4 caching query approach
r5000 self.expiration_time = expiration_time
self.ignore_expiration = ignore_expiration
project: added all source files and assets
r1 self._relationship_options = {
(attribute.property.parent.class_, attribute.property.key): self
}
caches: use new sqlalchemy 1.4 caching query approach
r5000 def _process_orm_context(self, orm_context):
current_path = orm_context.loader_strategy_path
caches: use dogpile for sql_cache_short region.
r2883
caches: use new sqlalchemy 1.4 caching query approach
r5000 if current_path:
mapper, prop = current_path[-2:]
caches: use dogpile for sql_cache_short region.
r2883 key = prop.key
project: added all source files and assets
r1
for cls in mapper.class_.__mro__:
if (cls, key) in self._relationship_options:
caches: use new sqlalchemy 1.4 caching query approach
r5000 relationship_option = self._relationship_options[
(cls, key)
]
return relationship_option
project: added all source files and assets
r1
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