threadbased.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import asyncio
  2. import inspect
  3. import logging
  4. import queue
  5. import sys
  6. import threading
  7. import traceback
  8. from functools import wraps
  9. from .base import AbstractSession
  10. from ..exceptions import SessionNotFoundException, SessionClosedException
  11. from ..utils import random_str, LimitedSizeQueue
  12. logger = logging.getLogger(__name__)
  13. """
  14. 基于线程的会话实现
  15. 主任务线程退出后,连接关闭,但不会清理主任务线程产生的其他线程
  16. 客户端连接关闭后,后端线程不会退出,但是再次调用输入输出函数会引发异常
  17. todo: thread 重名
  18. """
  19. # todo 线程安全
  20. class ThreadBasedSession(AbstractSession):
  21. thread2session = {} # thread_id -> session
  22. unhandled_task_mq_maxsize = 1000
  23. event_mq_maxsize = 100
  24. callback_mq_maxsize = 100
  25. _active_session_cnt = 0
  26. @classmethod
  27. def active_session_count(cls):
  28. return cls._active_session_cnt
  29. @classmethod
  30. def get_current_session(cls) -> "ThreadBasedSession":
  31. curr = id(threading.current_thread())
  32. session = cls.thread2session.get(curr)
  33. if session is None:
  34. raise SessionNotFoundException(
  35. "Can't find current session. Maybe session closed. Did you forget to use `register_thread` ?")
  36. return session
  37. @classmethod
  38. def get_current_task_id(cls):
  39. return cls._get_task_id(threading.current_thread())
  40. @staticmethod
  41. def _get_task_id(thread: threading.Thread):
  42. tname = getattr(thread, '_target', 'task')
  43. tname = getattr(tname, '__name__', tname)
  44. return '%s-%s' % (tname, id(thread))
  45. def __init__(self, target, on_task_command=None, on_session_close=None, loop=None):
  46. """
  47. :param target: 会话运行的函数
  48. :param on_task_command: 当Task内发送Command给session的时候触发的处理函数
  49. :param on_session_close: 会话结束的处理函数
  50. :param loop: 事件循环。若 on_task_command 或者 on_session_close 中有调用使用asyncio事件循环的调用,
  51. 则需要事件循环实例来将回调在事件循环的线程中执行
  52. """
  53. assert (not asyncio.iscoroutinefunction(target)) and (not inspect.isgeneratorfunction(target)), ValueError(
  54. "ThreadBasedSession only accept a simple function as task function, "
  55. "not coroutine function or generator function. ")
  56. ThreadBasedSession._active_session_cnt += 1
  57. self._on_task_command = on_task_command or (lambda _: None)
  58. self._on_session_close = on_session_close or (lambda: None)
  59. self._loop = loop
  60. self.threads = [] # 注册到当前会话的线程集合
  61. self.unhandled_task_msgs = LimitedSizeQueue(maxsize=self.unhandled_task_mq_maxsize)
  62. self.task_mqs = {} # task_id -> event msg queue
  63. self._closed = False
  64. # 用于实现回调函数的注册
  65. self.callback_mq = None
  66. self.callback_thread = None
  67. self.callbacks = {} # callback_id -> (callback_func, is_mutex)
  68. self._start_main_task(target)
  69. def _start_main_task(self, target):
  70. @wraps(target)
  71. def main_task(target):
  72. try:
  73. target()
  74. except Exception as e:
  75. self.on_task_exception()
  76. finally:
  77. for t in self.threads:
  78. if t.is_alive() and t is not threading.current_thread():
  79. t.join()
  80. self.send_task_command(dict(command='close_session'))
  81. self._trigger_close_event()
  82. self.close()
  83. thread = threading.Thread(target=main_task, kwargs=dict(target=target),
  84. daemon=True, name='main_task')
  85. self.register_thread(thread)
  86. thread.start()
  87. def send_task_command(self, command):
  88. """向会话发送来自协程内的消息
  89. :param dict command: 消息
  90. """
  91. self.unhandled_task_msgs.put(command)
  92. if self._loop:
  93. self._loop.call_soon_threadsafe(self._on_task_command, self)
  94. else:
  95. self._on_task_command(self)
  96. def next_client_event(self):
  97. task_id = self.get_current_task_id()
  98. event_mq = self.get_current_session().task_mqs.get(task_id)
  99. return event_mq.get()
  100. def send_client_event(self, event):
  101. """向会话发送来自用户浏览器的事件️
  102. :param dict event: 事件️消息
  103. """
  104. task_id = event['task_id']
  105. mq = self.task_mqs.get(task_id)
  106. if not mq and task_id in self.callbacks:
  107. mq = self.callback_mq
  108. if not mq:
  109. logger.error('event_mqs not found, task_id:%s', task_id)
  110. return
  111. mq.put(event)
  112. def get_task_commands(self):
  113. return self.unhandled_task_msgs.get()
  114. def _trigger_close_event(self):
  115. """触发Backend on_session_close callback"""
  116. if self._loop:
  117. self._loop.call_soon_threadsafe(self._on_session_close)
  118. else:
  119. self._on_session_close()
  120. def _cleanup(self):
  121. self.task_mqs = {}
  122. self.unhandled_task_msgs.wait_empty(8)
  123. if not self.unhandled_task_msgs.empty():
  124. logger.debug("Unhandled task messages when session close:%s", self.unhandled_task_msgs.get())
  125. raise RuntimeError('There are unhandled task messages when session close!')
  126. for t in self.threads:
  127. del ThreadBasedSession.thread2session[id(t)]
  128. if self.callback_mq is not None: # 回调功能已经激活
  129. self.callback_mq.put(None) # 结束回调线程
  130. ThreadBasedSession._active_session_cnt -= 1
  131. def close(self):
  132. """关闭当前Session。由Backend调用"""
  133. if self._closed:
  134. return
  135. self._closed = True
  136. self._cleanup()
  137. def closed(self):
  138. return self._closed
  139. def on_task_exception(self):
  140. from ..output import put_markdown # todo
  141. logger.exception('Error in coroutine executing')
  142. type, value, tb = sys.exc_info()
  143. tb_len = len(list(traceback.walk_tb(tb)))
  144. lines = traceback.format_exception(type, value, tb, limit=1 - tb_len)
  145. traceback_msg = ''.join(lines)
  146. try:
  147. put_markdown("发生错误:\n```\n%s\n```" % traceback_msg)
  148. except:
  149. pass
  150. def _activate_callback_env(self):
  151. """激活回调功能
  152. ThreadBasedSession 的回调实现原理是:创建一个单独的线程用于接收回调事件,进而调用相关的回调函数。
  153. 当用户Task中并没有使用到回调功能时,不必开启此线程,可以节省资源
  154. """
  155. if self.callback_mq is not None: # 回调功能已经激活
  156. return
  157. self.callback_mq = queue.Queue(maxsize=self.callback_mq_maxsize)
  158. self.callback_thread = threading.Thread(target=self._dispatch_callback_event,
  159. daemon=True, name='callback-' + random_str(10))
  160. self.register_thread(self.callback_thread)
  161. self.callback_thread.start()
  162. logger.debug('Callback thread start')
  163. def _dispatch_callback_event(self):
  164. while not self.closed():
  165. event = self.callback_mq.get()
  166. if event is None: # 结束信号
  167. break
  168. callback_info = self.callbacks.get(event['task_id'])
  169. if not callback_info:
  170. logger.error("No callback for callback_id:%s", event['task_id'])
  171. return
  172. callback, mutex = callback_info
  173. @wraps(callback)
  174. def run(callback):
  175. try:
  176. callback(event['data'])
  177. except:
  178. # 子类可能会重写 get_current_session ,所以不要用 ThreadBasedSession.get_current_session 来调用
  179. self.get_current_session().on_task_exception()
  180. if mutex:
  181. run(callback)
  182. else:
  183. t = threading.Thread(target=run, kwargs=dict(callback=callback),
  184. daemon=True)
  185. self.register_thread(t)
  186. t.start()
  187. def register_callback(self, callback, serial_mode=False):
  188. """ 向Session注册一个回调函数,返回回调id
  189. Session需要保证当收到前端发送的事件消息 ``{event: "callback",task_id: 回调id, data:...}`` 时,
  190. ``callback`` 回调函数被执行, 并传入事件消息中的 ``data`` 字段值作为参数
  191. :param bool serial_mode: 串行模式模式。若为 ``True`` ,则对于同一组件的点击事件,串行执行其回调函数
  192. """
  193. assert (not asyncio.iscoroutinefunction(callback)) and (not inspect.isgeneratorfunction(callback)), ValueError(
  194. "In ThreadBasedSession.register_callback, `callback` must be a simple function, "
  195. "not coroutine function or generator function. ")
  196. self._activate_callback_env()
  197. callback_id = 'CB-%s-%s' % (getattr(callback, '__name__', ''), random_str(10))
  198. self.callbacks[callback_id] = (callback, serial_mode)
  199. return callback_id
  200. def register_thread(self, t: threading.Thread):
  201. """将线程注册到当前会话,以便在线程内调用 pywebio 交互函数。
  202. 会话会一直保持直到所有通过 `register_thread` 注册的线程以及当前会话的主任务线程退出
  203. :param threading.Thread thread: 线程对象
  204. """
  205. self.threads.append(t)
  206. self.thread2session[id(t)] = self
  207. event_mq = queue.Queue(maxsize=self.event_mq_maxsize)
  208. self.task_mqs[self._get_task_id(t)] = event_mq
  209. class ScriptModeSession(ThreadBasedSession):
  210. """Script mode的会话实现"""
  211. @classmethod
  212. def get_current_session(cls) -> "ScriptModeSession":
  213. if cls.instance is None:
  214. raise SessionNotFoundException("Can't find current session. It might be a bug.")
  215. if cls.instance.closed():
  216. raise SessionClosedException()
  217. return cls.instance
  218. @classmethod
  219. def get_current_task_id(cls):
  220. task_id = super().get_current_task_id()
  221. session = cls.get_current_session()
  222. if task_id not in session.task_mqs:
  223. session.register_thread(threading.current_thread())
  224. return task_id
  225. instance = None
  226. def __init__(self, thread, on_task_command=None, loop=None):
  227. """
  228. :param on_task_command: 会话结束的处理函数。后端Backend在相应on_session_close时关闭连接时,
  229. 需要保证会话内的所有消息都传送到了客户端
  230. :param loop: 事件循环。若 on_task_command 或者on_session_close中有调用使用asyncio事件循环的调用,
  231. 则需要事件循环实例来将回调在事件循环的线程中执行
  232. """
  233. if ScriptModeSession.instance is not None:
  234. raise RuntimeError("ScriptModeSession can only be created once.")
  235. ScriptModeSession.instance = self
  236. ThreadBasedSession._active_session_cnt += 1
  237. self._on_task_command = on_task_command or (lambda _: None)
  238. self._on_session_close = lambda: None
  239. self._loop = loop
  240. self.threads = [] # 当前会话的线程
  241. self.unhandled_task_msgs = LimitedSizeQueue(maxsize=self.unhandled_task_mq_maxsize)
  242. self.task_mqs = {} # task_id -> event msg queue
  243. self._closed = False
  244. # 用于实现回调函数的注册
  245. self.callback_mq = None
  246. self.callback_thread = None
  247. self.callbacks = {} # callback_id -> (callback_func, is_mutex)
  248. tid = id(thread)
  249. event_mq = queue.Queue(maxsize=self.event_mq_maxsize)
  250. self.task_mqs[tid] = event_mq