Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/zmq/eventloop/zmqstream.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

292 statements  

1# Derived from iostream.py from tornado 1.0, Copyright 2009 Facebook 

2# Used under Apache License Version 2.0 

3# 

4# Modifications are Copyright (C) PyZMQ Developers 

5# Distributed under the terms of the Modified BSD License. 

6"""A utility class for event-based messaging on a zmq socket using tornado. 

7 

8.. seealso:: 

9 

10 - :mod:`zmq.asyncio` 

11 - :mod:`zmq.eventloop.future` 

12""" 

13 

14from __future__ import annotations 

15 

16import asyncio 

17import pickle 

18import warnings 

19from collections.abc import Awaitable, Sequence 

20from queue import Queue 

21from typing import TYPE_CHECKING, Any, Callable, Literal, overload 

22 

23from tornado.ioloop import IOLoop 

24from tornado.log import gen_log 

25 

26import zmq 

27import zmq._future 

28from zmq import POLLIN, POLLOUT 

29from zmq.utils import jsonapi 

30 

31if TYPE_CHECKING: 

32 from typing_extensions import Concatenate, ParamSpec, Self, TypeAlias, TypeVar 

33 

34 from zmq.sugar.socket import _SocketContext 

35 

36 _RecvT = TypeVar("_RecvT", bound=bytes | zmq.Frame) 

37 _P = ParamSpec("_P") 

38 

39 _Ignored: TypeAlias = object 

40 

41 _CallbackRecv: TypeAlias = Callable[[list[_RecvT]], _Ignored] 

42 _CallbackRecvStream: TypeAlias = Callable[["ZMQStream", list[_RecvT]], _Ignored] 

43 

44 _CallbackSend: TypeAlias = Callable[ 

45 [list[Any], zmq.MessageTracker | None], 

46 _Ignored, 

47 ] 

48 

49 

50class ZMQStream: 

51 """A utility class to register callbacks when a zmq socket sends and receives 

52 

53 For use with tornado IOLoop. 

54 

55 There are three main methods 

56 

57 Methods: 

58 

59 * **on_recv(callback, copy=True):** 

60 register a callback to be run every time the socket has something to receive 

61 * **on_send(callback):** 

62 register a callback to be run every time you call send 

63 * **send_multipart(self, msg, flags=0, copy=False, callback=None):** 

64 perform a send that will trigger the callback 

65 if callback is passed, on_send is also called. 

66 

67 There are also send_multipart(), send_json(), send_pyobj() 

68 

69 Three other methods for deactivating the callbacks: 

70 

71 * **stop_on_recv():** 

72 turn off the recv callback 

73 * **stop_on_send():** 

74 turn off the send callback 

75 

76 which simply call ``on_<evt>(None)``. 

77 

78 The entire socket interface, excluding direct recv methods, is also 

79 provided, primarily through direct-linking the methods. 

80 e.g. 

81 

82 >>> stream.bind is stream.socket.bind 

83 True 

84 

85 

86 .. versionadded:: 25 

87 

88 send/recv callbacks can be coroutines. 

89 

90 .. versionchanged:: 25 

91 

92 ZMQStreams only support base zmq.Socket classes (this has always been true, but not enforced). 

93 If ZMQStreams are created with e.g. async Socket subclasses, 

94 a RuntimeWarning will be shown, 

95 and the socket cast back to the default zmq.Socket 

96 before connecting events. 

97 

98 Previously, using async sockets (or any zmq.Socket subclass) would result in undefined behavior for the 

99 arguments passed to callback functions. 

100 Now, the callback functions reliably get the return value of the base `zmq.Socket` send/recv_multipart methods 

101 (the list of message frames). 

102 """ 

103 

104 socket: zmq.Socket 

105 io_loop: IOLoop 

106 poller: zmq.Poller 

107 _send_queue: Queue 

108 _recv_callback: Callable | None 

109 _send_callback: Callable | None 

110 _close_callback: Callable | None 

111 _state: int = 0 

112 _flushed: bool = False 

113 _recv_copy: bool = False 

114 _fd: int 

115 

116 bind: Callable[[str], _SocketContext[zmq.Socket]] 

