From e3135b7aa75e06f4c8036d219b6634e1077481b4 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sat, 2 Nov 2019 22:32:40 +0100 Subject: [PATCH] bot: keep track of trigger processing threads --- sopel/bot.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/sopel/bot.py b/sopel/bot.py index 66a159d763..021e27ba41 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -44,6 +44,8 @@ def __init__(self, config, daemon=False): super(Sopel, self).__init__(config) self._daemon = daemon # Used for iPython. TODO something saner here self.wantsrestart = False + self._running_triggers = [] + self._running_triggers_lock = threading.Lock() # `re.compile('.*') is re.compile('.*')` because of caching, so we need # to associate a list with each regex, since they are unexpectedly @@ -650,6 +652,8 @@ def dispatch(self, pretrigger): :meth:`~get_triggered_callables`. This method is also responsible for telling ``dispatch`` if the function is blocked or not. """ + # list of commands running in separate threads for this dispatch + running_triggers = [] # nickname/hostname blocking nick_blocked = host_blocked = self._is_pretrigger_blocked(pretrigger) blocked = bool(nick_blocked or host_blocked) @@ -659,11 +663,9 @@ def dispatch(self, pretrigger): for priority in ('high', 'medium', 'low'): items = self.get_triggered_callables(priority, pretrigger, blocked) for func, trigger, is_blocked in items: + function_name = "%s.%s" % (func.__module__, func.__name__) # skip running blocked functions, but track them for logging if is_blocked: - function_name = "%s.%s" % ( - func.__module__, func.__name__ - ) list_of_blocked_functions.append(function_name) continue @@ -671,12 +673,17 @@ def dispatch(self, pretrigger): wrapper = SopelWrapper( self, trigger, output_prefix=func.output_prefix) if func.thread: + # run in a separate thread targs = (func, wrapper, trigger) t = threading.Thread(target=self.call, args=targs) + t.name = '%s-%s' % (t.name, function_name) t.start() + running_triggers.append(t) else: + # direct call self.call(func, wrapper, trigger) + # log blocked functions if list_of_blocked_functions: if nick_blocked and host_blocked: block_type = 'both' @@ -684,13 +691,45 @@ def dispatch(self, pretrigger): block_type = 'nick' else: block_type = 'host' - LOGGER.info( - "[%s]%s prevented from using %s.", + LOGGER.debug( + "[%s] %s prevented from using %s.", block_type, pretrigger.nick, ', '.join(list_of_blocked_functions) ) + # update currently running triggers + self._update_running_triggers(running_triggers) + + @property + def running_triggers(self): + """Current active threads for triggers. + + This read-only list contains the currently running thread processing + a trigger. This is for testing and debugging purpose only. + """ + with self._running_triggers_lock: + return [t for t in self._running_triggers if t.is_alive()] + + def _update_running_triggers(self, running_triggers): + """Update list of running triggers. + + :param list running_triggers: new started threads + + We want to keep track of running triggers, mostly for testing and + debugging purpose. For instance, it'll help make sure, in test, that + a bot's plugin finished processing of a trigger, by manually joining + all running threads. + + This is kept private, as this is pure internal machinery and isn't made + to be manipulated by an outside code. + """ + # update bot's global running triggers + with self._running_triggers_lock: + running_triggers = running_triggers + self._running_triggers + self._running_triggers = [ + t for t in running_triggers if t.is_alive()] + def on_scheduler_error(self, scheduler, exc): """Called when the Job Scheduler fails.