Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

423 lignes
17 KiB

  1. # Copyright 2016 OpenMarket Ltd
  2. # Copyright 2021 The 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 argparse
  16. import logging
  17. from typing import Any, Dict, List, Union
  18. import attr
  19. from synapse.types import JsonDict
  20. from ._base import (
  21. Config,
  22. ConfigError,
  23. RoutableShardedWorkerHandlingConfig,
  24. ShardedWorkerHandlingConfig,
  25. )
  26. from .server import ListenerConfig, parse_listener_def
  27. _FEDERATION_SENDER_WITH_SEND_FEDERATION_ENABLED_ERROR = """
  28. The send_federation config option must be disabled in the main
  29. synapse process before they can be run in a separate worker.
  30. Please add ``send_federation: false`` to the main config
  31. """
  32. _PUSHER_WITH_START_PUSHERS_ENABLED_ERROR = """
  33. The start_pushers config option must be disabled in the main
  34. synapse process before they can be run in a separate worker.
  35. Please add ``start_pushers: false`` to the main config
  36. """
  37. _DEPRECATED_WORKER_DUTY_OPTION_USED = """
  38. The '%s' configuration option is deprecated and will be removed in a future
  39. Synapse version. Please use ``%s: name_of_worker`` instead.
  40. """
  41. logger = logging.getLogger(__name__)
  42. def _instance_to_list_converter(obj: Union[str, List[str]]) -> List[str]:
  43. """Helper for allowing parsing a string or list of strings to a config
  44. option expecting a list of strings.
  45. """
  46. if isinstance(obj, str):
  47. return [obj]
  48. return obj
  49. @attr.s(auto_attribs=True)
  50. class InstanceLocationConfig:
  51. """The host and port to talk to an instance via HTTP replication."""
  52. host: str
  53. port: int
  54. @attr.s
  55. class WriterLocations:
  56. """Specifies the instances that write various streams.
  57. Attributes:
  58. events: The instances that write to the event and backfill streams.
  59. typing: The instances that write to the typing stream. Currently
  60. can only be a single instance.
  61. to_device: The instances that write to the to_device stream. Currently
  62. can only be a single instance.
  63. account_data: The instances that write to the account data streams. Currently
  64. can only be a single instance.
  65. receipts: The instances that write to the receipts stream. Currently
  66. can only be a single instance.
  67. presence: The instances that write to the presence stream. Currently
  68. can only be a single instance.
  69. """
  70. events: List[str] = attr.ib(
  71. default=["master"],
  72. converter=_instance_to_list_converter,
  73. )
  74. typing: List[str] = attr.ib(
  75. default=["master"],
  76. converter=_instance_to_list_converter,
  77. )
  78. to_device: List[str] = attr.ib(
  79. default=["master"],
  80. converter=_instance_to_list_converter,
  81. )
  82. account_data: List[str] = attr.ib(
  83. default=["master"],
  84. converter=_instance_to_list_converter,
  85. )
  86. receipts: List[str] = attr.ib(
  87. default=["master"],
  88. converter=_instance_to_list_converter,
  89. )
  90. presence: List[str] = attr.ib(
  91. default=["master"],
  92. converter=_instance_to_list_converter,
  93. )
  94. class WorkerConfig(Config):
  95. """The workers are processes run separately to the main synapse process.
  96. They have their own pid_file and listener configuration. They use the
  97. replication_url to talk to the main synapse process."""
  98. section = "worker"
  99. def read_config(self, config: JsonDict, **kwargs: Any) -> None:
  100. self.worker_app = config.get("worker_app")
  101. # Canonicalise worker_app so that master always has None
  102. if self.worker_app == "synapse.app.homeserver":
  103. self.worker_app = None
  104. self.worker_listeners = [
  105. parse_listener_def(x) for x in config.get("worker_listeners", [])
  106. ]
  107. self.worker_daemonize = bool(config.get("worker_daemonize"))
  108. self.worker_pid_file = config.get("worker_pid_file")
  109. worker_log_config = config.get("worker_log_config")
  110. if worker_log_config is not None and not isinstance(worker_log_config, str):
  111. raise ConfigError("worker_log_config must be a string")
  112. self.worker_log_config = worker_log_config
  113. # The host used to connect to the main synapse
  114. self.worker_replication_host = config.get("worker_replication_host", None)
  115. # The port on the main synapse for TCP replication
  116. self.worker_replication_port = config.get("worker_replication_port", None)
  117. # The port on the main synapse for HTTP replication endpoint
  118. self.worker_replication_http_port = config.get("worker_replication_http_port")
  119. # The shared secret used for authentication when connecting to the main synapse.
  120. self.worker_replication_secret = config.get("worker_replication_secret", None)
  121. self.worker_name = config.get("worker_name", self.worker_app)
  122. self.instance_name = self.worker_name or "master"
  123. self.worker_main_http_uri = config.get("worker_main_http_uri", None)
  124. # This option is really only here to support `--manhole` command line
  125. # argument.
  126. manhole = config.get("worker_manhole")
  127. if manhole:
  128. self.worker_listeners.append(
  129. ListenerConfig(
  130. port=manhole,
  131. bind_addresses=["127.0.0.1"],
  132. type="manhole",
  133. )
  134. )
  135. # Handle federation sender configuration.
  136. #
  137. # There are two ways of configuring which instances handle federation
  138. # sending:
  139. # 1. The old way where "send_federation" is set to false and running a
  140. # `synapse.app.federation_sender` worker app.
  141. # 2. Specifying the workers sending federation in
  142. # `federation_sender_instances`.
  143. #
  144. send_federation = config.get("send_federation", True)
  145. federation_sender_instances = config.get("federation_sender_instances")
  146. if federation_sender_instances is None:
  147. # Default to an empty list, which means "another, unknown, worker is
  148. # responsible for it".
  149. federation_sender_instances = []
  150. # If no federation sender instances are set we check if
  151. # `send_federation` is set, which means use master
  152. if send_federation:
  153. federation_sender_instances = ["master"]
  154. if self.worker_app == "synapse.app.federation_sender":
  155. if send_federation:
  156. # If we're running federation senders, and not using
  157. # `federation_sender_instances`, then we should have
  158. # explicitly set `send_federation` to false.
  159. raise ConfigError(
  160. _FEDERATION_SENDER_WITH_SEND_FEDERATION_ENABLED_ERROR
  161. )
  162. federation_sender_instances = [self.worker_name]
  163. self.send_federation = self.instance_name in federation_sender_instances
  164. self.federation_shard_config = ShardedWorkerHandlingConfig(
  165. federation_sender_instances
  166. )
  167. # A map from instance name to host/port of their HTTP replication endpoint.
  168. instance_map = config.get("instance_map") or {}
  169. self.instance_map = {
  170. name: InstanceLocationConfig(**c) for name, c in instance_map.items()
  171. }
  172. # Map from type of streams to source, c.f. WriterLocations.
  173. writers = config.get("stream_writers") or {}
  174. self.writers = WriterLocations(**writers)
  175. # Check that the configured writers for events and typing also appears in
  176. # `instance_map`.
  177. for stream in (
  178. "events",
  179. "typing",
  180. "to_device",
  181. "account_data",
  182. "receipts",
  183. "presence",
  184. ):
  185. instances = _instance_to_list_converter(getattr(self.writers, stream))
  186. for instance in instances:
  187. if instance != "master" and instance not in self.instance_map:
  188. raise ConfigError(
  189. "Instance %r is configured to write %s but does not appear in `instance_map` config."
  190. % (instance, stream)
  191. )
  192. if len(self.writers.typing) != 1:
  193. raise ConfigError(
  194. "Must only specify one instance to handle `typing` messages."
  195. )
  196. if len(self.writers.to_device) != 1:
  197. raise ConfigError(
  198. "Must only specify one instance to handle `to_device` messages."
  199. )
  200. if len(self.writers.account_data) != 1:
  201. raise ConfigError(
  202. "Must only specify one instance to handle `account_data` messages."
  203. )
  204. if len(self.writers.receipts) != 1:
  205. raise ConfigError(
  206. "Must only specify one instance to handle `receipts` messages."
  207. )
  208. if len(self.writers.events) == 0:
  209. raise ConfigError("Must specify at least one instance to handle `events`.")
  210. if len(self.writers.presence) != 1:
  211. raise ConfigError(
  212. "Must only specify one instance to handle `presence` messages."
  213. )
  214. self.events_shard_config = RoutableShardedWorkerHandlingConfig(
  215. self.writers.events
  216. )
  217. # Handle sharded push
  218. start_pushers = config.get("start_pushers", True)
  219. pusher_instances = config.get("pusher_instances")
  220. if pusher_instances is None:
  221. # Default to an empty list, which means "another, unknown, worker is
  222. # responsible for it".
  223. pusher_instances = []
  224. # If no pushers instances are set we check if `start_pushers` is
  225. # set, which means use master
  226. if start_pushers:
  227. pusher_instances = ["master"]
  228. if self.worker_app == "synapse.app.pusher":
  229. if start_pushers:
  230. # If we're running pushers, and not using
  231. # `pusher_instances`, then we should have explicitly set
  232. # `start_pushers` to false.
  233. raise ConfigError(_PUSHER_WITH_START_PUSHERS_ENABLED_ERROR)
  234. pusher_instances = [self.instance_name]
  235. self.start_pushers = self.instance_name in pusher_instances
  236. self.pusher_shard_config = ShardedWorkerHandlingConfig(pusher_instances)
  237. # Whether this worker should run background tasks or not.
  238. #
  239. # As a note for developers, the background tasks guarded by this should
  240. # be able to run on only a single instance (meaning that they don't
  241. # depend on any in-memory state of a particular worker).
  242. #
  243. # No effort is made to ensure only a single instance of these tasks is
  244. # running.
  245. background_tasks_instance = config.get("run_background_tasks_on") or "master"
  246. self.run_background_tasks = (
  247. self.worker_name is None and background_tasks_instance == "master"
  248. ) or self.worker_name == background_tasks_instance
  249. self.should_notify_appservices = self._should_this_worker_perform_duty(
  250. config,
  251. legacy_master_option_name="notify_appservices",
  252. legacy_worker_app_name="synapse.app.appservice",
  253. new_option_name="notify_appservices_from_worker",
  254. )
  255. self.should_update_user_directory = self._should_this_worker_perform_duty(
  256. config,
  257. legacy_master_option_name="update_user_directory",
  258. legacy_worker_app_name="synapse.app.user_dir",
  259. new_option_name="update_user_directory_from_worker",
  260. )
  261. def _should_this_worker_perform_duty(
  262. self,
  263. config: Dict[str, Any],
  264. legacy_master_option_name: str,
  265. legacy_worker_app_name: str,
  266. new_option_name: str,
  267. ) -> bool:
  268. """
  269. Figures out whether this worker should perform a certain duty.
  270. This function is temporary and is only to deal with the complexity
  271. of allowing old, transitional and new configurations all at once.
  272. Contradictions between the legacy and new part of a transitional configuration
  273. will lead to a ConfigError.
  274. Parameters:
  275. config: The config dictionary
  276. legacy_master_option_name: The name of a legacy option, whose value is boolean,
  277. specifying whether it's the master that should handle a certain duty.
  278. e.g. "notify_appservices"
  279. legacy_worker_app_name: The name of a legacy Synapse worker application
  280. that would traditionally perform this duty.
  281. e.g. "synapse.app.appservice"
  282. new_option_name: The name of the new option, whose value is the name of a
  283. designated worker to perform the duty.
  284. e.g. "notify_appservices_from_worker"
  285. """
  286. # None means 'unspecified'; True means 'run here' and False means
  287. # 'don't run here'.
  288. new_option_should_run_here = None
  289. if new_option_name in config:
  290. designated_worker = config[new_option_name] or "master"
  291. new_option_should_run_here = (
  292. designated_worker == "master" and self.worker_name is None
  293. ) or designated_worker == self.worker_name
  294. legacy_option_should_run_here = None
  295. if legacy_master_option_name in config:
  296. run_on_master = bool(config[legacy_master_option_name])
  297. legacy_option_should_run_here = (
  298. self.worker_name is None and run_on_master
  299. ) or (self.worker_app == legacy_worker_app_name and not run_on_master)
  300. # Suggest using the new option instead.
  301. logger.warning(
  302. _DEPRECATED_WORKER_DUTY_OPTION_USED,
  303. legacy_master_option_name,
  304. new_option_name,
  305. )
  306. if self.worker_app == legacy_worker_app_name and config.get(
  307. legacy_master_option_name, True
  308. ):
  309. # As an extra bit of complication, we need to check that the
  310. # specialised worker is only used if the legacy config says the
  311. # master isn't performing the duties.
  312. raise ConfigError(
  313. f"Cannot use deprecated worker app type '{legacy_worker_app_name}' whilst deprecated option '{legacy_master_option_name}' is not set to false.\n"
  314. f"Consider setting `worker_app: synapse.app.generic_worker` and using the '{new_option_name}' option instead.\n"
  315. f"The '{new_option_name}' option replaces '{legacy_master_option_name}'."
  316. )
  317. if new_option_should_run_here is None and legacy_option_should_run_here is None:
  318. # Neither option specified; the fallback behaviour is to run on the main process
  319. return self.worker_name is None
  320. if (
  321. new_option_should_run_here is not None
  322. and legacy_option_should_run_here is not None
  323. ):
  324. # Both options specified; ensure they match!
  325. if new_option_should_run_here != legacy_option_should_run_here:
  326. update_worker_type = (
  327. " and set worker_app: synapse.app.generic_worker"
  328. if self.worker_app == legacy_worker_app_name
  329. else ""
  330. )
  331. # If the values conflict, we suggest the admin removes the legacy option
  332. # for simplicity.
  333. raise ConfigError(
  334. f"Conflicting configuration options: {legacy_master_option_name} (legacy), {new_option_name} (new).\n"
  335. f"Suggestion: remove {legacy_master_option_name}{update_worker_type}.\n"
  336. )
  337. # We've already validated that these aren't conflicting; now just see if
  338. # either is True.
  339. # (By this point, these are either the same value or only one is not None.)
  340. return bool(new_option_should_run_here or legacy_option_should_run_here)
  341. def read_arguments(self, args: argparse.Namespace) -> None:
  342. # We support a bunch of command line arguments that override options in
  343. # the config. A lot of these options have a worker_* prefix when running
  344. # on workers so we also have to override them when command line options
  345. # are specified.
  346. if args.daemonize is not None:
  347. self.worker_daemonize = args.daemonize
  348. if args.manhole is not None:
  349. self.worker_manhole = args.worker_manhole