cache.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. # Copyright 2019-2021 Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. import os
  16. import re
  17. import threading
  18. from typing import Callable, Dict, Optional
  19. import attr
  20. from synapse.python_dependencies import DependencyException, check_requirements
  21. from ._base import Config, ConfigError
  22. logger = logging.getLogger(__name__)
  23. # The prefix for all cache factor-related environment variables
  24. _CACHE_PREFIX = "SYNAPSE_CACHE_FACTOR"
  25. # Map from canonicalised cache name to cache.
  26. _CACHES: Dict[str, Callable[[float], None]] = {}
  27. # a lock on the contents of _CACHES
  28. _CACHES_LOCK = threading.Lock()
  29. _DEFAULT_FACTOR_SIZE = 0.5
  30. _DEFAULT_EVENT_CACHE_SIZE = "10K"
  31. @attr.s(slots=True, auto_attribs=True)
  32. class CacheProperties:
  33. # The default factor size for all caches
  34. default_factor_size: float = float(
  35. os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE)
  36. )
  37. resize_all_caches_func: Optional[Callable[[], None]] = None
  38. properties = CacheProperties()
  39. def _canonicalise_cache_name(cache_name: str) -> str:
  40. """Gets the canonical form of the cache name.
  41. Since we specify cache names in config and environment variables we need to
  42. ignore case and special characters. For example, some caches have asterisks
  43. in their name to denote that they're not attached to a particular database
  44. function, and these asterisks need to be stripped out
  45. """
  46. cache_name = re.sub(r"[^A-Za-z_1-9]", "", cache_name)
  47. return cache_name.lower()
  48. def add_resizable_cache(
  49. cache_name: str, cache_resize_callback: Callable[[float], None]
  50. ) -> None:
  51. """Register a cache that's size can dynamically change
  52. Args:
  53. cache_name: A reference to the cache
  54. cache_resize_callback: A callback function that will be ran whenever
  55. the cache needs to be resized
  56. """
  57. # Some caches have '*' in them which we strip out.
  58. cache_name = _canonicalise_cache_name(cache_name)
  59. # sometimes caches are initialised from background threads, so we need to make
  60. # sure we don't conflict with another thread running a resize operation
  61. with _CACHES_LOCK:
  62. _CACHES[cache_name] = cache_resize_callback
  63. # Ensure all loaded caches are sized appropriately
  64. #
  65. # This method should only run once the config has been read,
  66. # as it uses values read from it
  67. if properties.resize_all_caches_func:
  68. properties.resize_all_caches_func()
  69. class CacheConfig(Config):
  70. section = "caches"
  71. _environ = os.environ
  72. @staticmethod
  73. def reset() -> None:
  74. """Resets the caches to their defaults. Used for tests."""
  75. properties.default_factor_size = float(
  76. os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE)
  77. )
  78. properties.resize_all_caches_func = None
  79. with _CACHES_LOCK:
  80. _CACHES.clear()
  81. def generate_config_section(self, **kwargs) -> str:
  82. return """\
  83. ## Caching ##
  84. # Caching can be configured through the following options.
  85. #
  86. # A cache 'factor' is a multiplier that can be applied to each of
  87. # Synapse's caches in order to increase or decrease the maximum
  88. # number of entries that can be stored.
  89. # The number of events to cache in memory. Not affected by
  90. # caches.global_factor.
  91. #
  92. #event_cache_size: 10K
  93. caches:
  94. # Controls the global cache factor, which is the default cache factor
  95. # for all caches if a specific factor for that cache is not otherwise
  96. # set.
  97. #
  98. # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment
  99. # variable. Setting by environment variable takes priority over
  100. # setting through the config file.
  101. #
  102. # Defaults to 0.5, which will half the size of all caches.
  103. #
  104. #global_factor: 1.0
  105. # A dictionary of cache name to cache factor for that individual
  106. # cache. Overrides the global cache factor for a given cache.
  107. #
  108. # These can also be set through environment variables comprised
  109. # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital
  110. # letters and underscores. Setting by environment variable
  111. # takes priority over setting through the config file.
  112. # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0
  113. #
  114. # Some caches have '*' and other characters that are not
  115. # alphanumeric or underscores. These caches can be named with or
  116. # without the special characters stripped. For example, to specify
  117. # the cache factor for `*stateGroupCache*` via an environment
  118. # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`.
  119. #
  120. per_cache_factors:
  121. #get_users_who_share_room_with_user: 2.0
  122. # Controls whether cache entries are evicted after a specified time
  123. # period. Defaults to true. Uncomment to disable this feature.
  124. #
  125. #expire_caches: false
  126. # If expire_caches is enabled, this flag controls how long an entry can
  127. # be in a cache without having been accessed before being evicted.
  128. # Defaults to 30m. Uncomment to set a different time to live for cache entries.
  129. #
  130. #cache_entry_ttl: 30m
  131. # Controls how long the results of a /sync request are cached for after
  132. # a successful response is returned. A higher duration can help clients with
  133. # intermittent connections, at the cost of higher memory usage.
  134. #
  135. # By default, this is zero, which means that sync responses are not cached
  136. # at all.
  137. #
  138. #sync_response_cache_duration: 2m
  139. """
  140. def read_config(self, config, **kwargs) -> None:
  141. self.event_cache_size = self.parse_size(
  142. config.get("event_cache_size", _DEFAULT_EVENT_CACHE_SIZE)
  143. )
  144. self.cache_factors: Dict[str, float] = {}
  145. cache_config = config.get("caches") or {}
  146. self.global_factor = cache_config.get(
  147. "global_factor", properties.default_factor_size
  148. )
  149. if not isinstance(self.global_factor, (int, float)):
  150. raise ConfigError("caches.global_factor must be a number.")
  151. # Set the global one so that it's reflected in new caches
  152. properties.default_factor_size = self.global_factor
  153. # Load cache factors from the config
  154. individual_factors = cache_config.get("per_cache_factors") or {}
  155. if not isinstance(individual_factors, dict):
  156. raise ConfigError("caches.per_cache_factors must be a dictionary")
  157. # Canonicalise the cache names *before* updating with the environment
  158. # variables.
  159. individual_factors = {
  160. _canonicalise_cache_name(key): val
  161. for key, val in individual_factors.items()
  162. }
  163. # Override factors from environment if necessary
  164. individual_factors.update(
  165. {
  166. _canonicalise_cache_name(key[len(_CACHE_PREFIX) + 1 :]): float(val)
  167. for key, val in self._environ.items()
  168. if key.startswith(_CACHE_PREFIX + "_")
  169. }
  170. )
  171. for cache, factor in individual_factors.items():
  172. if not isinstance(factor, (int, float)):
  173. raise ConfigError(
  174. "caches.per_cache_factors.%s must be a number" % (cache,)
  175. )
  176. self.cache_factors[cache] = factor
  177. self.track_memory_usage = cache_config.get("track_memory_usage", False)
  178. if self.track_memory_usage:
  179. try:
  180. check_requirements("cache_memory")
  181. except DependencyException as e:
  182. raise ConfigError(
  183. e.message # noqa: B306, DependencyException.message is a property
  184. )
  185. expire_caches = cache_config.get("expire_caches", True)
  186. cache_entry_ttl = cache_config.get("cache_entry_ttl", "30m")
  187. if expire_caches:
  188. self.expiry_time_msec: Optional[int] = self.parse_duration(cache_entry_ttl)
  189. else:
  190. self.expiry_time_msec = None
  191. # Backwards compatibility support for the now-removed "expiry_time" config flag.
  192. expiry_time = cache_config.get("expiry_time")
  193. if expiry_time and expire_caches:
  194. logger.warning(
  195. "You have set two incompatible options, expiry_time and expire_caches. Please only use the "
  196. "expire_caches and cache_entry_ttl options and delete the expiry_time option as it is "
  197. "deprecated."
  198. )
  199. if expiry_time:
  200. logger.warning(
  201. "Expiry_time is a deprecated option, please use the expire_caches and cache_entry_ttl options "
  202. "instead."
  203. )
  204. self.expiry_time_msec = self.parse_duration(expiry_time)
  205. self.sync_response_cache_duration = self.parse_duration(
  206. cache_config.get("sync_response_cache_duration", 0)
  207. )
  208. # Resize all caches (if necessary) with the new factors we've loaded
  209. self.resize_all_caches()
  210. # Store this function so that it can be called from other classes without
  211. # needing an instance of Config
  212. properties.resize_all_caches_func = self.resize_all_caches
  213. def resize_all_caches(self) -> None:
  214. """Ensure all cache sizes are up to date
  215. For each cache, run the mapped callback function with either
  216. a specific cache factor or the default, global one.
  217. """
  218. # block other threads from modifying _CACHES while we iterate it.
  219. with _CACHES_LOCK:
  220. for cache_name, callback in _CACHES.items():
  221. new_factor = self.cache_factors.get(cache_name, self.global_factor)
  222. callback(new_factor)