117 bind_to_random_port: Callable[Concatenate[str, ...], int] 

118 connect: Callable[[str], _SocketContext[zmq.Socket]] 

119 setsockopt: Callable[[int, int | bytes], None] 

120 getsockopt: Callable[[int], int | bytes] 

121 setsockopt_string: Callable[Concatenate[int, str, ...], None] 

122 setsockopt_unicode: Callable[Concatenate[int, str, ...], None] 

123 getsockopt_string: Callable[Concatenate[int, ...], str] 

124 getsockopt_unicode: Callable[Concatenate[int, ...], str] 

125 

126 def __init__(self, socket: zmq.Socket, io_loop: IOLoop | None = None) -> None: 

127 if isinstance(socket, zmq._future._AsyncSocket): 

128 warnings.warn( 

129 f"""ZMQStream only supports the base zmq.Socket class. 

130 

131 Use zmq.Socket(shadow=other_socket) 

132 or `ctx.socket(zmq.{socket._type_name}, socket_class=zmq.Socket)` 

133 to create a base zmq.Socket object, 

134 no matter what other kind of socket your Context creates. 

135 """, 

136 RuntimeWarning, 

137 stacklevel=2, 

138 ) 

139 # shadow back to base zmq.Socket, 

140 # otherwise callbacks like `on_recv` will get the wrong types. 

141 socket = zmq.Socket(shadow=socket) 

142 self.socket = socket 

143 

144 # IOLoop.current() is deprecated if called outside the event loop 

145 # that means 

146 self.io_loop = io_loop or IOLoop.current() 

147 self.poller = zmq.Poller() 

148 self._fd = self.socket.FD 

149 

150 self._send_queue = Queue() 

151 self._recv_callback = None 

152 self._send_callback = None 

153 self._close_callback = None 

154 self._recv_copy = False 

155 self._flushed = False 

156 

157 self._state = 0 

158 self._init_io_state() 

159 

160 # shortcircuit some socket methods 

161 self.bind = self.socket.bind 

162 self.bind_to_random_port = self.socket.bind_to_random_port 

163 self.connect = self.socket.connect 

164 self.setsockopt = self.socket.setsockopt 

165 self.getsockopt = self.socket.getsockopt 

166 self.setsockopt_string = self.socket.setsockopt_string 

167 self.getsockopt_string = self.socket.getsockopt_string 

168 self.setsockopt_unicode = self.socket.setsockopt_unicode 

169 self.getsockopt_unicode = self.socket.getsockopt_unicode 

170 

171 def stop_on_recv(self) -> None: 

172 """Disable callback and automatic receiving.""" 

173 return self.on_recv(None) 

174 

175 def stop_on_send(self) -> None: 

176 """Disable callback on sending.""" 

177 return self.on_send(None) 

178 

179 def stop_on_err(self) -> None: 

180 """DEPRECATED, does nothing""" 

181 gen_log.warning("on_err does nothing, and will be removed") 

182 

183 def on_err(self, callback: Callable) -> None: 

184 """DEPRECATED, does nothing""" 

185 gen_log.warning("on_err does nothing, and will be removed") 

186 

187 @overload 

188 def on_recv( 

189 self, 

190 callback: _CallbackRecv[bytes] | None, 

191 copy: Literal[True] = True, 

192 ) -> None: ... 

193 @overload 

194 def on_recv( 

195 self, 

196 callback: _CallbackRecv[zmq.Frame], 

197 copy: Literal[False], 

198 ) -> None: ... 

199 @overload 

200 def on_recv( 

201 self, 

202 callback: _CallbackRecv[_RecvT] | None, 

203 copy: bool = True, 

204 ) -> None: ... 

205 def on_recv( 

206 self, 

207 callback: _CallbackRecv[_RecvT] | None, 

208 copy: bool = True, 

209 ) -> None: 

