Skip to content

Commit 6facf98

Browse files
Be mindful of other SIGHUP handlers in 3rd-party code (#19095)
Be mindful that Synapse can be run alongside other code in the same Python process. We shouldn't clobber other `SIGHUP` handlers as only one can be set at time. (no clobber) ### Background As part of Element's plan to support a light form of vhosting (virtual host) (multiple instances of Synapse in the same Python process), we're currently diving into the details and implications of running multiple instances of Synapse in the same Python process. "Per-tenant logging" tracked internally by element-hq/synapse-small-hosts#48 Relevant to logging as we use a `SIGHUP` to reload log config in Synapse.
1 parent 0417296 commit 6facf98

File tree

3 files changed

+74
-49
lines changed

3 files changed

+74
-49
lines changed

changelog.d/19095.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid clobbering other `SIGHUP` handlers in 3rd-party code.

synapse/app/_base.py

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
import warnings
3030
from textwrap import indent
3131
from threading import Thread
32+
from types import FrameType
3233
from typing import (
3334
TYPE_CHECKING,
3435
Any,
3536
Awaitable,
3637
Callable,
3738
NoReturn,
3839
Optional,
40+
Union,
3941
cast,
4042
)
4143
from wsgiref.simple_server import WSGIServer
@@ -72,7 +74,6 @@
7274
from synapse.http.site import SynapseSite
7375
from synapse.logging.context import LoggingContext, PreserveLoggingContext
7476
from synapse.metrics import install_gc_manager, register_threadpool
75-
from synapse.metrics.background_process_metrics import run_as_background_process
7677
from synapse.metrics.jemalloc import setup_jemalloc_stats
7778
from synapse.module_api.callbacks.spamchecker_callbacks import load_legacy_spam_checkers
7879
from synapse.module_api.callbacks.third_party_event_rules_callbacks import (
@@ -108,7 +109,7 @@
108109

109110

110111
def register_sighup(
111-
homeserver_instance_id: str,
112+
hs: "HomeServer",
112113
func: Callable[P, None],
113114
*args: P.args,
114115
**kwargs: P.kwargs,
@@ -123,19 +124,25 @@ def register_sighup(
123124
*args, **kwargs: args and kwargs to be passed to the target function.
124125
"""
125126

126-
_instance_id_to_sighup_callbacks_map.setdefault(homeserver_instance_id, []).append(
127-
(func, args, kwargs)
127+
# Wrap the function so we can run it within a logcontext
128+
def _callback_wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
129+
with LoggingContext(name="sighup", server_name=hs.hostname):
130+
func(*args, **kwargs)
131+
132+
_instance_id_to_sighup_callbacks_map.setdefault(hs.get_instance_id(), []).append(
133+
(_callback_wrapper, args, kwargs)
128134
)
129135

130136

131-
def unregister_sighups(instance_id: str) -> None:
137+
def unregister_sighups(homeserver_instance_id: str) -> None:
132138
"""
133139
Unregister all sighup functions associated with this Synapse instance.
134140
135141
Args:
136-
instance_id: Unique ID for this Synapse process instance.
142+
homeserver_instance_id: The unique ID for this Synapse process instance to
143+
unregister hooks for (`hs.get_instance_id()`).
137144
"""
138-
_instance_id_to_sighup_callbacks_map.pop(instance_id, [])
145+
_instance_id_to_sighup_callbacks_map.pop(homeserver_instance_id, [])
139146

140147

141148
def start_worker_reactor(
@@ -540,6 +547,61 @@ def refresh_certificate(hs: "HomeServer") -> None:
540547
logger.info("Context factories updated.")
541548

542549

550+
_already_setup_sighup_handling = False
551+
"""
552+
Marks whether we've already successfully ran `setup_sighup_handling()`.
553+
"""
554+
555+
556+
def setup_sighup_handling() -> None:
557+
"""
558+
Set up SIGHUP handling to call registered callbacks.
559+
560+
This can be called multiple times safely.
561+
"""
562+
global _already_setup_sighup_handling
563+
# We only need to set things up once per process.
564+
if _already_setup_sighup_handling:
565+
return
566+
567+
previous_sighup_handler: Union[
568+
Callable[[int, Optional[FrameType]], Any], int, None
569+
] = None
570+
571+
# Set up the SIGHUP machinery.
572+
if hasattr(signal, "SIGHUP"):
573+
574+
def handle_sighup(*args: Any, **kwargs: Any) -> None:
575+
# Tell systemd our state, if we're using it. This will silently fail if
576+
# we're not using systemd.
577+
sdnotify(b"RELOADING=1")
578+
579+
if callable(previous_sighup_handler):
580+
previous_sighup_handler(*args, **kwargs)
581+
582+
for sighup_callbacks in _instance_id_to_sighup_callbacks_map.values():
583+
for func, args, kwargs in sighup_callbacks:
584+
func(*args, **kwargs)
585+
586+
sdnotify(b"READY=1")
587+
588+
# We defer running the sighup handlers until next reactor tick. This
589+
# is so that we're in a sane state, e.g. flushing the logs may fail
590+
# if the sighup happens in the middle of writing a log entry.
591+
def run_sighup(*args: Any, **kwargs: Any) -> None:
592+
# `callFromThread` should be "signal safe" as well as thread
593+
# safe.
594+
reactor.callFromThread(handle_sighup, *args, **kwargs)
595+
596+
# Register for the SIGHUP signal, chaining any existing handler as there can
597+
# only be one handler per signal and we don't want to clobber any existing
598+
# handlers (like the `multi_synapse` shard process in the context of Synapse Pro
599+
# for small hosts)
600+
previous_sighup_handler = signal.signal(signal.SIGHUP, run_sighup)
601+
602+
_already_setup_sighup_handling = True
603+
604+
543605
async def start(hs: "HomeServer", freeze: bool = True) -> None:
544606
"""
545607
Start a Synapse server or worker.
@@ -579,45 +641,9 @@ async def start(hs: "HomeServer", freeze: bool = True) -> None:
579641
name="gai_resolver", server_name=server_name, threadpool=resolver_threadpool
580642
)
581643

582-
# Set up the SIGHUP machinery.
583-
if hasattr(signal, "SIGHUP"):
584-
585-
def handle_sighup(*args: Any, **kwargs: Any) -> "defer.Deferred[None]":
586-
async def _handle_sighup(*args: Any, **kwargs: Any) -> None:
587-
# Tell systemd our state, if we're using it. This will silently fail if
588-
# we're not using systemd.
589-
sdnotify(b"RELOADING=1")
590-
591-
for sighup_callbacks in _instance_id_to_sighup_callbacks_map.values():
592-
for func, args, kwargs in sighup_callbacks:
593-
func(*args, **kwargs)
594-
595-
sdnotify(b"READY=1")
596-
597-
# It's okay to ignore the linter error here and call
598-
# `run_as_background_process` directly because `_handle_sighup` operates
599-
# outside of the scope of a specific `HomeServer` instance and holds no
600-
# references to it which would prevent a clean shutdown.
601-
return run_as_background_process( # type: ignore[untracked-background-process]
602-
"sighup",
603-
server_name,
604-
_handle_sighup,
605-
*args,
606-
**kwargs,
607-
)
608-
609-
# We defer running the sighup handlers until next reactor tick. This
610-
# is so that we're in a sane state, e.g. flushing the logs may fail
611-
# if the sighup happens in the middle of writing a log entry.
612-
def run_sighup(*args: Any, **kwargs: Any) -> None:
613-
# `callFromThread` should be "signal safe" as well as thread
614-
# safe.
615-
reactor.callFromThread(handle_sighup, *args, **kwargs)
616-
617-
signal.signal(signal.SIGHUP, run_sighup)
618-
619-
register_sighup(hs.get_instance_id(), refresh_certificate, hs)
620-
register_sighup(hs.get_instance_id(), reload_cache_config, hs.config)
644+
setup_sighup_handling()
645+
register_sighup(hs, refresh_certificate, hs)
646+
register_sighup(hs, reload_cache_config, hs.config)
621647

622648
# Apply the cache config.
623649
hs.config.caches.resize_all_caches()

synapse/config/logger.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,7 @@ def setup_logging(
345345
# Add a SIGHUP handler to reload the logging configuration, if one is available.
346346
from synapse.app import _base as appbase
347347

348-
appbase.register_sighup(
349-
hs.get_instance_id(), _reload_logging_config, log_config_path
350-
)
348+
appbase.register_sighup(hs, _reload_logging_config, log_config_path)
351349

352350
# Log immediately so we can grep backwards.
353351
logger.warning("***** STARTING SERVER *****")

0 commit comments

Comments
 (0)