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

Commit a271e23

Browse files
Add a linearizer on (appservice, stream) when handling ephemeral events. (#11207)
Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
1 parent af54167 commit a271e23

File tree

3 files changed

+103
-18
lines changed

3 files changed

+103
-18
lines changed

changelog.d/11207.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug which could result in serialization errors and potentially duplicate transaction data when sending ephemeral events to application services. Contributed by @Fizzadar at Beeper.

synapse/handlers/appservice.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
)
3535
from synapse.storage.databases.main.directory import RoomAliasMapping
3636
from synapse.types import JsonDict, RoomAlias, RoomStreamToken, UserID
37+
from synapse.util.async_helpers import Linearizer
3738
from synapse.util.metrics import Measure
3839

3940
if TYPE_CHECKING:
@@ -58,6 +59,10 @@ def __init__(self, hs: "HomeServer"):
5859
self.current_max = 0
5960
self.is_processing = False
6061

62+
self._ephemeral_events_linearizer = Linearizer(
63+
name="appservice_ephemeral_events"
64+
)
65+
6166
def notify_interested_services(self, max_token: RoomStreamToken) -> None:
6267
"""Notifies (pushes) all application services interested in this event.
6368
@@ -260,26 +265,37 @@ async def _notify_interested_services_ephemeral(
260265
events = await self._handle_typing(service, new_token)
261266
if events:
262267
self.scheduler.submit_ephemeral_events_for_as(service, events)
268+
continue
263269

264-
elif stream_key == "receipt_key":
265-
events = await self._handle_receipts(service)
266-
if events:
267-
self.scheduler.submit_ephemeral_events_for_as(service, events)
268-
269-
# Persist the latest handled stream token for this appservice
270-
await self.store.set_type_stream_id_for_appservice(
271-
service, "read_receipt", new_token
270+
# Since we read/update the stream position for this AS/stream
271+
with (
272+
await self._ephemeral_events_linearizer.queue(
273+
(service.id, stream_key)
272274
)
275+
):
276+
if stream_key == "receipt_key":
277+
events = await self._handle_receipts(service, new_token)
278+
if events:
279+
self.scheduler.submit_ephemeral_events_for_as(
280+
service, events
281+
)
282+
283+
# Persist the latest handled stream token for this appservice
284+
await self.store.set_type_stream_id_for_appservice(
285+
service, "read_receipt", new_token
286+
)
273287

274-
elif stream_key == "presence_key":
275-
events = await self._handle_presence(service, users)
276-
if events:
277-
self.scheduler.submit_ephemeral_events_for_as(service, events)
288+
elif stream_key == "presence_key":
289+
events = await self._handle_presence(service, users, new_token)
290+
if events:
291+
self.scheduler.submit_ephemeral_events_for_as(
292+
service, events
293+
)
278294

279-
# Persist the latest handled stream token for this appservice
280-
await self.store.set_type_stream_id_for_appservice(
281-
service, "presence", new_token
282-
)
295+
# Persist the latest handled stream token for this appservice
296+
await self.store.set_type_stream_id_for_appservice(
297+
service, "presence", new_token
298+
)
283299

284300
async def _handle_typing(
285301
self, service: ApplicationService, new_token: int
@@ -316,7 +332,9 @@ async def _handle_typing(
316332
)
317333
return typing
318334

319-
async def _handle_receipts(self, service: ApplicationService) -> List[JsonDict]:
335+
async def _handle_receipts(
336+
self, service: ApplicationService, new_token: Optional[int]
337+
) -> List[JsonDict]:
320338
"""
321339
Return the latest read receipts that the given application service should receive.
322340
@@ -335,14 +353,23 @@ async def _handle_receipts(self, service: ApplicationService) -> List[JsonDict]:
335353
from_key = await self.store.get_type_stream_id_for_appservice(
336354
service, "read_receipt"
337355
)
356+
if new_token is not None and new_token <= from_key:
357+
logger.debug(
358+
"Rejecting token lower than or equal to stored: %s" % (new_token,)
359+
)
360+
return []
361+
338362
receipts_source = self.event_sources.sources.receipt
339363
receipts, _ = await receipts_source.get_new_events_as(
340364
service=service, from_key=from_key
341365
)
342366
return receipts
343367

344368
async def _handle_presence(
345-
self, service: ApplicationService, users: Collection[Union[str, UserID]]
369+
self,
370+
service: ApplicationService,
371+
users: Collection[Union[str, UserID]],
372+
new_token: Optional[int],
346373
) -> List[JsonDict]:
347374
"""
348375
Return the latest presence updates that the given application service should receive.
@@ -365,6 +392,12 @@ async def _handle_presence(
365392
from_key = await self.store.get_type_stream_id_for_appservice(
366393
service, "presence"
367394
)
395+
if new_token is not None and new_token <= from_key:
396+
logger.debug(
397+
"Rejecting token lower than or equal to stored: %s" % (new_token,)
398+
)
399+
return []
400+
368401
for user in users:
369402
if isinstance(user, str):
370403
user = UserID.from_string(user)

tests/handlers/test_appservice.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def setUp(self):
4040
hs.get_application_service_scheduler.return_value = self.mock_scheduler
4141
hs.get_clock.return_value = MockClock()
4242
self.handler = ApplicationServicesHandler(hs)
43+
self.event_source = hs.get_event_sources()
4344

4445
def test_notify_interested_services(self):
4546
interested_service = self._mkservice(is_interested=True)
@@ -252,6 +253,56 @@ async def get_3pe_protocol(service, unusedProtocol):
252253
},
253254
)
254255

256+
def test_notify_interested_services_ephemeral(self):
257+
"""
258+
Test sending ephemeral events to the appservice handler are scheduled
259+
to be pushed out to interested appservices, and that the stream ID is
260+
updated accordingly.
261+
"""
262+
interested_service = self._mkservice(is_interested=True)
263+
services = [interested_service]
264+
265+
self.mock_store.get_app_services.return_value = services
266+
self.mock_store.get_type_stream_id_for_appservice.return_value = make_awaitable(
267+
579
268+
)
269+
270+
event = Mock(event_id="event_1")
271+
self.event_source.sources.receipt.get_new_events_as.return_value = (
272+
make_awaitable(([event], None))
273+
)
274+
275+
self.handler.notify_interested_services_ephemeral("receipt_key", 580)
276+
self.mock_scheduler.submit_ephemeral_events_for_as.assert_called_once_with(
277+
interested_service, [event]
278+
)
279+
self.mock_store.set_type_stream_id_for_appservice.assert_called_once_with(
280+
interested_service,
281+
"read_receipt",
282+
580,
283+
)
284+
285+
def test_notify_interested_services_ephemeral_out_of_order(self):
286+
"""
287+
Test sending out of order ephemeral events to the appservice handler
288+
are ignored.
289+
"""
290+
interested_service = self._mkservice(is_interested=True)
291+
services = [interested_service]
292+
293+
self.mock_store.get_app_services.return_value = services
294+
self.mock_store.get_type_stream_id_for_appservice.return_value = make_awaitable(
295+
580
296+
)
297+
298+
event = Mock(event_id="event_1")
299+
self.event_source.sources.receipt.get_new_events_as.return_value = (
300+
make_awaitable(([event], None))
301+
)
302+
303+
self.handler.notify_interested_services_ephemeral("receipt_key", 579)
304+
self.mock_scheduler.submit_ephemeral_events_for_as.assert_not_called()
305+
255306
def _mkservice(self, is_interested, protocols=None):
256307
service = Mock()
257308
service.is_interested.return_value = make_awaitable(is_interested)

0 commit comments

Comments
 (0)