[FIX] bus: pop unused channels from channel map

When a websocket is disconnected, its subscription is removed.
Each channel leads to a set of subscribed websockets. When no
more sockets are listening to a channel, this channel should be
pop from this mapping or the map will keep growing.

closes odoo/odoo#98766

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
tsm-odoo
2022-08-25 03:11:51 +02:00
parent ae97357675
commit bf9ae3bbc8
2 changed files with 67 additions and 5 deletions
+17 -5
View File
@@ -151,9 +151,13 @@ class ImDispatch(threading.Thread):
given channels will be sent through the websocket. If a subscription
is already present, overwrite it.
"""
channels = [channel_with_db(db, c) for c in channels]
channels = {hashable(channel_with_db(db, c)) for c in channels}
subscription = self._ws_to_subscription.get(websocket)
if subscription:
outdated_channels = subscription.channels - channels
self._clear_outdated_channels(websocket, outdated_channels)
for channel in channels:
self._channels_to_ws.setdefault(hashable(channel), set()).add(websocket)
self._channels_to_ws.setdefault(channel, set()).add(websocket)
self._ws_to_subscription[websocket] = BusSubscription(channels, last)
if not self.is_alive():
self.start()
@@ -161,9 +165,17 @@ class ImDispatch(threading.Thread):
self._dispatch_notifications(websocket)
def unsubscribe(self, websocket):
self._ws_to_subscription.pop(websocket, None)
for websockets in self._channels_to_ws.values():
websockets.discard(websocket)
websocket_subscription = self._ws_to_subscription.pop(websocket, None)
if not websocket_subscription:
return
self._clear_outdated_channels(websocket, websocket_subscription.channels)
def _clear_outdated_channels(self, websocket, outdated_channels):
""" Remove channels from channel to websocket map. """
for channel in outdated_channels:
self._channels_to_ws[channel].remove(websocket)
if not self._channels_to_ws[channel]:
self._channels_to_ws.pop(channel)
def loop(self):
""" Dispatch postgres notifications to the relevant websockets """
@@ -5,6 +5,7 @@ import json
from collections import defaultdict
from datetime import timedelta
from freezegun import freeze_time
from threading import Event
from unittest.mock import patch
from odoo.api import Environment
@@ -140,3 +141,52 @@ class TestWebsocketCaryall(WebsocketCase):
self.env['bus.bus']._sendone('channel1', 'notif type', 'message')
dispatch._dispatch_notifications(next(iter(dispatch._ws_to_subscription.keys())))
self.assert_close_with_code(websocket, CloseCode.SESSION_EXPIRED)
def test_channel_subscription_disconnect(self):
subscribe_done_event = Event()
original_subscribe = dispatch.subscribe
def patched_subscribe(*args):
original_subscribe(*args)
subscribe_done_event.set()
with patch.object(dispatch, 'subscribe', patched_subscribe):
websocket = self.websocket_connect()
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel'], 'last': 0}
}))
subscribe_done_event.wait(timeout=5)
# channel is added as expected to the channel to websocket map.
self.assertIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)
websocket.close(CloseCode.CLEAN)
self.wait_remaining_websocket_connections()
# channel is removed as expected when removing the last
# websocket that was listening to this channel.
self.assertNotIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)
def test_channel_subscription_update(self):
subscribe_done_event = Event()
original_subscribe = dispatch.subscribe
def patched_subscribe(*args):
original_subscribe(*args)
subscribe_done_event.set()
with patch.object(dispatch, 'subscribe', patched_subscribe):
websocket = self.websocket_connect()
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel'], 'last': 0}
}))
subscribe_done_event.wait(timeout=5)
subscribe_done_event.clear()
# channel is added as expected to the channel to websocket map.
self.assertIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel_2'], 'last': 0}
}))
subscribe_done_event.wait(timeout=5)
# channel is removed as expected when updating the subscription.
self.assertNotIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)