mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-15 03:03:50 +01:00
Allow moving typing off master (#7869)
This commit is contained in:
parent
649a7ead5c
commit
f2e38ca867
10 changed files with 282 additions and 176 deletions
1
changelog.d/7869.feature
Normal file
1
changelog.d/7869.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add experimental support for moving typing off master.
|
|
@ -111,6 +111,7 @@ from synapse.rest.client.v1.room import (
|
||||||
RoomSendEventRestServlet,
|
RoomSendEventRestServlet,
|
||||||
RoomStateEventRestServlet,
|
RoomStateEventRestServlet,
|
||||||
RoomStateRestServlet,
|
RoomStateRestServlet,
|
||||||
|
RoomTypingRestServlet,
|
||||||
)
|
)
|
||||||
from synapse.rest.client.v1.voip import VoipRestServlet
|
from synapse.rest.client.v1.voip import VoipRestServlet
|
||||||
from synapse.rest.client.v2_alpha import groups, sync, user_directory
|
from synapse.rest.client.v2_alpha import groups, sync, user_directory
|
||||||
|
@ -451,37 +452,6 @@ class GenericWorkerPresence(BasePresenceHandler):
|
||||||
await self._bump_active_client(user_id=user_id)
|
await self._bump_active_client(user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
class GenericWorkerTyping(object):
|
|
||||||
def __init__(self, hs):
|
|
||||||
self._latest_room_serial = 0
|
|
||||||
self._reset()
|
|
||||||
|
|
||||||
def _reset(self):
|
|
||||||
"""
|
|
||||||
Reset the typing handler's data caches.
|
|
||||||
"""
|
|
||||||
# map room IDs to serial numbers
|
|
||||||
self._room_serials = {}
|
|
||||||
# map room IDs to sets of users currently typing
|
|
||||||
self._room_typing = {}
|
|
||||||
|
|
||||||
def process_replication_rows(self, token, rows):
|
|
||||||
if self._latest_room_serial > token:
|
|
||||||
# The master has gone backwards. To prevent inconsistent data, just
|
|
||||||
# clear everything.
|
|
||||||
self._reset()
|
|
||||||
|
|
||||||
# Set the latest serial token to whatever the server gave us.
|
|
||||||
self._latest_room_serial = token
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
self._room_serials[row.room_id] = token
|
|
||||||
self._room_typing[row.room_id] = row.user_ids
|
|
||||||
|
|
||||||
def get_current_token(self) -> int:
|
|
||||||
return self._latest_room_serial
|
|
||||||
|
|
||||||
|
|
||||||
class GenericWorkerSlavedStore(
|
class GenericWorkerSlavedStore(
|
||||||
# FIXME(#3714): We need to add UserDirectoryStore as we write directly
|
# FIXME(#3714): We need to add UserDirectoryStore as we write directly
|
||||||
# rather than going via the correct worker.
|
# rather than going via the correct worker.
|
||||||
|
@ -558,6 +528,7 @@ class GenericWorkerServer(HomeServer):
|
||||||
KeyUploadServlet(self).register(resource)
|
KeyUploadServlet(self).register(resource)
|
||||||
AccountDataServlet(self).register(resource)
|
AccountDataServlet(self).register(resource)
|
||||||
RoomAccountDataServlet(self).register(resource)
|
RoomAccountDataServlet(self).register(resource)
|
||||||
|
RoomTypingRestServlet(self).register(resource)
|
||||||
|
|
||||||
sync.register_servlets(self, resource)
|
sync.register_servlets(self, resource)
|
||||||
events.register_servlets(self, resource)
|
events.register_servlets(self, resource)
|
||||||
|
@ -669,9 +640,6 @@ class GenericWorkerServer(HomeServer):
|
||||||
def build_presence_handler(self):
|
def build_presence_handler(self):
|
||||||
return GenericWorkerPresence(self)
|
return GenericWorkerPresence(self)
|
||||||
|
|
||||||
def build_typing_handler(self):
|
|
||||||
return GenericWorkerTyping(self)
|
|
||||||
|
|
||||||
|
|
||||||
class GenericWorkerReplicationHandler(ReplicationDataHandler):
|
class GenericWorkerReplicationHandler(ReplicationDataHandler):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
|
|
|
@ -34,9 +34,11 @@ class WriterLocations:
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
events: The instance that writes to the event and backfill streams.
|
events: The instance that writes to the event and backfill streams.
|
||||||
|
events: The instance that writes to the typing stream.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
events = attr.ib(default="master", type=str)
|
events = attr.ib(default="master", type=str)
|
||||||
|
typing = attr.ib(default="master", type=str)
|
||||||
|
|
||||||
|
|
||||||
class WorkerConfig(Config):
|
class WorkerConfig(Config):
|
||||||
|
@ -93,15 +95,14 @@ class WorkerConfig(Config):
|
||||||
writers = config.get("stream_writers") or {}
|
writers = config.get("stream_writers") or {}
|
||||||
self.writers = WriterLocations(**writers)
|
self.writers = WriterLocations(**writers)
|
||||||
|
|
||||||
# Check that the configured writer for events also appears in
|
# Check that the configured writer for events and typing also appears in
|
||||||
# `instance_map`.
|
# `instance_map`.
|
||||||
if (
|
for stream in ("events", "typing"):
|
||||||
self.writers.events != "master"
|
instance = getattr(self.writers, stream)
|
||||||
and self.writers.events not in self.instance_map
|
if instance != "master" and instance not in self.instance_map:
|
||||||
):
|
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
"Instance %r is configured to write events but does not appear in `instance_map` config."
|
"Instance %r is configured to write %s but does not appear in `instance_map` config."
|
||||||
% (self.writers.events,)
|
% (instance, stream)
|
||||||
)
|
)
|
||||||
|
|
||||||
def read_arguments(self, args):
|
def read_arguments(self, args):
|
||||||
|
|
|
@ -15,7 +15,18 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable, Dict, List, Match, Optional, Tuple, Union
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Match,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
from canonicaljson import json
|
from canonicaljson import json
|
||||||
from prometheus_client import Counter, Histogram
|
from prometheus_client import Counter, Histogram
|
||||||
|
@ -56,6 +67,9 @@ from synapse.util import glob_to_regex, unwrapFirstError
|
||||||
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
||||||
from synapse.util.caches.response_cache import ResponseCache
|
from synapse.util.caches.response_cache import ResponseCache
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
# when processing incoming transactions, we try to handle multiple rooms in
|
# when processing incoming transactions, we try to handle multiple rooms in
|
||||||
# parallel, up to this limit.
|
# parallel, up to this limit.
|
||||||
TRANSACTION_CONCURRENCY_LIMIT = 10
|
TRANSACTION_CONCURRENCY_LIMIT = 10
|
||||||
|
@ -768,11 +782,30 @@ class FederationHandlerRegistry(object):
|
||||||
query type for incoming federation traffic.
|
query type for incoming federation traffic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.edu_handlers = {}
|
self.config = hs.config
|
||||||
self.query_handlers = {}
|
self.http_client = hs.get_simple_http_client()
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self._instance_name = hs.get_instance_name()
|
||||||
|
|
||||||
def register_edu_handler(self, edu_type: str, handler: Callable[[str, dict], None]):
|
# These are safe to load in monolith mode, but will explode if we try
|
||||||
|
# and use them. However we have guards before we use them to ensure that
|
||||||
|
# we don't route to ourselves, and in monolith mode that will always be
|
||||||
|
# the case.
|
||||||
|
self._get_query_client = ReplicationGetQueryRestServlet.make_client(hs)
|
||||||
|
self._send_edu = ReplicationFederationSendEduRestServlet.make_client(hs)
|
||||||
|
|
||||||
|
self.edu_handlers = (
|
||||||
|
{}
|
||||||
|
) # type: Dict[str, Callable[[str, dict], Awaitable[None]]]
|
||||||
|
self.query_handlers = {} # type: Dict[str, Callable[[dict], Awaitable[None]]]
|
||||||
|
|
||||||
|
# Map from type to instance name that we should route EDU handling to.
|
||||||
|
self._edu_type_to_instance = {} # type: Dict[str, str]
|
||||||
|
|
||||||
|
def register_edu_handler(
|
||||||
|
self, edu_type: str, handler: Callable[[str, dict], Awaitable[None]]
|
||||||
|
):
|
||||||
"""Sets the handler callable that will be used to handle an incoming
|
"""Sets the handler callable that will be used to handle an incoming
|
||||||
federation EDU of the given type.
|
federation EDU of the given type.
|
||||||
|
|
||||||
|
@ -809,12 +842,18 @@ class FederationHandlerRegistry(object):
|
||||||
|
|
||||||
self.query_handlers[query_type] = handler
|
self.query_handlers[query_type] = handler
|
||||||
|
|
||||||
|
def register_instance_for_edu(self, edu_type: str, instance_name: str):
|
||||||
|
"""Register that the EDU handler is on a different instance than master.
|
||||||
|
"""
|
||||||
|
self._edu_type_to_instance[edu_type] = instance_name
|
||||||
|
|
||||||
async def on_edu(self, edu_type: str, origin: str, content: dict):
|
async def on_edu(self, edu_type: str, origin: str, content: dict):
|
||||||
handler = self.edu_handlers.get(edu_type)
|
if not self.config.use_presence and edu_type == "m.presence":
|
||||||
if not handler:
|
|
||||||
logger.warning("No handler registered for EDU type %s", edu_type)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check if we have a handler on this instance
|
||||||
|
handler = self.edu_handlers.get(edu_type)
|
||||||
|
if handler:
|
||||||
with start_active_span_from_edu(content, "handle_edu"):
|
with start_active_span_from_edu(content, "handle_edu"):
|
||||||
try:
|
try:
|
||||||
await handler(origin, content)
|
await handler(origin, content)
|
||||||
|
@ -822,53 +861,37 @@ class FederationHandlerRegistry(object):
|
||||||
logger.info("Failed to handle edu %r: %r", edu_type, e)
|
logger.info("Failed to handle edu %r: %r", edu_type, e)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to handle edu %r", edu_type)
|
logger.exception("Failed to handle edu %r", edu_type)
|
||||||
|
|
||||||
def on_query(self, query_type: str, args: dict) -> defer.Deferred:
|
|
||||||
handler = self.query_handlers.get(query_type)
|
|
||||||
if not handler:
|
|
||||||
logger.warning("No handler registered for query type %s", query_type)
|
|
||||||
raise NotFoundError("No handler for Query type '%s'" % (query_type,))
|
|
||||||
|
|
||||||
return handler(args)
|
|
||||||
|
|
||||||
|
|
||||||
class ReplicationFederationHandlerRegistry(FederationHandlerRegistry):
|
|
||||||
"""A FederationHandlerRegistry for worker processes.
|
|
||||||
|
|
||||||
When receiving EDU or queries it will check if an appropriate handler has
|
|
||||||
been registered on the worker, if there isn't one then it calls off to the
|
|
||||||
master process.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, hs):
|
|
||||||
self.config = hs.config
|
|
||||||
self.http_client = hs.get_simple_http_client()
|
|
||||||
self.clock = hs.get_clock()
|
|
||||||
|
|
||||||
self._get_query_client = ReplicationGetQueryRestServlet.make_client(hs)
|
|
||||||
self._send_edu = ReplicationFederationSendEduRestServlet.make_client(hs)
|
|
||||||
|
|
||||||
super(ReplicationFederationHandlerRegistry, self).__init__()
|
|
||||||
|
|
||||||
async def on_edu(self, edu_type: str, origin: str, content: dict):
|
|
||||||
"""Overrides FederationHandlerRegistry
|
|
||||||
"""
|
|
||||||
if not self.config.use_presence and edu_type == "m.presence":
|
|
||||||
return
|
return
|
||||||
|
|
||||||
handler = self.edu_handlers.get(edu_type)
|
# Check if we can route it somewhere else that isn't us
|
||||||
if handler:
|
route_to = self._edu_type_to_instance.get(edu_type, "master")
|
||||||
return await super(ReplicationFederationHandlerRegistry, self).on_edu(
|
if route_to != self._instance_name:
|
||||||
edu_type, origin, content
|
try:
|
||||||
|
await self._send_edu(
|
||||||
|
instance_name=route_to,
|
||||||
|
edu_type=edu_type,
|
||||||
|
origin=origin,
|
||||||
|
content=content,
|
||||||
)
|
)
|
||||||
|
except SynapseError as e:
|
||||||
|
logger.info("Failed to handle edu %r: %r", edu_type, e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to handle edu %r", edu_type)
|
||||||
|
return
|
||||||
|
|
||||||
return await self._send_edu(edu_type=edu_type, origin=origin, content=content)
|
# Oh well, let's just log and move on.
|
||||||
|
logger.warning("No handler registered for EDU type %s", edu_type)
|
||||||
|
|
||||||
async def on_query(self, query_type: str, args: dict):
|
async def on_query(self, query_type: str, args: dict):
|
||||||
"""Overrides FederationHandlerRegistry
|
|
||||||
"""
|
|
||||||
handler = self.query_handlers.get(query_type)
|
handler = self.query_handlers.get(query_type)
|
||||||
if handler:
|
if handler:
|
||||||
return await handler(args)
|
return await handler(args)
|
||||||
|
|
||||||
|
# Check if we can route it somewhere else that isn't us
|
||||||
|
if self._instance_name == "master":
|
||||||
return await self._get_query_client(query_type=query_type, args=args)
|
return await self._get_query_client(query_type=query_type, args=args)
|
||||||
|
|
||||||
|
# Uh oh, no handler! Let's raise an exception so the request returns an
|
||||||
|
# error.
|
||||||
|
logger.warning("No handler registered for query type %s", query_type)
|
||||||
|
raise NotFoundError("No handler for Query type '%s'" % (query_type,))
|
||||||
|
|
|
@ -15,15 +15,19 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import List, Tuple
|
from typing import TYPE_CHECKING, List, Set, Tuple
|
||||||
|
|
||||||
from synapse.api.errors import AuthError, SynapseError
|
from synapse.api.errors import AuthError, SynapseError
|
||||||
from synapse.logging.context import run_in_background
|
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||||
|
from synapse.replication.tcp.streams import TypingStream
|
||||||
from synapse.types import UserID, get_domain_from_id
|
from synapse.types import UserID, get_domain_from_id
|
||||||
from synapse.util.caches.stream_change_cache import StreamChangeCache
|
from synapse.util.caches.stream_change_cache import StreamChangeCache
|
||||||
from synapse.util.metrics import Measure
|
from synapse.util.metrics import Measure
|
||||||
from synapse.util.wheel_timer import WheelTimer
|
from synapse.util.wheel_timer import WheelTimer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,48 +43,48 @@ FEDERATION_TIMEOUT = 60 * 1000
|
||||||
FEDERATION_PING_INTERVAL = 40 * 1000
|
FEDERATION_PING_INTERVAL = 40 * 1000
|
||||||
|
|
||||||
|
|
||||||
class TypingHandler(object):
|
class FollowerTypingHandler:
|
||||||
def __init__(self, hs):
|
"""A typing handler on a different process than the writer that is updated
|
||||||
|
via replication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.server_name = hs.config.server_name
|
self.server_name = hs.config.server_name
|
||||||
self.auth = hs.get_auth()
|
|
||||||
self.is_mine_id = hs.is_mine_id
|
|
||||||
self.notifier = hs.get_notifier()
|
|
||||||
self.state = hs.get_state_handler()
|
|
||||||
|
|
||||||
self.hs = hs
|
|
||||||
|
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.wheel_timer = WheelTimer(bucket_size=5000)
|
self.is_mine_id = hs.is_mine_id
|
||||||
|
|
||||||
|
self.federation = None
|
||||||
|
if hs.should_send_federation():
|
||||||
self.federation = hs.get_federation_sender()
|
self.federation = hs.get_federation_sender()
|
||||||
|
|
||||||
hs.get_federation_registry().register_edu_handler("m.typing", self._recv_edu)
|
if hs.config.worker.writers.typing != hs.get_instance_name():
|
||||||
|
hs.get_federation_registry().register_instance_for_edu(
|
||||||
hs.get_distributor().observe("user_left_room", self.user_left_room)
|
"m.typing", hs.config.worker.writers.typing,
|
||||||
|
|
||||||
self._member_typing_until = {} # clock time we expect to stop
|
|
||||||
self._member_last_federation_poke = {}
|
|
||||||
|
|
||||||
self._latest_room_serial = 0
|
|
||||||
self._reset()
|
|
||||||
|
|
||||||
# caches which room_ids changed at which serials
|
|
||||||
self._typing_stream_change_cache = StreamChangeCache(
|
|
||||||
"TypingStreamChangeCache", self._latest_room_serial
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# map room IDs to serial numbers
|
||||||
|
self._room_serials = {}
|
||||||
|
# map room IDs to sets of users currently typing
|
||||||
|
self._room_typing = {}
|
||||||
|
|
||||||
|
self._member_last_federation_poke = {}
|
||||||
|
self.wheel_timer = WheelTimer(bucket_size=5000)
|
||||||
|
self._latest_room_serial = 0
|
||||||
|
|
||||||
self.clock.looping_call(self._handle_timeouts, 5000)
|
self.clock.looping_call(self._handle_timeouts, 5000)
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
"""
|
"""Reset the typing handler's data caches.
|
||||||
Reset the typing handler's data caches.
|
|
||||||
"""
|
"""
|
||||||
# map room IDs to serial numbers
|
# map room IDs to serial numbers
|
||||||
self._room_serials = {}
|
self._room_serials = {}
|
||||||
# map room IDs to sets of users currently typing
|
# map room IDs to sets of users currently typing
|
||||||
self._room_typing = {}
|
self._room_typing = {}
|
||||||
|
|
||||||
|
self._member_last_federation_poke = {}
|
||||||
|
self.wheel_timer = WheelTimer(bucket_size=5000)
|
||||||
|
|
||||||
def _handle_timeouts(self):
|
def _handle_timeouts(self):
|
||||||
logger.debug("Checking for typing timeouts")
|
logger.debug("Checking for typing timeouts")
|
||||||
|
|
||||||
|
@ -89,22 +93,21 @@ class TypingHandler(object):
|
||||||
members = set(self.wheel_timer.fetch(now))
|
members = set(self.wheel_timer.fetch(now))
|
||||||
|
|
||||||
for member in members:
|
for member in members:
|
||||||
|
self._handle_timeout_for_member(now, member)
|
||||||
|
|
||||||
|
def _handle_timeout_for_member(self, now: int, member: RoomMember):
|
||||||
if not self.is_typing(member):
|
if not self.is_typing(member):
|
||||||
# Nothing to do if they're no longer typing
|
# Nothing to do if they're no longer typing
|
||||||
continue
|
return
|
||||||
|
|
||||||
until = self._member_typing_until.get(member, None)
|
|
||||||
if not until or until <= now:
|
|
||||||
logger.info("Timing out typing for: %s", member.user_id)
|
|
||||||
self._stopped_typing(member)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if we need to resend a keep alive over federation for this
|
# Check if we need to resend a keep alive over federation for this
|
||||||
# user.
|
# user.
|
||||||
if self.hs.is_mine_id(member.user_id):
|
if self.federation and self.is_mine_id(member.user_id):
|
||||||
last_fed_poke = self._member_last_federation_poke.get(member, None)
|
last_fed_poke = self._member_last_federation_poke.get(member, None)
|
||||||
if not last_fed_poke or last_fed_poke + FEDERATION_PING_INTERVAL <= now:
|
if not last_fed_poke or last_fed_poke + FEDERATION_PING_INTERVAL <= now:
|
||||||
run_in_background(self._push_remote, member=member, typing=True)
|
run_as_background_process(
|
||||||
|
"typing._push_remote", self._push_remote, member=member, typing=True
|
||||||
|
)
|
||||||
|
|
||||||
# Add a paranoia timer to ensure that we always have a timer for
|
# Add a paranoia timer to ensure that we always have a timer for
|
||||||
# each person typing.
|
# each person typing.
|
||||||
|
@ -113,6 +116,117 @@ class TypingHandler(object):
|
||||||
def is_typing(self, member):
|
def is_typing(self, member):
|
||||||
return member.user_id in self._room_typing.get(member.room_id, [])
|
return member.user_id in self._room_typing.get(member.room_id, [])
|
||||||
|
|
||||||
|
async def _push_remote(self, member, typing):
|
||||||
|
if not self.federation:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
users = await self.store.get_users_in_room(member.room_id)
|
||||||
|
self._member_last_federation_poke[member] = self.clock.time_msec()
|
||||||
|
|
||||||
|
now = self.clock.time_msec()
|
||||||
|
self.wheel_timer.insert(
|
||||||
|
now=now, obj=member, then=now + FEDERATION_PING_INTERVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
for domain in {get_domain_from_id(u) for u in users}:
|
||||||
|
if domain != self.server_name:
|
||||||
|
logger.debug("sending typing update to %s", domain)
|
||||||
|
self.federation.build_and_send_edu(
|
||||||
|
destination=domain,
|
||||||
|
edu_type="m.typing",
|
||||||
|
content={
|
||||||
|
"room_id": member.room_id,
|
||||||
|
"user_id": member.user_id,
|
||||||
|
"typing": typing,
|
||||||
|
},
|
||||||
|
key=member,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error pushing typing notif to remotes")
|
||||||
|
|
||||||
|
def process_replication_rows(
|
||||||
|
self, token: int, rows: List[TypingStream.TypingStreamRow]
|
||||||
|
):
|
||||||
|
"""Should be called whenever we receive updates for typing stream.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._latest_room_serial > token:
|
||||||
|
# The master has gone backwards. To prevent inconsistent data, just
|
||||||
|
# clear everything.
|
||||||
|
self._reset()
|
||||||
|
|
||||||
|
# Set the latest serial token to whatever the server gave us.
|
||||||
|
self._latest_room_serial = token
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
self._room_serials[row.room_id] = token
|
||||||
|
|
||||||
|
prev_typing = set(self._room_typing.get(row.room_id, []))
|
||||||
|
now_typing = set(row.user_ids)
|
||||||
|
self._room_typing[row.room_id] = row.user_ids
|
||||||
|
|
||||||
|
run_as_background_process(
|
||||||
|
"_handle_change_in_typing",
|
||||||
|
self._handle_change_in_typing,
|
||||||
|
row.room_id,
|
||||||
|
prev_typing,
|
||||||
|
now_typing,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _handle_change_in_typing(
|
||||||
|
self, room_id: str, prev_typing: Set[str], now_typing: Set[str]
|
||||||
|
):
|
||||||
|
"""Process a change in typing of a room from replication, sending EDUs
|
||||||
|
for any local users.
|
||||||
|
"""
|
||||||
|
for user_id in now_typing - prev_typing:
|
||||||
|
if self.is_mine_id(user_id):
|
||||||
|
await self._push_remote(RoomMember(room_id, user_id), True)
|
||||||
|
|
||||||
|
for user_id in prev_typing - now_typing:
|
||||||
|
if self.is_mine_id(user_id):
|
||||||
|
await self._push_remote(RoomMember(room_id, user_id), False)
|
||||||
|
|
||||||
|
def get_current_token(self):
|
||||||
|
return self._latest_room_serial
|
||||||
|
|
||||||
|
|
||||||
|
class TypingWriterHandler(FollowerTypingHandler):
|
||||||
|
def __init__(self, hs):
|
||||||
|
super().__init__(hs)
|
||||||
|
|
||||||
|
assert hs.config.worker.writers.typing == hs.get_instance_name()
|
||||||
|
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.notifier = hs.get_notifier()
|
||||||
|
|
||||||
|
self.hs = hs
|
||||||
|
|
||||||
|
hs.get_federation_registry().register_edu_handler("m.typing", self._recv_edu)
|
||||||
|
|
||||||
|
hs.get_distributor().observe("user_left_room", self.user_left_room)
|
||||||
|
|
||||||
|
self._member_typing_until = {} # clock time we expect to stop
|
||||||
|
|
||||||
|
# caches which room_ids changed at which serials
|
||||||
|
self._typing_stream_change_cache = StreamChangeCache(
|
||||||
|
"TypingStreamChangeCache", self._latest_room_serial
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_timeout_for_member(self, now: int, member: RoomMember):
|
||||||
|
super()._handle_timeout_for_member(now, member)
|
||||||
|
|
||||||
|
if not self.is_typing(member):
|
||||||
|
# Nothing to do if they're no longer typing
|
||||||
|
return
|
||||||
|
|
||||||
|
until = self._member_typing_until.get(member, None)
|
||||||
|
if not until or until <= now:
|
||||||
|
logger.info("Timing out typing for: %s", member.user_id)
|
||||||
|
self._stopped_typing(member)
|
||||||
|
return
|
||||||
|
|
||||||
async def started_typing(self, target_user, auth_user, room_id, timeout):
|
async def started_typing(self, target_user, auth_user, room_id, timeout):
|
||||||
target_user_id = target_user.to_string()
|
target_user_id = target_user.to_string()
|
||||||
auth_user_id = auth_user.to_string()
|
auth_user_id = auth_user.to_string()
|
||||||
|
@ -179,36 +293,12 @@ class TypingHandler(object):
|
||||||
def _push_update(self, member, typing):
|
def _push_update(self, member, typing):
|
||||||
if self.hs.is_mine_id(member.user_id):
|
if self.hs.is_mine_id(member.user_id):
|
||||||
# Only send updates for changes to our own users.
|
# Only send updates for changes to our own users.
|
||||||
run_in_background(self._push_remote, member, typing)
|
run_as_background_process(
|
||||||
|
"typing._push_remote", self._push_remote, member, typing
|
||||||
|
)
|
||||||
|
|
||||||
self._push_update_local(member=member, typing=typing)
|
self._push_update_local(member=member, typing=typing)
|
||||||
|
|
||||||
async def _push_remote(self, member, typing):
|
|
||||||
try:
|
|
||||||
users = await self.store.get_users_in_room(member.room_id)
|
|
||||||
self._member_last_federation_poke[member] = self.clock.time_msec()
|
|
||||||
|
|
||||||
now = self.clock.time_msec()
|
|
||||||
self.wheel_timer.insert(
|
|
||||||
now=now, obj=member, then=now + FEDERATION_PING_INTERVAL
|
|
||||||
)
|
|
||||||
|
|
||||||
for domain in {get_domain_from_id(u) for u in users}:
|
|
||||||
if domain != self.server_name:
|
|
||||||
logger.debug("sending typing update to %s", domain)
|
|
||||||
self.federation.build_and_send_edu(
|
|
||||||
destination=domain,
|
|
||||||
edu_type="m.typing",
|
|
||||||
content={
|
|
||||||
"room_id": member.room_id,
|
|
||||||
"user_id": member.user_id,
|
|
||||||
"typing": typing,
|
|
||||||
},
|
|
||||||
key=member,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Error pushing typing notif to remotes")
|
|
||||||
|
|
||||||
async def _recv_edu(self, origin, content):
|
async def _recv_edu(self, origin, content):
|
||||||
room_id = content["room_id"]
|
room_id = content["room_id"]
|
||||||
user_id = content["user_id"]
|
user_id = content["user_id"]
|
||||||
|
@ -304,8 +394,11 @@ class TypingHandler(object):
|
||||||
|
|
||||||
return rows, current_id, limited
|
return rows, current_id, limited
|
||||||
|
|
||||||
def get_current_token(self):
|
def process_replication_rows(
|
||||||
return self._latest_room_serial
|
self, token: int, rows: List[TypingStream.TypingStreamRow]
|
||||||
|
):
|
||||||
|
# The writing process should never get updates from replication.
|
||||||
|
raise Exception("Typing writer instance got typing info over replication")
|
||||||
|
|
||||||
|
|
||||||
class TypingNotificationEventSource(object):
|
class TypingNotificationEventSource(object):
|
||||||
|
|
|
@ -42,6 +42,7 @@ from synapse.replication.tcp.streams import (
|
||||||
EventsStream,
|
EventsStream,
|
||||||
FederationStream,
|
FederationStream,
|
||||||
Stream,
|
Stream,
|
||||||
|
TypingStream,
|
||||||
)
|
)
|
||||||
from synapse.util.async_helpers import Linearizer
|
from synapse.util.async_helpers import Linearizer
|
||||||
|
|
||||||
|
@ -96,6 +97,14 @@ class ReplicationCommandHandler:
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if isinstance(stream, TypingStream):
|
||||||
|
# Only add TypingStream as a source on the instance in charge of
|
||||||
|
# typing.
|
||||||
|
if hs.config.worker.writers.typing == hs.get_instance_name():
|
||||||
|
self._streams_to_replicate.append(stream)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
# Only add any other streams if we're on master.
|
# Only add any other streams if we're on master.
|
||||||
if hs.config.worker_app is not None:
|
if hs.config.worker_app is not None:
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -294,11 +294,12 @@ class TypingStream(Stream):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
typing_handler = hs.get_typing_handler()
|
typing_handler = hs.get_typing_handler()
|
||||||
|
|
||||||
if hs.config.worker_app is None:
|
writer_instance = hs.config.worker.writers.typing
|
||||||
# on the master, query the typing handler
|
if writer_instance == hs.get_instance_name():
|
||||||
|
# On the writer, query the typing handler
|
||||||
update_function = typing_handler.get_all_typing_updates
|
update_function = typing_handler.get_all_typing_updates
|
||||||
else:
|
else:
|
||||||
# Query master process
|
# Query the typing writer process
|
||||||
update_function = make_http_update_function(hs, self.NAME)
|
update_function = make_http_update_function(hs, self.NAME)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|
|
@ -817,9 +817,18 @@ class RoomTypingRestServlet(RestServlet):
|
||||||
self.typing_handler = hs.get_typing_handler()
|
self.typing_handler = hs.get_typing_handler()
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
|
|
||||||
|
# If we're not on the typing writer instance we should scream if we get
|
||||||
|
# requests.
|
||||||
|
self._is_typing_writer = (
|
||||||
|
hs.config.worker.writers.typing == hs.get_instance_name()
|
||||||
|
)
|
||||||
|
|
||||||
async def on_PUT(self, request, room_id, user_id):
|
async def on_PUT(self, request, room_id, user_id):
|
||||||
requester = await self.auth.get_user_by_req(request)
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
|
if not self._is_typing_writer:
|
||||||
|
raise Exception("Got /typing request on instance that is not typing writer")
|
||||||
|
|
||||||
room_id = urlparse.unquote(room_id)
|
room_id = urlparse.unquote(room_id)
|
||||||
target_user = UserID.from_string(urlparse.unquote(user_id))
|
target_user = UserID.from_string(urlparse.unquote(user_id))
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,6 @@ from synapse.federation.federation_client import FederationClient
|
||||||
from synapse.federation.federation_server import (
|
from synapse.federation.federation_server import (
|
||||||
FederationHandlerRegistry,
|
FederationHandlerRegistry,
|
||||||
FederationServer,
|
FederationServer,
|
||||||
ReplicationFederationHandlerRegistry,
|
|
||||||
)
|
)
|
||||||
from synapse.federation.send_queue import FederationRemoteSendQueue
|
from synapse.federation.send_queue import FederationRemoteSendQueue
|
||||||
from synapse.federation.sender import FederationSender
|
from synapse.federation.sender import FederationSender
|
||||||
|
@ -84,7 +83,7 @@ from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
|
||||||
from synapse.handlers.set_password import SetPasswordHandler
|
from synapse.handlers.set_password import SetPasswordHandler
|
||||||
from synapse.handlers.stats import StatsHandler
|
from synapse.handlers.stats import StatsHandler
|
||||||
from synapse.handlers.sync import SyncHandler
|
from synapse.handlers.sync import SyncHandler
|
||||||
from synapse.handlers.typing import TypingHandler
|
from synapse.handlers.typing import FollowerTypingHandler, TypingWriterHandler
|
||||||
from synapse.handlers.user_directory import UserDirectoryHandler
|
from synapse.handlers.user_directory import UserDirectoryHandler
|
||||||
from synapse.http.client import InsecureInterceptableContextFactory, SimpleHttpClient
|
from synapse.http.client import InsecureInterceptableContextFactory, SimpleHttpClient
|
||||||
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
||||||
|
@ -378,7 +377,10 @@ class HomeServer(object):
|
||||||
return PresenceHandler(self)
|
return PresenceHandler(self)
|
||||||
|
|
||||||
def build_typing_handler(self):
|
def build_typing_handler(self):
|
||||||
return TypingHandler(self)
|
if self.config.worker.writers.typing == self.get_instance_name():
|
||||||
|
return TypingWriterHandler(self)
|
||||||
|
else:
|
||||||
|
return FollowerTypingHandler(self)
|
||||||
|
|
||||||
def build_sync_handler(self):
|
def build_sync_handler(self):
|
||||||
return SyncHandler(self)
|
return SyncHandler(self)
|
||||||
|
@ -534,10 +536,7 @@ class HomeServer(object):
|
||||||
return RoomMemberMasterHandler(self)
|
return RoomMemberMasterHandler(self)
|
||||||
|
|
||||||
def build_federation_registry(self):
|
def build_federation_registry(self):
|
||||||
if self.config.worker_app:
|
return FederationHandlerRegistry(self)
|
||||||
return ReplicationFederationHandlerRegistry(self)
|
|
||||||
else:
|
|
||||||
return FederationHandlerRegistry()
|
|
||||||
|
|
||||||
def build_server_notices_manager(self):
|
def build_server_notices_manager(self):
|
||||||
if self.config.worker_app:
|
if self.config.worker_app:
|
||||||
|
|
|
@ -148,3 +148,5 @@ class HomeServer(object):
|
||||||
self,
|
self,
|
||||||
) -> synapse.http.matrixfederationclient.MatrixFederationHttpClient:
|
) -> synapse.http.matrixfederationclient.MatrixFederationHttpClient:
|
||||||
pass
|
pass
|
||||||
|
def should_send_federation(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
Loading…
Reference in a new issue