You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

319 lines
12 KiB

  1. # Copyright 2016 OpenMarket Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import List, Union
  15. import attr
  16. from ._base import (
  17. Config,
  18. ConfigError,
  19. RoutableShardedWorkerHandlingConfig,
  20. ShardedWorkerHandlingConfig,
  21. )
  22. from .server import ListenerConfig, parse_listener_def
  23. _FEDERATION_SENDER_WITH_SEND_FEDERATION_ENABLED_ERROR = """
  24. The send_federation config option must be disabled in the main
  25. synapse process before they can be run in a separate worker.
  26. Please add ``send_federation: false`` to the main config
  27. """
  28. _PUSHER_WITH_START_PUSHERS_ENABLED_ERROR = """
  29. The start_pushers config option must be disabled in the main
  30. synapse process before they can be run in a separate worker.
  31. Please add ``start_pushers: false`` to the main config
  32. """
  33. def _instance_to_list_converter(obj: Union[str, List[str]]) -> List[str]:
  34. """Helper for allowing parsing a string or list of strings to a config
  35. option expecting a list of strings.
  36. """
  37. if isinstance(obj, str):
  38. return [obj]
  39. return obj
  40. @attr.s
  41. class InstanceLocationConfig:
  42. """The host and port to talk to an instance via HTTP replication."""
  43. host = attr.ib(type=str)
  44. port = attr.ib(type=int)
  45. @attr.s
  46. class WriterLocations:
  47. """Specifies the instances that write various streams.
  48. Attributes:
  49. events: The instances that write to the event and backfill streams.
  50. typing: The instance that writes to the typing stream.
  51. """
  52. events = attr.ib(
  53. default=["master"], type=List[str], converter=_instance_to_list_converter
  54. )
  55. typing = attr.ib(default="master", type=str)
  56. to_device = attr.ib(
  57. default=["master"],
  58. type=List[str],
  59. converter=_instance_to_list_converter,
  60. )
  61. account_data = attr.ib(
  62. default=["master"],
  63. type=List[str],
  64. converter=_instance_to_list_converter,
  65. )
  66. receipts = attr.ib(
  67. default=["master"],
  68. type=List[str],
  69. converter=_instance_to_list_converter,
  70. )
  71. class WorkerConfig(Config):
  72. """The workers are processes run separately to the main synapse process.
  73. They have their own pid_file and listener configuration. They use the
  74. replication_url to talk to the main synapse process."""
  75. section = "worker"
  76. def read_config(self, config, **kwargs):
  77. self.worker_app = config.get("worker_app")
  78. # Canonicalise worker_app so that master always has None
  79. if self.worker_app == "synapse.app.homeserver":
  80. self.worker_app = None
  81. self.worker_listeners = [
  82. parse_listener_def(x) for x in config.get("worker_listeners", [])
  83. ]
  84. self.worker_daemonize = config.get("worker_daemonize")
  85. self.worker_pid_file = config.get("worker_pid_file")
  86. self.worker_log_config = config.get("worker_log_config")
  87. # The host used to connect to the main synapse
  88. self.worker_replication_host = config.get("worker_replication_host", None)
  89. # The port on the main synapse for TCP replication
  90. self.worker_replication_port = config.get("worker_replication_port", None)
  91. # The port on the main synapse for HTTP replication endpoint
  92. self.worker_replication_http_port = config.get("worker_replication_http_port")
  93. # The shared secret used for authentication when connecting to the main synapse.
  94. self.worker_replication_secret = config.get("worker_replication_secret", None)
  95. self.worker_name = config.get("worker_name", self.worker_app)
  96. self.instance_name = self.worker_name or "master"
  97. self.worker_main_http_uri = config.get("worker_main_http_uri", None)
  98. # This option is really only here to support `--manhole` command line
  99. # argument.
  100. manhole = config.get("worker_manhole")
  101. if manhole:
  102. self.worker_listeners.append(
  103. ListenerConfig(
  104. port=manhole,
  105. bind_addresses=["127.0.0.1"],
  106. type="manhole",
  107. )
  108. )
  109. # Handle federation sender configuration.
  110. #
  111. # There are two ways of configuring which instances handle federation
  112. # sending:
  113. # 1. The old way where "send_federation" is set to false and running a
  114. # `synapse.app.federation_sender` worker app.
  115. # 2. Specifying the workers sending federation in
  116. # `federation_sender_instances`.
  117. #
  118. send_federation = config.get("send_federation", True)
  119. federation_sender_instances = config.get("federation_sender_instances")
  120. if federation_sender_instances is None:
  121. # Default to an empty list, which means "another, unknown, worker is
  122. # responsible for it".
  123. federation_sender_instances = []
  124. # If no federation sender instances are set we check if
  125. # `send_federation` is set, which means use master
  126. if send_federation:
  127. federation_sender_instances = ["master"]
  128. if self.worker_app == "synapse.app.federation_sender":
  129. if send_federation:
  130. # If we're running federation senders, and not using
  131. # `federation_sender_instances`, then we should have
  132. # explicitly set `send_federation` to false.
  133. raise ConfigError(
  134. _FEDERATION_SENDER_WITH_SEND_FEDERATION_ENABLED_ERROR
  135. )
  136. federation_sender_instances = [self.worker_name]
  137. self.send_federation = self.instance_name in federation_sender_instances
  138. self.federation_shard_config = ShardedWorkerHandlingConfig(
  139. federation_sender_instances
  140. )
  141. # A map from instance name to host/port of their HTTP replication endpoint.
  142. instance_map = config.get("instance_map") or {}
  143. self.instance_map = {
  144. name: InstanceLocationConfig(**c) for name, c in instance_map.items()
  145. }
  146. # Map from type of streams to source, c.f. WriterLocations.
  147. writers = config.get("stream_writers") or {}
  148. self.writers = WriterLocations(**writers)
  149. # Check that the configured writers for events and typing also appears in
  150. # `instance_map`.
  151. for stream in ("events", "typing", "to_device", "account_data", "receipts"):
  152. instances = _instance_to_list_converter(getattr(self.writers, stream))
  153. for instance in instances:
  154. if instance != "master" and instance not in self.instance_map:
  155. raise ConfigError(
  156. "Instance %r is configured to write %s but does not appear in `instance_map` config."
  157. % (instance, stream)
  158. )
  159. if len(self.writers.to_device) != 1:
  160. raise ConfigError(
  161. "Must only specify one instance to handle `to_device` messages."
  162. )
  163. if len(self.writers.account_data) != 1:
  164. raise ConfigError(
  165. "Must only specify one instance to handle `account_data` messages."
  166. )
  167. if len(self.writers.receipts) != 1:
  168. raise ConfigError(
  169. "Must only specify one instance to handle `receipts` messages."
  170. )
  171. if len(self.writers.events) == 0:
  172. raise ConfigError("Must specify at least one instance to handle `events`.")
  173. self.events_shard_config = RoutableShardedWorkerHandlingConfig(
  174. self.writers.events
  175. )
  176. # Handle sharded push
  177. start_pushers = config.get("start_pushers", True)
  178. pusher_instances = config.get("pusher_instances")
  179. if pusher_instances is None:
  180. # Default to an empty list, which means "another, unknown, worker is
  181. # responsible for it".
  182. pusher_instances = []
  183. # If no pushers instances are set we check if `start_pushers` is
  184. # set, which means use master
  185. if start_pushers:
  186. pusher_instances = ["master"]
  187. if self.worker_app == "synapse.app.pusher":
  188. if start_pushers:
  189. # If we're running pushers, and not using
  190. # `pusher_instances`, then we should have explicitly set
  191. # `start_pushers` to false.
  192. raise ConfigError(_PUSHER_WITH_START_PUSHERS_ENABLED_ERROR)
  193. pusher_instances = [self.instance_name]
  194. self.start_pushers = self.instance_name in pusher_instances
  195. self.pusher_shard_config = ShardedWorkerHandlingConfig(pusher_instances)
  196. # Whether this worker should run background tasks or not.
  197. #
  198. # As a note for developers, the background tasks guarded by this should
  199. # be able to run on only a single instance (meaning that they don't
  200. # depend on any in-memory state of a particular worker).
  201. #
  202. # No effort is made to ensure only a single instance of these tasks is
  203. # running.
  204. background_tasks_instance = config.get("run_background_tasks_on") or "master"
  205. self.run_background_tasks = (
  206. self.worker_name is None and background_tasks_instance == "master"
  207. ) or self.worker_name == background_tasks_instance
  208. def generate_config_section(self, config_dir_path, server_name, **kwargs):
  209. return """\
  210. ## Workers ##
  211. # Disables sending of outbound federation transactions on the main process.
  212. # Uncomment if using a federation sender worker.
  213. #
  214. #send_federation: false
  215. # It is possible to run multiple federation sender workers, in which case the
  216. # work is balanced across them.
  217. #
  218. # This configuration must be shared between all federation sender workers, and if
  219. # changed all federation sender workers must be stopped at the same time and then
  220. # started, to ensure that all instances are running with the same config (otherwise
  221. # events may be dropped).
  222. #
  223. #federation_sender_instances:
  224. # - federation_sender1
  225. # When using workers this should be a map from `worker_name` to the
  226. # HTTP replication listener of the worker, if configured.
  227. #
  228. #instance_map:
  229. # worker1:
  230. # host: localhost
  231. # port: 8034
  232. # Experimental: When using workers you can define which workers should
  233. # handle event persistence and typing notifications. Any worker
  234. # specified here must also be in the `instance_map`.
  235. #
  236. #stream_writers:
  237. # events: worker1
  238. # typing: worker1
  239. # The worker that is used to run background tasks (e.g. cleaning up expired
  240. # data). If not provided this defaults to the main process.
  241. #
  242. #run_background_tasks_on: worker1
  243. # A shared secret used by the replication APIs to authenticate HTTP requests
  244. # from workers.
  245. #
  246. # By default this is unused and traffic is not authenticated.
  247. #
  248. #worker_replication_secret: ""
  249. """
  250. def read_arguments(self, args):
  251. # We support a bunch of command line arguments that override options in
  252. # the config. A lot of these options have a worker_* prefix when running
  253. # on workers so we also have to override them when command line options
  254. # are specified.
  255. if args.daemonize is not None:
  256. self.worker_daemonize = args.daemonize
  257. if args.manhole is not None:
  258. self.worker_manhole = args.worker_manhole