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

Send device messages over federation #1074

Merged
merged 11 commits into from
Sep 8, 2016
6 changes: 6 additions & 0 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ def send_edu(self, destination, edu_type, content):
self._transaction_queue.enqueue_edu(edu)
return defer.succeed(None)

@log_function
def send_device_messages(self, destination):
"""Sends the device messages in the local database to the remote
destination"""
self._transaction_queue.enqueue_device_messages(destination)

@log_function
def send_failure(self, failure, destination):
self._transaction_queue.enqueue_failure(failure, destination)
Expand Down
2 changes: 1 addition & 1 deletion synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def received_edu(self, origin, edu_type, content):
except SynapseError as e:
logger.info("Failed to handle edu %r: %r", edu_type, e)
except Exception as e:
logger.exception("Failed to handle edu %r", edu_type, e)
logger.exception("Failed to handle edu %r", edu_type)
else:
logger.warn("Received EDU of type %s with no handler", edu_type)

Expand Down
65 changes: 56 additions & 9 deletions synapse/federation/transaction_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from twisted.internet import defer

from .persistence import TransactionActions
from .units import Transaction
from .units import Transaction, Edu

from synapse.api.errors import HttpResponseException
from synapse.util.async import run_on_reactor
Expand Down Expand Up @@ -81,6 +81,8 @@ def __init__(self, hs, transport_layer):
# destination -> list of tuple(failure, deferred)
self.pending_failures_by_dest = {}

self.last_device_stream_id_by_dest = {}

# HACK to get unique tx id
self._next_txn_id = int(self.clock.time_msec())

Expand Down Expand Up @@ -155,6 +157,17 @@ def enqueue_failure(self, failure, destination):
self._attempt_new_transaction, destination
)

def enqueue_device_messages(self, destination):
if destination == self.server_name or destination == "localhost":
return

if not self.can_send_to(destination):
return

preserve_context_over_fn(
self._attempt_new_transaction, destination
)

@defer.inlineCallbacks
def _attempt_new_transaction(self, destination):
yield run_on_reactor()
Expand All @@ -175,6 +188,12 @@ def _attempt_new_transaction(self, destination):
pending_edus = self.pending_edus_by_dest.pop(destination, [])
pending_failures = self.pending_failures_by_dest.pop(destination, [])

device_message_edus, device_stream_id = (
yield self._get_new_device_messages(destination)
)

pending_edus.extend(device_message_edus)

if pending_pdus:
logger.debug("TX [%s] len(pending_pdus_by_dest[dest]) = %d",
destination, len(pending_pdus))
Expand All @@ -184,13 +203,34 @@ def _attempt_new_transaction(self, destination):
return

yield self._send_new_transaction(
destination, pending_pdus, pending_edus, pending_failures
destination, pending_pdus, pending_edus, pending_failures,
device_stream_id,
should_delete_from_device_stream=bool(device_message_edus)
)

@defer.inlineCallbacks
def _get_new_device_messages(self, destination):
last_device_stream_id = self.last_device_stream_id_by_dest.get(destination, 0)
to_device_stream_id = self.store.get_to_device_stream_token()
contents, stream_id = yield self.store.get_new_device_msgs_for_remote(
destination, last_device_stream_id, to_device_stream_id
)
edus = [
Edu(
origin=self.server_name,
destination=destination,
edu_type="m.direct_to_device",
content=content,
)
for content in contents
]
defer.returnValue((edus, stream_id))

@measure_func("_send_new_transaction")
@defer.inlineCallbacks
def _send_new_transaction(self, destination, pending_pdus, pending_edus,
pending_failures):
pending_failures, device_stream_id,
should_delete_from_device_stream):

# Sort based on the order field
pending_pdus.sort(key=lambda t: t[1])
Expand All @@ -215,9 +255,9 @@ def _send_new_transaction(self, destination, pending_pdus, pending_edus,
"TX [%s] {%s} Attempting new transaction"
" (pdus: %d, edus: %d, failures: %d)",
destination, txn_id,
len(pending_pdus),
len(pending_edus),
len(pending_failures)
len(pdus),
len(edus),
len(failures)
)

