Repository: matrix-org/synapse

Issue:
Enable cache time-based expiry by default
The 'new' time-based expiry for caches in Synapse seems to have helped out with memory usage (empirically, based on what people are telling me).
It seems like it would be worth enabling by default so that people don't have to know about this obscure option to get the benefits.

Related: #11641 (it's not obvious that even though Synapse's memory usage only grows by default, it is capable of trimming back down).


Discussion:
I'd preach some caution here. We opted out of time-based expiry for a particular cache in #10826. I forget the details (cc @erikjohnston); I'm not sure if we ever convinced ourselves if that change helped?
I always advise people to do this - enabling it by default would probably help in the majority of cases.
We've been very happy with this on matrix.org for long enough to be confident that this is reasonable to turn on by default.

Resolving patch:
diff --git a/synapse/config/background_updates.py b/synapse/config/background_updates.py
new file mode 100644
diff --git a/synapse/config/cache.py b/synapse/config/cache.py
--- a/synapse/config/cache.py
+++ b/synapse/config/cache.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import logging
 import os
 import re
 import threading
@@ -23,6 +24,8 @@
 
 from ._base import Config, ConfigError
 
+logger = logging.getLogger(__name__)
+
 # The prefix for all cache factor-related environment variables
 _CACHE_PREFIX = "SYNAPSE_CACHE_FACTOR"
 
@@ -148,11 +151,16 @@ def generate_config_section(self, **kwargs) -> str:
           per_cache_factors:
             #get_users_who_share_room_with_user: 2.0
 
-          # Controls how long an entry can be in a cache without having been
-          # accessed before being evicted. Defaults to None, which means
-          # entries are never evicted based on time.
+          # Controls whether cache entries are evicted after a specified time
+          # period. Defaults to true. Uncomment to disable this feature.
+          #
+          #expire_caches: false
+
+          # If expire_caches is enabled, this flag controls how long an entry can
+          # be in a cache without having been accessed before being evicted.
+          # Defaults to 30m. Uncomment to set a different time to live for cache entries.
           #
-          #expiry_time: 30m
+          #cache_entry_ttl: 30m
 
           # Controls how long the results of a /sync request are cached for after
           # a successful response is returned. A higher duration can help clients with
@@ -217,12 +225,30 @@ def read_config(self, config, **kwargs) -> None:
                     e.message  # noqa: B306, DependencyException.message is a property
                 )
 
-        expiry_time = cache_config.get("expiry_time")
-        if expiry_time:
-            self.expiry_time_msec: Optional[int] = self.parse_duration(expiry_time)
+        expire_caches = cache_config.get("expire_caches", True)
+        cache_entry_ttl = cache_config.get("cache_entry_ttl", "30m")
+
+        if expire_caches:
+            self.expiry_time_msec: Optional[int] = self.parse_duration(cache_entry_ttl)
         else:
             self.expiry_time_msec = None
 
+        # Backwards compatibility support for the now-removed "expiry_time" config flag.
+        expiry_time = cache_config.get("expiry_time")
+
+        if expiry_time and expire_caches:
+            logger.warning(
+                "You have set two incompatible options, expiry_time and expire_caches. Please only use the "
+                "expire_caches and cache_entry_ttl options and delete the expiry_time option as it is "
+                "deprecated."
+            )
+        if expiry_time:
+            logger.warning(
+                "Expiry_time is a deprecated option, please use the expire_caches and cache_entry_ttl options "
+                "instead."
+            )
+            self.expiry_time_msec = self.parse_duration(expiry_time)
+
         self.sync_response_cache_duration = self.parse_duration(
             cache_config.get("sync_response_cache_duration", 0)
         )


Test patch: