summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDjLegolas <DjLegolas@users.noreply.github.com>2018-10-22 02:24:20 +0200
committerCalum Lind <calumlind+deluge@gmail.com>2023-11-27 16:00:56 +0100
commitb5f8c5af2dcc0bf34bfae5073b5f951066fc3483 (patch)
treecbdad08d61919bddd1479933902b8794d7b6e13c
parent[CI] Fix windows packaging build (diff)
downloaddeluge-b5f8c5af2dcc0bf34bfae5073b5f951066fc3483.tar.xz
deluge-b5f8c5af2dcc0bf34bfae5073b5f951066fc3483.zip
[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
-rw-r--r--deluge/component.py6
-rw-r--r--deluge/conftest.py10
-rw-r--r--deluge/core/alertmanager.py31
-rw-r--r--deluge/tests/test_alertmanager.py48
4 files changed, 85 insertions, 10 deletions
diff --git a/deluge/component.py b/deluge/component.py
index fec510903..421f49a7b 100644
--- a/deluge/component.py
+++ b/deluge/component.py
@@ -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
diff --git a/deluge/conftest.py b/deluge/conftest.py
index d394a271e..ca25cce8e 100644
--- a/deluge/conftest.py
+++ b/deluge/conftest.py
@@ -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
diff --git a/deluge/core/alertmanager.py b/deluge/core/alertmanager.py
index 51a7f2993..452c159bf 100644
--- a/deluge/core/alertmanager.py
+++ b/deluge/core/alertmanager.py
@@ -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
diff --git a/deluge/tests/test_alertmanager.py b/deluge/tests/test_alertmanager.py
index 683b011b4..576e76693 100644
--- a/deluge/tests/test_alertmanager.py
+++ b/deluge/tests/test_alertmanager.py
@@ -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):
...