logger.debug("TX [%s] Persisting transaction...", destination)
Expand All @@ -242,9 +282,9 @@ def _send_new_transaction(self, destination, pending_pdus, pending_edus,
" (PDUs: %d, EDUs: %d, failures: %d)",
destination, txn_id,
transaction.transaction_id,
len(pending_pdus),
len(pending_edus),
len(pending_failures),
len(pdus),
len(edus),
len(failures),
)

with limiter:
Expand Down Expand Up @@ -299,6 +339,13 @@ def json_data_cb():
logger.info(
"Failed to send event %s to %s", p.event_id, destination
)
else:
# Remove the acknowledged device messages from the database
if should_delete_from_device_stream:
yield self.store.delete_device_msgs_for_remote(
destination, device_stream_id
)
self.last_device_stream_id_by_dest[destination] = device_stream_id
except NotRetryingDestination:
logger.info(
"TX [%s] not ready for retry yet - "
Expand Down
117 changes: 117 additions & 0 deletions synapse/handlers/devicemessage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Copyright 2016 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from twisted.internet import defer

from synapse.types import get_domain_from_id
from synapse.util.stringutils import random_string


logger = logging.getLogger(__name__)


class DeviceMessageHandler(object):

def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
"""
self.store = hs.get_datastore()
self.notifier = hs.get_notifier()
self.is_mine_id = hs.is_mine_id
self.federation = hs.get_replication_layer()

self.federation.register_edu_handler(
"m.direct_to_device", self.on_direct_to_device_edu
)

@defer.inlineCallbacks
def on_direct_to_device_edu(self, origin, content):
local_messages = {}
sender_user_id = content["sender"]
if origin != get_domain_from_id(sender_user_id):
logger.warn(
"Dropping device message from %r with spoofed sender %r",
origin, sender_user_id
)
message_type = content["type"]
message_id = content["message_id"]
for user_id, by_device in content["messages"].items():
messages_by_device = {
device_id: {
"content": message_content,
"type": message_type,
"sender": sender_user_id,
}
for device_id, message_content in by_device.items()
}
if messages_by_device:
local_messages[user_id] = messages_by_device

stream_id = yield self.store.add_messages_from_remote_to_device_inbox(
origin, message_id, local_messages
)

self.notifier.on_new_event(
"to_device_key", stream_id, users=local_messages.keys()
)

@defer.inlineCallbacks
def send_device_message(self, sender_user_id, message_type, messages):

local_messages = {}
remote_messages = {}
for user_id, by_device in messages.items():
if self.is_mine_id(user_id):
messages_by_device = {
device_id: {
"content": message_content,
"type": message_type,
"sender": sender_user_id,
}
for device_id, message_content in by_device.items()
}
if messages_by_device:
local_messages[user_id] = messages_by_device
else:
destination = get_domain_from_id(user_id)
remote_messages.setdefault(destination, {})[user_id] = by_device

message_id = random_string(16)

remote_edu_contents = {}
for destination, messages in remote_messages.items():
remote_edu_contents[destination] = {
"messages": messages,
"sender": sender_user_id,
"type": message_type,
"message_id": message_id,
}

stream_id = yield self.store.add_messages_to_device_inbox(
local_messages, remote_edu_contents
)

self.notifier.on_new_event(
"to_device_key", stream_id, users=local_messages.keys()
)

for destination in remote_messages.keys():
# Enqueue a new federation transaction to send the new
# device messages to each remote destination.
self.federation.send_device_messages(destination)
11 changes: 11 additions & 0 deletions synapse/replication/slave/storage/deviceinbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ._base import BaseSlavedStore
from ._slaved_id_tracker import SlavedIdTracker
from synapse.storage import DataStore
from synapse.util.caches.stream_change_cache import StreamChangeCache


class SlavedDeviceInboxStore(BaseSlavedStore):
Expand All @@ -24,6 +25,10 @@ def __init__(self, db_conn, hs):
self._device_inbox_id_gen = SlavedIdTracker(
db_conn, "device_inbox", "stream_id",
)
self._device_inbox_stream_cache = StreamChangeCache(
"DeviceInboxStreamChangeCache",
self._device_inbox_id_gen.get_current_token()
)

get_to_device_stream_token = DataStore.get_to_device_stream_token.__func__
get_new_messages_for_device = DataStore.get_new_messages_for_device.__func__
Expand All @@ -38,5 +43,11 @@ def process_replication(self, result):
stream = result.get("to_device")
if stream:
self._device_inbox_id_gen.advance(int(stream["position"]))
for row in stream["rows"]:
stream_id = row[0]
user_id = row[1]
self._device_inbox_stream_cache.entity_has_changed(
user_id, stream_id
)

return super(SlavedDeviceInboxStore, self).process_replication(result)
33 changes: 7 additions & 26 deletions synapse/rest/client/v2_alpha/sendtodevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
import logging

from twisted.internet import defer
from synapse.http.servlet import parse_json_object_from_request

from synapse.http import servlet
from synapse.http.servlet import parse_json_object_from_request
from synapse.rest.client.v1.transactions import HttpTransactionStore

from ._base import client_v2_patterns

logger = logging.getLogger(__name__)
Expand All @@ -39,10 +40,8 @@ def __init__(self, hs):
super(SendToDeviceRestServlet, self).__init__()
self.hs = hs
self.auth = hs.get_auth()
self.store = hs.get_datastore()
self.notifier = hs.get_notifier()
self.is_mine_id = hs.is_mine_id
self.txns = HttpTransactionStore()
self.device_message_handler = hs.get_device_message_handler()

@defer.inlineCallbacks
def on_PUT(self, request, message_type, txn_id):
Expand All @@ -57,28 +56,10 @@ def on_PUT(self, request, message_type, txn_id):

content = parse_json_object_from_request(request)

# TODO: Prod the notifier to wake up sync streams.
# TODO: Implement replication for the messages.
# TODO: Send the messages to remote servers if needed.

local_messages = {}
for user_id, by_device in content["messages"].items():
if self.is_mine_id(user_id):
messages_by_device = {
device_id: {
"content": message_content,
"type": message_type,
"sender": requester.user.to_string(),
}
for device_id, message_content in by_device.items()
}
if messages_by_device:
local_messages[user_id] = messages_by_device

stream_id = yield self.store.add_messages_to_device_inbox(local_messages)

self.notifier.on_new_event(
"to_device_key", stream_id, users=local_messages.keys()
sender_user_id = requester.user.to_string()

yield self.device_message_handler.send_device_message(
sender_user_id, message_type, content["messages"]
)

response = (200, {})
Expand Down
5 changes: 5 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from synapse.handlers import Handlers
from synapse.handlers.appservice import ApplicationServicesHandler
from synapse.handlers.auth import AuthHandler
from synapse.handlers.devicemessage import DeviceMessageHandler
from synapse.handlers.device import DeviceHandler
from synapse.handlers.e2e_keys import E2eKeysHandler
from synapse.handlers.presence import PresenceHandler
Expand Down Expand Up @@ -100,6 +101,7 @@ def build_DEPENDENCY(self)
'application_service_api',
'application_service_scheduler',
'application_service_handler',
'device_message_handler',
'notifier',
'distributor',
'client_resource',
Expand Down Expand Up @@ -205,6 +207,9 @@ def build_auth_handler(self):
def build_device_handler(self):
return DeviceHandler(self)

def build_device_message_handler(self):
return DeviceMessageHandler(self)

def build_e2e_keys_handler(self):
return E2eKeysHandler(self)

Expand Down
Loading