[Core] Call wait_for_alert in a thread

This spawns a thread in alertmanager to call wait_for_alert in a thread.
This reduces latency to deluge responding to events.
And removes all `hasattr` checks in Component

Closes: https://github.com/deluge-torrent/deluge/pull/221
This commit is contained in:
DjLegolas 2018-10-22 03:24:20 +03:00 committed by Calum Lind
parent c7dc60571e
commit b5f8c5af2d
No known key found for this signature in database
GPG Key ID: 90597A687B836BA3
4 changed files with 85 additions and 10 deletions

View File

@ -64,9 +64,9 @@ class Component:
paused by instructing the :class:`ComponentRegistry` to pause
this Component.
**pause()** - This method is called when the component is being paused.
**pause()** - This method is called when the component is being paused.
**resume()** - This method is called when the component resumes from a Paused
**resume()** - This method is called when the component resumes from a Paused
state.
**shutdown()** - This method is called when the client is exiting. If the
@ -85,7 +85,7 @@ class Component:
**Stopped** - The Component has either been stopped or has yet to be started.
**Stopping** - The Component has had it's stop method called, but it hasn't
**Stopping** - The Component has had its stop method called, but it hasn't
fully stopped yet.
**Paused** - The Component has had its update timer stopped, but will

View File

@ -42,11 +42,13 @@ def mock_callback():
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
"""
def reset():
def reset(timeout=0.5, *args, **kwargs):
if mock.called:
original_reset_mock()
deferred = Deferred()
deferred.addTimeout(0.5, reactor)
original_reset_mock(*args, **kwargs)
if mock.deferred:
mock.deferred.cancel()
deferred = Deferred(canceller=lambda x: deferred.callback(None))
deferred.addTimeout(timeout, reactor)
mock.side_effect = lambda *args, **kw: deferred.callback((args, kw))
mock.deferred = deferred

View File

@ -16,11 +16,12 @@ This should typically only be used by the Core. Plugins should utilize the
"""
import contextlib
import logging
import threading
from collections import defaultdict
from types import SimpleNamespace
from typing import Any, Callable
from twisted.internet import reactor
from twisted.internet import reactor, threads
import deluge.component as component
from deluge._libtorrent import lt
@ -34,7 +35,7 @@ class AlertManager(component.Component):
def __init__(self):
log.debug('AlertManager init...')
component.Component.__init__(self, 'AlertManager', interval=0.3)
component.Component.__init__(self, 'AlertManager')
self.session = component.get('Core').session
# Increase the alert queue size so that alerts don't get lost.
@ -57,10 +58,17 @@ class AlertManager(component.Component):
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
self.handlers = defaultdict(list)
self.delayed_calls = []
self._event = threading.Event()
def update(self):
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
self.handle_alerts()
def start(self):
thread = threading.Thread(
target=self.wait_for_alert_in_thread, name='alert-poller', daemon=True
)
thread.start()
self._event.set()
def stop(self):
for delayed_call in self.delayed_calls:
@ -68,6 +76,23 @@ class AlertManager(component.Component):
delayed_call.cancel()
self.delayed_calls = []
def pause(self):
self._event.clear()
def resume(self):
self._event.set()
def wait_for_alert_in_thread(self):
while self._component_state not in ('Stopping', 'Stopped'):
if self.session.wait_for_alert(1000) is None:
continue
if self._event.wait():
threads.blockingCallFromThread(reactor, self.maybe_handle_alerts)
def maybe_handle_alerts(self):
if self._component_state == 'Started':
self.handle_alerts()
def register_handler(self, alert_type: str, handler: Callable[[Any], None]) -> None:
"""
Registers a function that will be called when 'alert_type' is pop'd

View File

@ -3,17 +3,47 @@
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from types import SimpleNamespace
import pytest_twisted
import deluge.component as component
from deluge.conftest import BaseTestCase
from deluge.core.core import Core
class DummyAlert1:
def __init__(self):
self.message = '1'
class DummyAlert2:
def __init__(self):
self.message = '2'
class SessionMock:
def __init__(self):
self.alerts = []
def set_alerts(self):
self.alerts = [DummyAlert1(), DummyAlert2()]
def wait_for_alert(self, timeout):
return self.alerts[0] if len(self.alerts) > 0 else None
def pop_alerts(self):
alerts = self.alerts
self.alerts = []
return alerts
class TestAlertManager(BaseTestCase):
def set_up(self):
self.core = Core()
self.core.config.config['lsd'] = False
self.am = component.get('AlertManager')
self.am.session = SessionMock()
return component.start(['AlertManager'])
def tear_down(self):
@ -28,6 +58,24 @@ class TestAlertManager(BaseTestCase):
assert self.am.handlers['dummy1'] == [handler]
assert self.am.handlers['dummy2'] == [handler]
@pytest_twisted.ensureDeferred
async def test_pop_alert(self, mock_callback):
mock_callback.reset_mock()
self.am.register_handler('DummyAlert1', mock_callback)
self.am.session.set_alerts()
await mock_callback.deferred
mock_callback.assert_called_once_with(SimpleNamespace(message='1'))
@pytest_twisted.ensureDeferred
async def test_pause_not_pop_alert(self, mock_callback):
await component.pause(['AlertManager'])
self.am.register_handler('DummyAlert1', mock_callback)
self.am.session.set_alerts()
await mock_callback.deferred
mock_callback.assert_not_called()
assert not self.am._event.isSet()
assert len(self.am.session.alerts) == 2
def test_deregister_handler(self):
def handler(alert):
...