cache.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2019 Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import os
  16. import re
  17. import threading
  18. from typing import Callable, Dict
  19. from ._base import Config, ConfigError
  20. # The prefix for all cache factor-related environment variables
  21. _CACHE_PREFIX = "SYNAPSE_CACHE_FACTOR"
  22. # Map from canonicalised cache name to cache.
  23. _CACHES = {}
  24. # a lock on the contents of _CACHES
  25. _CACHES_LOCK = threading.Lock()
  26. _DEFAULT_FACTOR_SIZE = 0.5
  27. _DEFAULT_EVENT_CACHE_SIZE = "10K"
  28. class CacheProperties:
  29. def __init__(self):
  30. # The default factor size for all caches
  31. self.default_factor_size = float(
  32. os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE)
  33. )
  34. self.resize_all_caches_func = None
  35. properties = CacheProperties()
  36. def _canonicalise_cache_name(cache_name: str) -> str:
  37. """Gets the canonical form of the cache name.
  38. Since we specify cache names in config and environment variables we need to
  39. ignore case and special characters. For example, some caches have asterisks
  40. in their name to denote that they're not attached to a particular database
  41. function, and these asterisks need to be stripped out
  42. """
  43. cache_name = re.sub(r"[^A-Za-z_1-9]", "", cache_name)
  44. return cache_name.lower()
  45. def add_resizable_cache(cache_name: str, cache_resize_callback: Callable):
  46. """Register a cache that's size can dynamically change
  47. Args:
  48. cache_name: A reference to the cache
  49. cache_resize_callback: A callback function that will be ran whenever
  50. the cache needs to be resized
  51. """
  52. # Some caches have '*' in them which we strip out.
  53. cache_name = _canonicalise_cache_name(cache_name)
  54. # sometimes caches are initialised from background threads, so we need to make
  55. # sure we don't conflict with another thread running a resize operation
  56. with _CACHES_LOCK:
  57. _CACHES[cache_name] = cache_resize_callback
  58. # Ensure all loaded caches are sized appropriately
  59. #
  60. # This method should only run once the config has been read,
  61. # as it uses values read from it
  62. if properties.resize_all_caches_func:
  63. properties.resize_all_caches_func()
  64. class CacheConfig(Config):
  65. section = "caches"
  66. _environ = os.environ
  67. @staticmethod
  68. def reset():
  69. """Resets the caches to their defaults. Used for tests."""
  70. properties.default_factor_size = float(
  71. os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE)
  72. )
  73. properties.resize_all_caches_func = None
  74. with _CACHES_LOCK:
  75. _CACHES.clear()
  76. def generate_config_section(self, **kwargs):
  77. return """\
  78. ## Caching ##
  79. # Caching can be configured through the following options.
  80. #
  81. # A cache 'factor' is a multiplier that can be applied to each of
  82. # Synapse's caches in order to increase or decrease the maximum
  83. # number of entries that can be stored.
  84. # The number of events to cache in memory. Not affected by
  85. # caches.global_factor.
  86. #
  87. #event_cache_size: 10K
  88. caches:
  89. # Controls the global cache factor, which is the default cache factor
  90. # for all caches if a specific factor for that cache is not otherwise
  91. # set.
  92. #
  93. # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment
  94. # variable. Setting by environment variable takes priority over
  95. # setting through the config file.
  96. #
  97. # Defaults to 0.5, which will half the size of all caches.
  98. #
  99. #global_factor: 1.0
  100. # A dictionary of cache name to cache factor for that individual
  101. # cache. Overrides the global cache factor for a given cache.
  102. #
  103. # These can also be set through environment variables comprised
  104. # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital
  105. # letters and underscores. Setting by environment variable
  106. # takes priority over setting through the config file.
  107. # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0
  108. #
  109. # Some caches have '*' and other characters that are not
  110. # alphanumeric or underscores. These caches can be named with or
  111. # without the special characters stripped. For example, to specify
  112. # the cache factor for `*stateGroupCache*` via an environment
  113. # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`.
  114. #
  115. per_cache_factors:
  116. #get_users_who_share_room_with_user: 2.0
  117. """
  118. def read_config(self, config, **kwargs):
  119. self.event_cache_size = self.parse_size(
  120. config.get("event_cache_size", _DEFAULT_EVENT_CACHE_SIZE)
  121. )
  122. self.cache_factors = {} # type: Dict[str, float]
  123. cache_config = config.get("caches") or {}
  124. self.global_factor = cache_config.get(
  125. "global_factor", properties.default_factor_size
  126. )
  127. if not isinstance(self.global_factor, (int, float)):
  128. raise ConfigError("caches.global_factor must be a number.")
  129. # Set the global one so that it's reflected in new caches
  130. properties.default_factor_size = self.global_factor
  131. # Load cache factors from the config
  132. individual_factors = cache_config.get("per_cache_factors") or {}
  133. if not isinstance(individual_factors, dict):
  134. raise ConfigError("caches.per_cache_factors must be a dictionary")
  135. # Canonicalise the cache names *before* updating with the environment
  136. # variables.
  137. individual_factors = {
  138. _canonicalise_cache_name(key): val
  139. for key, val in individual_factors.items()
  140. }
  141. # Override factors from environment if necessary
  142. individual_factors.update(
  143. {
  144. _canonicalise_cache_name(key[len(_CACHE_PREFIX) + 1 :]): float(val)
  145. for key, val in self._environ.items()
  146. if key.startswith(_CACHE_PREFIX + "_")
  147. }
  148. )
  149. for cache, factor in individual_factors.items():
  150. if not isinstance(factor, (int, float)):
  151. raise ConfigError(
  152. "caches.per_cache_factors.%s must be a number" % (cache,)
  153. )
  154. self.cache_factors[cache] = factor
  155. # Resize all caches (if necessary) with the new factors we've loaded
  156. self.resize_all_caches()
  157. # Store this function so that it can be called from other classes without
  158. # needing an instance of Config
  159. properties.resize_all_caches_func = self.resize_all_caches
  160. def resize_all_caches(self):
  161. """Ensure all cache sizes are up to date
  162. For each cache, run the mapped callback function with either
  163. a specific cache factor or the default, global one.
  164. """
  165. # block other threads from modifying _CACHES while we iterate it.
  166. with _CACHES_LOCK:
  167. for cache_name, callback in _CACHES.items():
  168. new_factor = self.cache_factors.get(cache_name, self.global_factor)
  169. callback(new_factor)