210 """Register a callback for when a message is ready to recv. 

211 

212 There can be only one callback registered at a time, so each 

213 call to `on_recv` replaces previously registered callbacks. 

214 

215 on_recv(None) disables recv event polling. 

216 

217 Use on_recv_stream(callback) instead, to register a callback that will receive 

218 both this ZMQStream and the message, instead of just the message. 

219 

220 Parameters 

221 ---------- 

222 

223 callback : callable 

224 callback must take exactly one argument, which will be a 

225 list, as returned by socket.recv_multipart() 

226 if callback is None, recv callbacks are disabled. 

227 copy : bool 

228 copy is passed directly to recv, so if copy is False, 

229 callback will receive Message objects. If copy is True, 

230 then callback will receive bytes/str objects. 

231 

232 Returns : None 

233 """ 

234 

235 self._check_closed() 

236 assert callback is None or callable(callback) 

237 self._recv_callback = callback 

238 self._recv_copy = copy 

239 if callback is None: 

240 self._drop_io_state(zmq.POLLIN) 

241 else: 

242 self._add_io_state(zmq.POLLIN) 

243 

244 @overload 

245 def on_recv_stream( 

246 self, 

247 callback: _CallbackRecvStream[bytes] | None, 

248 copy: Literal[True] = True, 

249 ) -> None: ... 

250 @overload 

251 def on_recv_stream( 

252 self, 

253 callback: _CallbackRecvStream[zmq.Frame], 

254 copy: Literal[False], 

255 ) -> None: ... 

256 @overload 

257 def on_recv_stream( 

258 self, 

259 callback: _CallbackRecvStream[_RecvT] | None, 

260 copy: bool = True, 

261 ) -> None: ... 

262 def on_recv_stream( 

263 self, 

264 callback: _CallbackRecvStream[_RecvT] | None, 

265 copy: bool = True, 

266 ) -> None: 

267 """Same as on_recv, but callback will get this stream as first argument 

268 

269 callback must take exactly two arguments, as it will be called as:: 

270 

271 callback(stream, msg) 

272 

273 Useful when a single callback should be used with multiple streams. 

274 """ 

275 if callback is None: 

276 self.stop_on_recv() 

277 else: 

278 

279 def stream_callback(msg): 

280 return callback(self, msg) 

281 

282 self.on_recv(stream_callback, copy=copy) 

283 

284 def on_send(self, callback: _CallbackSend | None) -> None: 

285 """Register a callback to be called on each send 

286 

287 There will be two arguments:: 

288 

289 callback(msg, status) 

290 

291 * `msg` will be the list of sendable objects that was just sent 

292 * `status` will be the return result of socket.send_multipart(msg) - 

293 MessageTracker or None. 

294 

295 Non-copying sends return a MessageTracker object whose 

296 `done` attribute will be True when the send is complete. 

297 This allows users to track when an object is safe to write to 

298 again. 

299 

300 The second argument will always be None if copy=True 

301 on the send. 

302 

303 Use on_send_stream(callback) to register a callback that will be passed 

304 this ZMQStream as the first argument, in addition to the other two. 

305 

306 on_send(None) disables recv event polling. 

307 

308 Parameters 

309 ---------- 

310 

311 callback : callable 

312 callback must take exactly two arguments, which will be 

313 the message being sent (always a list), 

314 and the return result of socket.send_multipart(msg) - 

315 MessageTracker or None. 

316 

317 if callback is None, send callbacks are disabled. 

318 """ 

319 

320 self._check_closed() 

321 assert callback is None or callable(callback) 

322 self._send_callback = callback 

323 

324 def on_send_stream( 

325 self, 

326 callback: Callable[[Self, list[Any], zmq.MessageTracker | None], Any] | None, 

327 ) -> None: 

328 """Same as on_send, but callback will get this stream as first argument 

329 

330 Callback will be passed three arguments:: 

331 

332 callback(stream, msg, status) 

333 

334 Useful when a single callback should be used with multiple streams. 

335 """ 

336 if callback is None: 

337 self.stop_on_send() 

338 else: 

339 self.on_send(lambda msg, status: callback(self, msg, status)) 

340 

341 def send( 

342 self, 

343 msg: Any, 

344 flags: int = 0, 

345 copy: bool = True, 

346 track: bool = False, 

347 callback: _CallbackSend | None = None, 

348 **kwargs: Any, 

349 ) -> None: 

