-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Fix limit logic for AccountDataStream #7384
Changes from 4 commits
2a6ad9c
ba2b934
3356d9f
5f4e481
18b7a57
a85682e
4206e3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Fix a bug where event updates might not be sent over replication to worker processes after the stream falls behind. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,14 +14,27 @@ | |
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import heapq | ||
import logging | ||
from collections import namedtuple | ||
from typing import Any, Awaitable, Callable, List, Optional, Tuple | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Any, | ||
Awaitable, | ||
Callable, | ||
List, | ||
Optional, | ||
Tuple, | ||
TypeVar, | ||
) | ||
|
||
import attr | ||
|
||
from synapse.replication.http.streams import ReplicationGetStreamUpdates | ||
|
||
if TYPE_CHECKING: | ||
import synapse.server | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
# the number of rows to request from an update_function. | ||
|
@@ -37,7 +50,7 @@ | |
# parsing with Stream.parse_row (which turns it into a `ROW_TYPE`). Normally it's | ||
# just a row from a database query, though this is dependent on the stream in question. | ||
# | ||
StreamRow = Tuple | ||
StreamRow = TypeVar("StreamRow", bound=Tuple) | ||
|
||
# The type returned by the update_function of a stream, as well as get_updates(), | ||
# get_updates_since, etc. | ||
|
@@ -499,32 +512,61 @@ class AccountDataStream(Stream): | |
""" | ||
|
||
AccountDataStreamRow = namedtuple( | ||
"AccountDataStream", ("user_id", "room_id", "data_type") # str # str # str | ||
"AccountDataStream", | ||
("user_id", "room_id", "data_type"), # str # Optional[str] # str | ||
) | ||
|
||
NAME = "account_data" | ||
ROW_TYPE = AccountDataStreamRow | ||
|
||
def __init__(self, hs): | ||
def __init__(self, hs: "synapse.server.HomeServer"): | ||
self.store = hs.get_datastore() | ||
super().__init__( | ||
hs.get_instance_name(), | ||
self.store.get_max_account_data_stream_id, | ||
db_query_to_update_function(self._update_function), | ||
self._update_function, | ||
) | ||
|
||
async def _update_function(self, from_token, to_token, limit): | ||
global_results, room_results = await self.store.get_all_updated_account_data( | ||
from_token, from_token, to_token, limit | ||
async def _update_function( | ||
self, instance_name: str, from_token: int, to_token: int, limit: int | ||
) -> StreamUpdateResult: | ||
limited = False | ||
global_results = await self.store.get_updated_global_account_data( | ||
from_token, to_token, limit | ||
) | ||
|
||
results = list(room_results) | ||
results.extend( | ||
(stream_id, user_id, None, account_data_type) | ||
# if the global results hit the limit, we'll need to limit the room results to | ||
# the same stream token. | ||
if len(global_results) >= limit: | ||
to_token = global_results[-1][0] | ||
limited = True | ||
|
||
room_results = await self.store.get_updated_room_account_data( | ||
from_token, to_token, limit | ||
) | ||
|
||
# likewise, if the room results hit the limit, limit the global results to | ||
# the same stream token. | ||
if len(room_results) >= limit: | ||
to_token = room_results[-1][0] | ||
limited = True | ||
|
||
# convert the global results to the right format, and limit them to the to_token | ||
# at the same time | ||
global_rows = ( | ||
(stream_id, (user_id, None, account_data_type)) | ||
for stream_id, user_id, account_data_type in global_results | ||
if stream_id <= to_token | ||
) | ||
|
||
room_rows = ( | ||
(stream_id, (user_id, room_id, account_data_type)) | ||
for stream_id, user_id, room_id, account_data_type in room_results | ||
) | ||
|
||
return results | ||
# we need to return a sorted list, so merge them together. | ||
updates = list(heapq.merge(room_rows, global_rows)) | ||
return updates, to_token, limited | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason not to move some of this handling into the store? I'm mainly thinking it might be more efficient that way, if for no other reason that we would only need one transaction rather than two. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dunno, maybe? It feels like that would be a different approach to that taken by all the other stream sources: it would mean we'd have to mess about with Another way would be to follow the example of get_all_device_list_changes_for_remotes and do one query with a UNION, but I only found that after I wrote this stuff... |
||
|
||
|
||
class GroupServerStream(Stream): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2020 The Matrix.org Foundation C.I.C. | ||
# | ||
# 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. | ||
|
||
from synapse.replication.tcp.streams._base import ( | ||
_STREAM_UPDATE_TARGET_ROW_COUNT, | ||
AccountDataStream, | ||
) | ||
|
||
from tests.replication.tcp.streams._base import BaseStreamTestCase | ||
|
||
|
||
class AccountDataStreamTestCase(BaseStreamTestCase): | ||
def test_update_function_room_account_data_limit(self): | ||
"""Test replication with many room account data updates | ||
""" | ||
store = self.hs.get_datastore() | ||
|
||
# generate lots of account data updates | ||
updates = [] | ||
for i in range(_STREAM_UPDATE_TARGET_ROW_COUNT + 5): | ||
update = "m.test_type.%i" % (i,) | ||
self.get_success( | ||
store.add_account_data_to_room("test_user", "test_room", update, {}) | ||
) | ||
updates.append(update) | ||
|
||
# also one global update | ||
self.get_success(store.add_account_data_for_user("test_user", "m.global", {})) | ||
|
||
# tell the notifier to catch up to avoid duplicate rows. | ||
# workaround for https://github.com/matrix-org/synapse/issues/7360 | ||
# FIXME remove this when the above is fixed | ||
self.replicate() | ||
|
||
# check we're testing what we think we are: no rows should yet have been | ||
# received | ||
self.assertEqual([], self.test_handler.received_rdata_rows) | ||
|
||
# now reconnect to pull the updates | ||
self.reconnect() | ||
self.replicate() | ||
|
||
# we should have received all the expected rows in the right order | ||
received_rows = self.test_handler.received_rdata_rows | ||
|
||
for t in updates: | ||
(stream_name, token, row) = received_rows.pop(0) | ||
self.assertEqual(stream_name, AccountDataStream.NAME) | ||
self.assertIsInstance(row, AccountDataStream.AccountDataStreamRow) | ||
self.assertEqual(row.data_type, t) | ||
self.assertEqual(row.room_id, "test_room") | ||
|
||
(stream_name, token, row) = received_rows.pop(0) | ||
self.assertIsInstance(row, AccountDataStream.AccountDataStreamRow) | ||
self.assertEqual(row.data_type, "m.global") | ||
self.assertIsNone(row.room_id) | ||
|
||
self.assertEqual([], received_rows) | ||
|
||
def test_update_function_global_account_data_limit(self): | ||
"""Test replication with many global account data updates | ||
""" | ||
store = self.hs.get_datastore() | ||
|
||
# generate lots of account data updates | ||
updates = [] | ||
for i in range(_STREAM_UPDATE_TARGET_ROW_COUNT + 5): | ||
update = "m.test_type.%i" % (i,) | ||
self.get_success(store.add_account_data_for_user("test_user", update, {})) | ||
updates.append(update) | ||
|
||
# also one per-room update | ||
self.get_success( | ||
store.add_account_data_to_room("test_user", "test_room", "m.per_room", {}) | ||
) | ||
|
||
# tell the notifier to catch up to avoid duplicate rows. | ||
# workaround for https://github.com/matrix-org/synapse/issues/7360 | ||
# FIXME remove this when the above is fixed | ||
self.replicate() | ||
|
||
# check we're testing what we think we are: no rows should yet have been | ||
# received | ||
self.assertEqual([], self.test_handler.received_rdata_rows) | ||
|
||
# now reconnect to pull the updates | ||
self.reconnect() | ||
self.replicate() | ||
|
||
# we should have received all the expected rows in the right order | ||
received_rows = self.test_handler.received_rdata_rows | ||
|
||
for t in updates: | ||
(stream_name, token, row) = received_rows.pop(0) | ||
self.assertEqual(stream_name, AccountDataStream.NAME) | ||
self.assertIsInstance(row, AccountDataStream.AccountDataStreamRow) | ||
self.assertEqual(row.data_type, t) | ||
self.assertIsNone(row.room_id) | ||
|
||
(stream_name, token, row) = received_rows.pop(0) | ||
self.assertIsInstance(row, AccountDataStream.AccountDataStreamRow) | ||
self.assertEqual(row.data_type, "m.per_room") | ||
self.assertEqual(row.room_id, "test_room") | ||
|
||
self.assertEqual([], received_rows) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that technically this doesn't need a
if stream_id <= token
clause, but I think adding may make it less confusing why we're omitting it (or add a comment)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I added a comment.