Changeset 704682


Ignore:
Timestamp:
01/21/2024 02:16:22 PM (18 months ago)
Author:
Calum Lind <calumlind+deluge@gmail.com>
Branches:
develop, master
Children:
b7450b
Parents:
fa8d19
git-author:
Calum Lind <calumlind+deluge@gmail.com> (12/03/2023 07:47:11 PM)
git-committer:
Calum Lind <calumlind+deluge@gmail.com> (01/21/2024 02:16:22 PM)
Message:

[Alerts] Fix alert handler segfault on lt.pop_alerts

We cannot handle an alert after calling lt.pop_alerts for a subsequent
time since the alert objects are invalidated and with cause a segfault.

To resolve this issue add a timeout to the handler calls and wait in the
alert thread for either the handlers to be called or eventually be
cancelled before getting more alerts.

This is still not an ideal solution and might leave to backlog of alerts
but this is better than crashing the application. Perhaps the timeout
could be tweaked to be shorter for certain alert types such as stats.

Related: https://github.com/arvidn/libtorrent/issues/6437

File:
1 edited

Legend:

Unmodified
Added
Removed
  • deluge/core/alertmanager.py

    rfa8d19 r704682  
    1818import logging
    1919import threading
     20import time
    2021from collections import defaultdict
     22from functools import partial
    2123from typing import Any, Callable
    2224
    23 from twisted.internet import reactor, threads
     25from twisted.internet import reactor, task, threads
    2426
    2527import deluge.component as component
     
    5759        # handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
    5860        self.handlers = defaultdict(list)
    59         self.handlers_retry_timeout = 0.3
    60         self.handlers_retry_count = 6
     61        self.handlers_timeout_secs = 2
    6162        self.delayed_calls = []
    6263        self._event = threading.Event()
     
    8384    def wait_for_alert_in_thread(self):
    8485        while self._component_state not in ('Stopping', 'Stopped'):
     86            if self.check_delayed_calls():
     87                time.sleep(0.05)
     88                continue
     89
    8590            if self.session.wait_for_alert(1000) is None:
    8691                continue
     
    8893                threads.blockingCallFromThread(reactor, self.maybe_handle_alerts)
    8994
     95    def on_delayed_call_timeout(self, result, timeout, **kwargs):
     96        log.warning('Alert handler was timed-out before being called %s', kwargs)
     97
    9098    def cancel_delayed_calls(self):
    9199        """Cancel all delayed handlers."""
    92100        for delayed_call in self.delayed_calls:
    93             if delayed_call.active():
    94                 delayed_call.cancel()
     101            delayed_call.cancel()
    95102        self.delayed_calls = []
    96103
    97     def check_delayed_calls(self, retries: int = 0) -> bool:
    98         """Returns True if any handler calls are delayed (upto retry limit)."""
    99         self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
    100         if not self.delayed_calls:
    101             return False
     104    def check_delayed_calls(self) -> bool:
     105        """Returns True if any handler calls are delayed."""
     106        self.delayed_calls = [dc for dc in self.delayed_calls if not dc.called]
     107        return len(self.delayed_calls) > 0
    102108
    103         if retries > self.handlers_retry_count:
    104             log.warning(
    105                 'Alert handlers timeout reached, cancelling: %s', self.delayed_calls
    106             )
    107             self.cancel_delayed_calls()
    108             return False
    109 
    110         return True
    111 
    112     def maybe_handle_alerts(self, retries: int = 0) -> None:
     109    def maybe_handle_alerts(self) -> None:
    113110        if self._component_state != 'Started':
    114             return
    115 
    116         if self.check_delayed_calls(retries):
    117             log.debug('Waiting for delayed alerts: %s', self.delayed_calls)
    118             retries += 1
    119             reactor.callLater(
    120                 self.handlers_retry_timeout, self.maybe_handle_alerts, retries
    121             )
    122111            return
    123112
     
    183172                if log.isEnabledFor(logging.DEBUG):
    184173                    log.debug('Handling alert: %s', alert_type)
    185 
    186                 self.delayed_calls.append(reactor.callLater(0, handler, alert))
     174                d = task.deferLater(reactor, 0, handler, alert)
     175                on_handler_timeout = partial(
     176                    self.on_delayed_call_timeout,
     177                    handler=handler.__qualname__,
     178                    alert_type=alert_type,
     179                )
     180                d.addTimeout(
     181                    self.handlers_timeout_secs,
     182                    reactor,
     183                    onTimeoutCancel=on_handler_timeout,
     184                )
     185                self.delayed_calls.append(d)
    187186
    188187    def set_alert_queue_size(self, queue_size):
Note: See TracChangeset for help on using the changeset viewer.