350 """Send a message, optionally also register a new callback for sends. 

351 See zmq.socket.send for details. 

352 """ 

353 return self.send_multipart( 

354 [msg], flags=flags, copy=copy, track=track, callback=callback, **kwargs 

355 ) 

356 

357 def send_multipart( 

358 self, 

359 msg: Sequence[Any], 

360 flags: int = 0, 

361 copy: bool = True, 

362 track: bool = False, 

363 callback: _CallbackSend | None = None, 

364 **kwargs: Any, 

365 ) -> None: 

366 """Send a multipart message, optionally also register a new callback for sends. 

367 See zmq.socket.send_multipart for details. 

368 """ 

369 kwargs.update(dict(flags=flags, copy=copy, track=track)) 

370 self._send_queue.put((msg, kwargs)) 

371 callback = callback or self._send_callback 

372 if callback is not None: 

373 self.on_send(callback) 

374 else: 

375 # noop callback 

376 self.on_send(lambda *args: None) 

377 self._add_io_state(zmq.POLLOUT) 

378 

379 def send_string( 

380 self, 

381 u: str, 

382 flags: int = 0, 

383 encoding: str = 'utf-8', 

384 callback: _CallbackSend | None = None, 

385 **kwargs: Any, 

386 ) -> None: 

387 """Send a unicode message with an encoding. 

388 See zmq.socket.send_unicode for details. 

389 """ 

390 if not isinstance(u, str): 

391 raise TypeError("unicode/str objects only") 

392 return self.send(u.encode(encoding), flags=flags, callback=callback, **kwargs) 

393 

394 send_unicode = send_string 

395 

396 def send_json( 

397 self, 

398 obj: Any, 

399 flags: int = 0, 

400 callback: _CallbackSend | None = None, 

401 **kwargs: Any, 

402 ) -> None: 

403 """Send json-serialized version of an object. 

404 See zmq.socket.send_json for details. 

405 """ 

406 msg = jsonapi.dumps(obj) 

407 return self.send(msg, flags=flags, callback=callback, **kwargs) 

408 

409 def send_pyobj( 

410 self, 

411 obj: Any, 

412 flags: int = 0, 

413 protocol: int = -1, 

414 callback: _CallbackSend | None = None, 

415 **kwargs: Any, 

416 ) -> None: 

417 """Send a Python object as a message using pickle to serialize. 

418 

419 See zmq.socket.send_json for details. 

420 """ 

421 msg = pickle.dumps(obj, protocol) 

422 return self.send(msg, flags, callback=callback, **kwargs) 

423 

424 def _finish_flush(self) -> None: 

425 """callback for unsetting _flushed flag.""" 

426 self._flushed = False 

427 

428 def flush( 

429 self, 

430 flag: int = zmq.POLLIN | zmq.POLLOUT, 

431 limit: int | None = None, 

432 ) -> int: 

433 """Flush pending messages. 

434 

435 This method safely handles all pending incoming and/or outgoing messages, 

436 bypassing the inner loop, passing them to the registered callbacks. 

437 

438 A limit can be specified, to prevent blocking under high load. 

439 

440 flush will return the first time ANY of these conditions are met: 

441 * No more events matching the flag are pending. 

442 * the total number of events handled reaches the limit. 

443 

444 Note that if ``flag|POLLIN != 0``, recv events will be flushed even if no callback 

445 is registered, unlike normal IOLoop operation. This allows flush to be 

446 used to remove *and ignore* incoming messages. 

447 

448 Parameters 

449 ---------- 

450 flag : int 

451 default=POLLIN|POLLOUT 

452 0MQ poll flags. 

453 If flag|POLLIN, recv events will be flushed. 

454 If flag|POLLOUT, send events will be flushed. 

455 Both flags can be set at once, which is the default. 

456 limit : None or int, optional 

457 The maximum number of messages to send or receive. 

458 Both send and recv count against this limit. 

459 

460 Returns 

461 ------- 

462 int : 

463 count of events handled (both send and recv) 

464 """ 

465 self._check_closed() 

466 # unset self._flushed, so callbacks will execute, in case flush has 

