core.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. # -----------------------------------------------------------------------------
  2. # Copyright (C) 2011-2012 Travis Cline
  3. #
  4. # This file is part of pyzmq
  5. # It is adapted from upstream project zeromq_gevent under the New BSD License
  6. #
  7. # Distributed under the terms of the New BSD License. The full license is in
  8. # the file LICENSE.BSD, distributed as part of this software.
  9. # -----------------------------------------------------------------------------
  10. """This module wraps the :class:`Socket` and :class:`Context` found in :mod:`pyzmq <zmq>` to be non blocking"""
  11. from __future__ import annotations
  12. import sys
  13. import time
  14. import warnings
  15. import gevent
  16. from gevent.event import AsyncResult
  17. from gevent.hub import get_hub
  18. import zmq
  19. from zmq import Context as _original_Context
  20. from zmq import Socket as _original_Socket
  21. from .poll import _Poller
  22. if hasattr(zmq, 'RCVTIMEO'):
  23. TIMEOS: tuple = (zmq.RCVTIMEO, zmq.SNDTIMEO)
  24. else:
  25. TIMEOS = ()
  26. def _stop(evt):
  27. """simple wrapper for stopping an Event, allowing for method rename in gevent 1.0"""
  28. try:
  29. evt.stop()
  30. except AttributeError:
  31. # gevent<1.0 compat
  32. evt.cancel()
  33. class _Socket(_original_Socket):
  34. """Green version of :class:`zmq.Socket`
  35. The following methods are overridden:
  36. * send
  37. * recv
  38. To ensure that the ``zmq.NOBLOCK`` flag is set and that sending or receiving
  39. is deferred to the hub if a ``zmq.EAGAIN`` (retry) error is raised.
  40. The `__state_changed` method is triggered when the zmq.FD for the socket is
  41. marked as readable and triggers the necessary read and write events (which
  42. are waited for in the recv and send methods).
  43. Some double underscore prefixes are used to minimize pollution of
  44. :class:`zmq.Socket`'s namespace.
  45. """
  46. __in_send_multipart = False
  47. __in_recv_multipart = False
  48. __writable = None
  49. __readable = None
  50. _state_event = None
  51. _gevent_bug_timeout = 11.6 # timeout for not trusting gevent
  52. _debug_gevent = False # turn on if you think gevent is missing events
  53. _poller_class = _Poller
  54. _repr_cls = "zmq.green.Socket"
  55. def __init__(self, *a, **kw):
  56. super().__init__(*a, **kw)
  57. self.__in_send_multipart = False
  58. self.__in_recv_multipart = False
  59. self.__setup_events()
  60. def __del__(self):
  61. self.close()
  62. def close(self, linger=None):
  63. super().close(linger)
  64. self.__cleanup_events()
  65. def __cleanup_events(self):
  66. # close the _state_event event, keeps the number of active file descriptors down
  67. if getattr(self, '_state_event', None):
  68. _stop(self._state_event)
  69. self._state_event = None
  70. # if the socket has entered a close state resume any waiting greenlets
  71. self.__writable.set()
  72. self.__readable.set()
  73. def __setup_events(self):
  74. self.__readable = AsyncResult()
  75. self.__writable = AsyncResult()
  76. self.__readable.set()
  77. self.__writable.set()
  78. try:
  79. self._state_event = get_hub().loop.io(
  80. self.getsockopt(zmq.FD), 1
  81. ) # read state watcher
  82. self._state_event.start(self.__state_changed)
  83. except AttributeError:
  84. # for gevent<1.0 compatibility
  85. from gevent.core import read_event
  86. self._state_event = read_event(
  87. self.getsockopt(zmq.FD), self.__state_changed, persist=True
  88. )
  89. def __state_changed(self, event=None, _evtype=None):
  90. if self.closed:
  91. self.__cleanup_events()
  92. return
  93. try:
  94. # avoid triggering __state_changed from inside __state_changed
  95. events = super().getsockopt(zmq.EVENTS)
  96. except zmq.ZMQError as exc:
  97. self.__writable.set_exception(exc)
  98. self.__readable.set_exception(exc)
  99. else:
  100. if events & zmq.POLLOUT:
  101. self.__writable.set()
  102. if events & zmq.POLLIN:
  103. self.__readable.set()
  104. def _wait_write(self):
  105. assert self.__writable.ready(), "Only one greenlet can be waiting on this event"
  106. self.__writable = AsyncResult()
  107. # timeout is because libzmq cannot be trusted to properly signal a new send event:
  108. # this is effectively a maximum poll interval of 1s
  109. tic = time.time()
  110. dt = self._gevent_bug_timeout
  111. if dt:
  112. timeout = gevent.Timeout(seconds=dt)
  113. else:
  114. timeout = None
  115. try:
  116. if timeout:
  117. timeout.start()
  118. self.__writable.get(block=True)
  119. except gevent.Timeout as t:
  120. if t is not timeout:
  121. raise
  122. toc = time.time()
  123. # gevent bug: get can raise timeout even on clean return
  124. # don't display zmq bug warning for gevent bug (this is getting ridiculous)
  125. if (
  126. self._debug_gevent
  127. and timeout
  128. and toc - tic > dt
  129. and self.getsockopt(zmq.EVENTS) & zmq.POLLOUT
  130. ):
  131. print(
  132. f"BUG: gevent may have missed a libzmq send event on {self.FD}!",
  133. file=sys.stderr,
  134. )
  135. finally:
  136. if timeout:
  137. timeout.close()
  138. self.__writable.set()
  139. def _wait_read(self):
  140. assert self.__readable.ready(), "Only one greenlet can be waiting on this event"
  141. self.__readable = AsyncResult()
  142. # timeout is because libzmq cannot always be trusted to play nice with libevent.
  143. # I can only confirm that this actually happens for send, but lets be symmetrical
  144. # with our dirty hacks.
  145. # this is effectively a maximum poll interval of 1s
  146. tic = time.time()
  147. dt = self._gevent_bug_timeout
  148. if dt:
  149. timeout = gevent.Timeout(seconds=dt)
  150. else:
  151. timeout = None
  152. try:
  153. if timeout:
  154. timeout.start()
  155. self.__readable.get(block=True)
  156. except gevent.Timeout as t:
  157. if t is not timeout:
  158. raise
  159. toc = time.time()
  160. # gevent bug: get can raise timeout even on clean return
  161. # don't display zmq bug warning for gevent bug (this is getting ridiculous)
  162. if (
  163. self._debug_gevent
  164. and timeout
  165. and toc - tic > dt
  166. and self.getsockopt(zmq.EVENTS) & zmq.POLLIN
  167. ):
  168. print(
  169. f"BUG: gevent may have missed a libzmq recv event on {self.FD}!",
  170. file=sys.stderr,
  171. )
  172. finally:
  173. if timeout:
  174. timeout.close()
  175. self.__readable.set()
  176. def send(self, data, flags=0, copy=True, track=False, **kwargs):
  177. """send, which will only block current greenlet
  178. state_changed always fires exactly once (success or fail) at the
  179. end of this method.
  180. """
  181. # if we're given the NOBLOCK flag act as normal and let the EAGAIN get raised
  182. if flags & zmq.NOBLOCK:
  183. try:
  184. msg = super().send(data, flags, copy, track, **kwargs)
  185. finally:
  186. if not self.__in_send_multipart:
  187. self.__state_changed()
  188. return msg
  189. # ensure the zmq.NOBLOCK flag is part of flags
  190. flags |= zmq.NOBLOCK
  191. while True: # Attempt to complete this operation indefinitely, blocking the current greenlet
  192. try:
  193. # attempt the actual call
  194. msg = super().send(data, flags, copy, track)
  195. except zmq.ZMQError as e:
  196. # if the raised ZMQError is not EAGAIN, reraise
  197. if e.errno != zmq.EAGAIN:
  198. if not self.__in_send_multipart:
  199. self.__state_changed()
  200. raise
  201. else:
  202. if not self.__in_send_multipart:
  203. self.__state_changed()
  204. return msg
  205. # defer to the event loop until we're notified the socket is writable
  206. self._wait_write()
  207. def recv(self, flags=0, copy=True, track=False):
  208. """recv, which will only block current greenlet
  209. state_changed always fires exactly once (success or fail) at the
  210. end of this method.
  211. """
  212. if flags & zmq.NOBLOCK:
  213. try:
  214. msg = super().recv(flags, copy, track)
  215. finally:
  216. if not self.__in_recv_multipart:
  217. self.__state_changed()
  218. return msg
  219. flags |= zmq.NOBLOCK
  220. while True:
  221. try:
  222. msg = super().recv(flags, copy, track)
  223. except zmq.ZMQError as e:
  224. if e.errno != zmq.EAGAIN:
  225. if not self.__in_recv_multipart:
  226. self.__state_changed()
  227. raise
  228. else:
  229. if not self.__in_recv_multipart:
  230. self.__state_changed()
  231. return msg
  232. self._wait_read()
  233. def recv_into(self, buffer, /, *, nbytes=0, flags=0):
  234. """recv_into, which will only block current greenlet"""
  235. if flags & zmq.DONTWAIT:
  236. return super().recv_into(buffer, nbytes=nbytes, flags=flags)
  237. flags |= zmq.DONTWAIT
  238. while True:
  239. try:
  240. recvd = super().recv_into(buffer, nbytes=nbytes, flags=flags)
  241. except zmq.ZMQError as e:
  242. if e.errno != zmq.EAGAIN:
  243. self.__state_changed()
  244. raise
  245. else:
  246. self.__state_changed()
  247. return recvd
  248. self._wait_read()
  249. def send_multipart(self, *args, **kwargs):
  250. """wrap send_multipart to prevent state_changed on each partial send"""
  251. self.__in_send_multipart = True
  252. try:
  253. msg = super().send_multipart(*args, **kwargs)
  254. finally:
  255. self.__in_send_multipart = False
  256. self.__state_changed()
  257. return msg
  258. def recv_multipart(self, *args, **kwargs):
  259. """wrap recv_multipart to prevent state_changed on each partial recv"""
  260. self.__in_recv_multipart = True
  261. try:
  262. msg = super().recv_multipart(*args, **kwargs)
  263. finally:
  264. self.__in_recv_multipart = False
  265. self.__state_changed()
  266. return msg
  267. def get(self, opt):
  268. """trigger state_changed on getsockopt(EVENTS)"""
  269. if opt in TIMEOS:
  270. warnings.warn(
  271. "TIMEO socket options have no effect in zmq.green", UserWarning
  272. )
  273. optval = super().get(opt)
  274. if opt == zmq.EVENTS:
  275. self.__state_changed()
  276. return optval
  277. def set(self, opt, val):
  278. """set socket option"""
  279. if opt in TIMEOS:
  280. warnings.warn(
  281. "TIMEO socket options have no effect in zmq.green", UserWarning
  282. )
  283. return super().set(opt, val)
  284. class _Context(_original_Context[_Socket]):
  285. """Replacement for :class:`zmq.Context`
  286. Ensures that the greened Socket above is used in calls to `socket`.
  287. """
  288. _socket_class = _Socket
  289. _repr_cls = "zmq.green.Context"
  290. # avoid sharing instance with base Context class
  291. _instance = None