Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit d38d242

Browse files
author
David Robertson
authored
Reload cache factors from disk on SIGHUP (#12673)
1 parent a559c8b commit d38d242

File tree

11 files changed

+199
-61
lines changed

11 files changed

+199
-61
lines changed

Diff for: changelog.d/12673.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Synapse will now reload [cache config](https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html#caching) when it receives a [SIGHUP](https://en.wikipedia.org/wiki/SIGHUP) signal.

Diff for: docs/sample_config.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,12 @@ retention:
730730
# A cache 'factor' is a multiplier that can be applied to each of
731731
# Synapse's caches in order to increase or decrease the maximum
732732
# number of entries that can be stored.
733+
#
734+
# The configuration for cache factors (caches.global_factor and
735+
# caches.per_cache_factors) can be reloaded while the application is running,
736+
# by sending a SIGHUP signal to the Synapse process. Changes to other parts of
737+
# the caching config will NOT be applied after a SIGHUP is received; a restart
738+
# is necessary.
733739

734740
# The number of events to cache in memory. Not affected by
735741
# caches.global_factor.

Diff for: docs/usage/configuration/config_documentation.md

+17
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,23 @@ caches:
11301130
expire_caches: false
11311131
sync_response_cache_duration: 2m
11321132
```
1133+
1134+
### Reloading cache factors
1135+
1136+
The cache factors (i.e. `caches.global_factor` and `caches.per_cache_factors`) may be reloaded at any time by sending a
1137+
[`SIGHUP`](https://en.wikipedia.org/wiki/SIGHUP) signal to Synapse using e.g.
1138+
1139+
```commandline
1140+
kill -HUP [PID_OF_SYNAPSE_PROCESS]
1141+
```
1142+
1143+
If you are running multiple workers, you must individually update the worker
1144+
config file and send this signal to each worker process.
1145+
1146+
If you're using the [example systemd service](https://github.com/matrix-org/synapse/blob/develop/contrib/systemd/matrix-synapse.service)
1147+
file in Synapse's `contrib` directory, you can send a `SIGHUP` signal by using
1148+
`systemctl reload matrix-synapse`.
1149+
11331150
---
11341151
## Database ##
11351152
Config options related to database settings.

Diff for: synapse/app/_base.py

+44
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@
4949
from twisted.protocols.tls import TLSMemoryBIOFactory
5050
from twisted.python.threadpool import ThreadPool
5151

52+
import synapse.util.caches
5253
from synapse.api.constants import MAX_PDU_SIZE
5354
from synapse.app import check_bind_error
5455
from synapse.app.phone_stats_home import start_phone_stats_home
56+
from synapse.config import ConfigError
57+
from synapse.config._base import format_config_error
5558
from synapse.config.homeserver import HomeServerConfig
5659
from synapse.config.server import ManholeConfig
5760
from synapse.crypto import context_factory
@@ -432,6 +435,10 @@ def run_sighup(*args: Any, **kwargs: Any) -> None:
432435
signal.signal(signal.SIGHUP, run_sighup)
433436

434437
register_sighup(refresh_certificate, hs)
438+
register_sighup(reload_cache_config, hs.config)
439+
440+
# Apply the cache config.
441+
hs.config.caches.resize_all_caches()
435442

436443
# Load the certificate from disk.
437444
refresh_certificate(hs)
@@ -486,6 +493,43 @@ def run_sighup(*args: Any, **kwargs: Any) -> None:
486493
atexit.register(gc.freeze)
487494

488495

496+
def reload_cache_config(config: HomeServerConfig) -> None:
497+
"""Reload cache config from disk and immediately apply it.resize caches accordingly.
498+
499+
If the config is invalid, a `ConfigError` is logged and no changes are made.
500+
501+
Otherwise, this:
502+
- replaces the `caches` section on the given `config` object,
503+
- resizes all caches according to the new cache factors, and
504+
505+
Note that the following cache config keys are read, but not applied:
506+
- event_cache_size: used to set a max_size and _original_max_size on
507+
EventsWorkerStore._get_event_cache when it is created. We'd have to update
508+
the _original_max_size (and maybe
509+
- sync_response_cache_duration: would have to update the timeout_sec attribute on
510+
HomeServer -> SyncHandler -> ResponseCache.
511+
- track_memory_usage. This affects synapse.util.caches.TRACK_MEMORY_USAGE which
512+
influences Synapse's self-reported metrics.
513+
514+
Also, the HTTPConnectionPool in SimpleHTTPClient sets its maxPersistentPerHost
515+
parameter based on the global_factor. This won't be applied on a config reload.
516+
"""
517+
try:
518+
previous_cache_config = config.reload_config_section("caches")
519+
except ConfigError as e:
520+
logger.warning("Failed to reload cache config")
521+
for f in format_config_error(e):
522+
logger.warning(f)
523+
else:
524+
logger.debug(
525+
"New cache config. Was:\n %s\nNow:\n",
526+
previous_cache_config.__dict__,
527+
config.caches.__dict__,
528+
)
529+
synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage
530+
config.caches.resize_all_caches()
531+
532+
489533
def setup_sentry(hs: "HomeServer") -> None:
490534
"""Enable sentry integration, if enabled in configuration"""
491535

Diff for: synapse/app/homeserver.py

+2-34
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
import os
1818
import sys
19-
from typing import Dict, Iterable, Iterator, List
19+
from typing import Dict, Iterable, List
2020

2121
from matrix_common.versionstring import get_distribution_version_string
2222

@@ -45,7 +45,7 @@
4545
redirect_stdio_to_logs,
4646
register_start,
4747
)
48-
from synapse.config._base import ConfigError
48+
from synapse.config._base import ConfigError, format_config_error
4949
from synapse.config.emailconfig import ThreepidBehaviour
5050
from synapse.config.homeserver import HomeServerConfig
5151
from synapse.config.server import ListenerConfig
@@ -399,38 +399,6 @@ async def start() -> None:
399399
return hs
400400

401401

402-
def format_config_error(e: ConfigError) -> Iterator[str]:
403-
"""
404-
Formats a config error neatly
405-
406-
The idea is to format the immediate error, plus the "causes" of those errors,
407-
hopefully in a way that makes sense to the user. For example:
408-
409-
Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template':
410-
Failed to parse config for module 'JinjaOidcMappingProvider':
411-
invalid jinja template:
412-
unexpected end of template, expected 'end of print statement'.
413-
414-
Args:
415-
e: the error to be formatted
416-
417-
Returns: An iterator which yields string fragments to be formatted
418-
"""
419-
yield "Error in configuration"
420-
421-
if e.path:
422-
yield " at '%s'" % (".".join(e.path),)
423-
424-
yield ":\n %s" % (e.msg,)
425-
426-
parent_e = e.__cause__
427-
indent = 1
428-
while parent_e:
429-
indent += 1
430-
yield ":\n%s%s" % (" " * indent, str(parent_e))
431-
parent_e = parent_e.__cause__
432-
433-
434402
def run(hs: HomeServer) -> None:
435403
_base.start_reactor(
436404
"synapse-homeserver",

Diff for: synapse/config/_base.py

+74-7
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616

1717
import argparse
1818
import errno
19+
import logging
1920
import os
2021
from collections import OrderedDict
2122
from hashlib import sha256
2223
from textwrap import dedent
2324
from typing import (
2425
Any,
26+
ClassVar,
27+
Collection,
2528
Dict,
2629
Iterable,
30+
Iterator,
2731
List,
2832
MutableMapping,
2933
Optional,
@@ -40,6 +44,8 @@
4044

4145
from synapse.util.templates import _create_mxc_to_http_filter, _format_ts_filter
4246

47+
logger = logging.getLogger(__name__)
48+
4349

4450
class ConfigError(Exception):
4551
"""Represents a problem parsing the configuration
@@ -55,6 +61,38 @@ def __init__(self, msg: str, path: Optional[Iterable[str]] = None):
5561
self.path = path
5662

5763

64+
def format_config_error(e: ConfigError) -> Iterator[str]:
65+
"""
66+
Formats a config error neatly
67+
68+
The idea is to format the immediate error, plus the "causes" of those errors,
69+
hopefully in a way that makes sense to the user. For example:
70+
71+
Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template':
72+
Failed to parse config for module 'JinjaOidcMappingProvider':
73+
invalid jinja template:
74+
unexpected end of template, expected 'end of print statement'.
75+
76+
Args:
77+
e: the error to be formatted
78+
79+
Returns: An iterator which yields string fragments to be formatted
80+
"""
81+
yield "Error in configuration"
82+
83+
if e.path:
84+
yield " at '%s'" % (".".join(e.path),)
85+
86+
yield ":\n %s" % (e.msg,)
87+
88+
parent_e = e.__cause__
89+
indent = 1
90+
while parent_e:
91+
indent += 1
92+
yield ":\n%s%s" % (" " * indent, str(parent_e))
93+
parent_e = parent_e.__cause__
94+
95+
5896
# We split these messages out to allow packages to override with package
5997
# specific instructions.
6098
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
@@ -119,7 +157,7 @@ class Config:
119157
defined in subclasses.
120158
"""
121159

122-
section: str
160+
section: ClassVar[str]
123161

124162
def __init__(self, root_config: "RootConfig" = None):
125163
self.root = root_config
@@ -309,9 +347,12 @@ class RootConfig:
309347
class, lower-cased and with "Config" removed.
310348
"""
311349

312-
config_classes = []
350+
config_classes: List[Type[Config]] = []
351+
352+
def __init__(self, config_files: Collection[str] = ()):
353+
# Capture absolute paths here, so we can reload config after we daemonize.
354+
self.config_files = [os.path.abspath(path) for path in config_files]
313355

314-
def __init__(self):
315356
for config_class in self.config_classes:
316357
if config_class.section is None:
317358
raise ValueError("%r requires a section name" % (config_class,))
@@ -512,12 +553,10 @@ def load_config_with_parser(
512553
object from parser.parse_args(..)`
513554
"""
514555

515-
obj = cls()
516-
517556
config_args = parser.parse_args(argv)
518557

519558
config_files = find_config_files(search_paths=config_args.config_path)
520-
559+
obj = cls(config_files)
521560
if not config_files:
522561
parser.error("Must supply a config file.")
523562

@@ -627,7 +666,7 @@ def load_or_generate_config(
627666

628667
generate_missing_configs = config_args.generate_missing_configs
629668

630-
obj = cls()
669+
obj = cls(config_files)
631670

632671
if config_args.generate_config:
633672
if config_args.report_stats is None:
@@ -727,6 +766,34 @@ def generate_missing_files(
727766
) -> None:
728767
self.invoke_all("generate_files", config_dict, config_dir_path)
729768

769+
def reload_config_section(self, section_name: str) -> Config:
770+
"""Reconstruct the given config section, leaving all others unchanged.
771+
772+
This works in three steps:
773+
774+
1. Create a new instance of the relevant `Config` subclass.
775+
2. Call `read_config` on that instance to parse the new config.
776+
3. Replace the existing config instance with the new one.
777+
778+
:raises ValueError: if the given `section` does not exist.
779+
:raises ConfigError: for any other problems reloading config.
780+
781+
:returns: the previous config object, which no longer has a reference to this
782+
RootConfig.
783+
"""
784+
existing_config: Optional[Config] = getattr(self, section_name, None)
785+
if existing_config is None:
786+
raise ValueError(f"Unknown config section '{section_name}'")
787+
logger.info("Reloading config section '%s'", section_name)
788+
789+
new_config_data = read_config_files(self.config_files)
790+
new_config = type(existing_config)(self)
791+
new_config.read_config(new_config_data)
792+
setattr(self, section_name, new_config)
793+
794+
existing_config.root = None
795+
return existing_config
796+
730797

731798
def read_config_files(config_files: Iterable[str]) -> Dict[str, Any]:
732799
"""Read the config files into a dict

Diff for: synapse/config/_base.pyi

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import argparse
22
from typing import (
33
Any,
4+
Collection,
45
Dict,
56
Iterable,
7+
Iterator,
68
List,
9+
Literal,
710
MutableMapping,
811
Optional,
912
Tuple,
1013
Type,
1114
TypeVar,
1215
Union,
16+
overload,
1317
)
1418

1519
import jinja2
@@ -64,6 +68,8 @@ class ConfigError(Exception):
6468
self.msg = msg
6569
self.path = path
6670

71+
def format_config_error(e: ConfigError) -> Iterator[str]: ...
72+
6773
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS: str
6874
MISSING_REPORT_STATS_SPIEL: str
6975
MISSING_SERVER_NAME: str
@@ -117,7 +123,8 @@ class RootConfig:
117123
background_updates: background_updates.BackgroundUpdateConfig
118124

119125
config_classes: List[Type["Config"]] = ...
120-
def __init__(self) -> None: ...
126+
config_files: List[str]
127+
def __init__(self, config_files: Collection[str] = ...) -> None: ...
121128
def invoke_all(
122129
self, func_name: str, *args: Any, **kwargs: Any
123130
) -> MutableMapping[str, Any]: ...
@@ -157,6 +164,12 @@ class RootConfig:
157164
def generate_missing_files(
158165
self, config_dict: dict, config_dir_path: str
159166
) -> None: ...
167+
@overload
168+
def reload_config_section(
169+
self, section_name: Literal["caches"]
170+
) -> cache.CacheConfig: ...
171+
@overload
172+
def reload_config_section(self, section_name: str) -> Config: ...
160173

161174
class Config:
162175
root: RootConfig

0 commit comments

Comments
 (0)