467 # already been called this iteration 

468 already_flushed = self._flushed 

469 self._flushed = False 

470 # initialize counters 

471 count = 0 

472 

473 def update_flag(): 

474 """Update the poll flag, to prevent registering POLLOUT events 

475 if we don't have pending sends.""" 

476 return flag & zmq.POLLIN | (self.sending() and flag & zmq.POLLOUT) 

477 

478 flag = update_flag() 

479 if not flag: 

480 # nothing to do 

481 return 0 

482 self.poller.register(self.socket, flag) 

483 events = self.poller.poll(0) 

484 while events and (not limit or count < limit): 

485 s, event = events[0] 

486 if event & POLLIN: # receiving 

487 self._handle_recv() 

488 count += 1 

489 if self.socket is None: 

490 # break if socket was closed during callback 

491 break 

492 if event & POLLOUT and self.sending(): 

493 self._handle_send() 

494 count += 1 

495 if self.socket is None: 

496 # break if socket was closed during callback 

497 break 

498 

499 flag = update_flag() 

500 if flag: 

501 self.poller.register(self.socket, flag) 

502 events = self.poller.poll(0) 

503 else: 

504 events = [] 

505 if count: # only bypass loop if we actually flushed something 

506 # skip send/recv callbacks this iteration 

507 self._flushed = True 

508 # reregister them at the end of the loop 

509 if not already_flushed: # don't need to do it again 

510 self.io_loop.add_callback(self._finish_flush) 

511 elif already_flushed: 

512 self._flushed = True 

513 

514 # update ioloop poll state, which may have changed 

515 self._rebuild_io_state() 

516 return count 

517 

518 def set_close_callback(self, callback: Callable[[], _Ignored] | None) -> None: 

519 """Call the given callback when the stream is closed.""" 

520 self._close_callback = callback 

521 

522 def close(self, linger: int | None = None) -> None: 

523 """Close this stream.""" 

524 if self.socket is not None: 

525 if self.socket.closed: 

526 # fallback on raw fd for closed sockets 

527 # hopefully this happened promptly after close, 

528 # otherwise somebody else may have the FD 

529 warnings.warn( 

530 f"Unregistering FD {self._fd} after closing socket. " 

531 "This could result in unregistering handlers for the wrong socket. " 

532 "Please use stream.close() instead of closing the socket directly.", 

533 stacklevel=2, 

534 ) 

535 self.io_loop.remove_handler(self._fd) 

536 else: 

537 self.io_loop.remove_handler(self.socket) 

538 self.socket.close(linger) 

539 self.socket = None # type: ignore 

540 if self._close_callback: 

541 self._run_callback(self._close_callback) 

542 

543 def receiving(self) -> bool: 

544 """Returns True if we are currently receiving from the stream.""" 

545 return self._recv_callback is not None 

546 

547 def sending(self) -> bool: 

548 """Returns True if we are currently sending to the stream.""" 

549 return not self._send_queue.empty() 

550 

551 def closed(self) -> bool: 

552 if self.socket is None: 

553 return True 

554 if self.socket.closed: 

555 # underlying socket has been closed, but not by us! 

556 # trigger our cleanup 

557 self.close() 

558 return True 

559 return False 

560 

561 def _run_callback( 

562 self, 

563 callback: Callable[_P, Any], 

564 *args: _P.args, 

565 **kwargs: _P.kwargs, 

566 ) -> None: 

567 """Wrap running callbacks in try/except to allow us to 

568 close our socket.""" 

569 try: 

570 f = callback(*args, **kwargs) 

571 if isinstance(f, Awaitable): 

572 f = asyncio.ensure_future(f) 

573 else: 

574 f = None 

575 except Exception: 

576 gen_log.error("Uncaught exception in ZMQStream callback", exc_info=True) 

577 # Re-raise the exception so that IOLoop.handle_callback_exception 

578 # can see it and log the error 

579 raise 

580 

581 if f is not None: 

582 # handle async callbacks 

583 def _log_error(f): 

584 try: 

585 f.result() 

586 except Exception: 

587 gen_log.error( 

588 "Uncaught exception in ZMQStream callback", exc_info=True 

589 ) 

590 

591 f.add_done_callback(_log_error) 

592 

593 def _handle_events(self, fd, events): 

594 """This method is the actual handler for IOLoop, that gets called whenever 

595 an event on my socket is posted. It dispatches to _handle_recv, etc.""" 

596 if not self.socket: 

597 gen_log.warning("Got events for closed stream %s", self) 

598 return 

599 try: 

600 zmq_events = self.socket.EVENTS 

601 except zmq.ContextTerminated: 

602 gen_log.warning("Got events for stream %s after terminating context", self) 

603 # trigger close check, this will unregister callbacks 

604 self.closed() 

605 return 

606 except zmq.ZMQError as e: 

607 # run close check 

608 # shadow sockets may have been closed elsewhere, 

609 # which should show up as ENOTSOCK here 

610 if self.closed(): 

611 gen_log.warning( 

612 "Got events for stream %s attached to closed socket: %s", self, e 

613 ) 

614 else: 

615 gen_log.error("Error getting events for %s: %s", self, e) 

616 return 

617 try: 

618 # dispatch events: 

619 if zmq_events & zmq.POLLIN and self.receiving(): 

620 self._handle_recv() 

621 if not self.socket: 

622 return 

623 if zmq_events & zmq.POLLOUT and self.sending(): 

624 self._handle_send() 

625 if not self.socket: 

626 return 

627 

628 # rebuild the poll state 

629 self._rebuild_io_state() 

630 except Exception: 

631 gen_log.error("Uncaught exception in zmqstream callback", exc_info=True) 

632 raise 

633 

634 def _handle_recv(self): 

635 """Handle a recv event.""" 

636 if self._flushed: 

637 return 

638 try: 

639 msg = self.socket.recv_multipart(zmq.NOBLOCK, copy=self._recv_copy) 

640 except zmq.ZMQError as e: 

641 if e.errno == zmq.EAGAIN: 

642 # state changed since poll event 

643 pass 

644 else: 

645 raise 

646 else: 

647 if self._recv_callback: 

648 callback = self._recv_callback 

649 self._run_callback(callback, msg) 

650 

651 def _handle_send(self): 

652 """Handle a send event.""" 

653 if self._flushed: 

654 return 

655 if not self.sending(): 

656 gen_log.error("Shouldn't have handled a send event") 

657 return 

658 

659 msg, kwargs = self._send_queue.get() 

660 try: 

661 status = self.socket.send_multipart(msg, **kwargs) 

662 except zmq.ZMQError as e: 

663 gen_log.error("SEND Error: %s", e) 

664 status = e 

665 if self._send_callback: 

666 callback = self._send_callback 

667 self._run_callback(callback, msg, status) 

668 

669 def _check_closed(self): 

670 if not self.socket: 

671 raise OSError("Stream is closed") 

672 

673 def _rebuild_io_state(self): 

674 """rebuild io state based on self.sending() and receiving()""" 

675 if self.socket is None: 

676 return 

677 state = 0 

678 if self.receiving(): 

679 state |= zmq.POLLIN 

680 if self.sending(): 

681 state |= zmq.POLLOUT 

682 

683 self._state = state 

684 self._update_handler(state) 

685 

686 def _add_io_state(self, state): 

687 """Add io_state to poller.""" 

688 self._state = self._state | state 

689 self._update_handler(self._state) 

690 

691 def _drop_io_state(self, state): 

692 """Stop poller from watching an io_state.""" 

693 self._state = self._state & (~state) 

694 self._update_handler(self._state) 

695 

696 def _update_handler(self, state): 

697 """Update IOLoop handler with state.""" 

698 if self.socket is None: 

699 return 

700 

701 if state & self.socket.events: 

702 # events still exist that haven't been processed 

703 # explicitly schedule handling to avoid missing events due to edge-triggered FDs 

704 self.io_loop.add_callback(lambda: self._handle_events(self.socket, 0)) 

705 

706 def _init_io_state(self): 

707 """initialize the ioloop event handler""" 

708 self.io_loop.add_handler(self.socket, self._handle_events, self.io_loop.READ)