|
5 | 5 | import selectors |
6 | 6 | import socket |
7 | 7 | import ssl |
| 8 | +import sys |
8 | 9 | import threading |
9 | 10 | import time |
10 | 11 | import types |
@@ -603,6 +604,40 @@ def test_connect_breaks_exception_reference_cycle(self): |
603 | 604 | assert connect_frame is not None |
604 | 605 | assert connect_frame.f_locals.get("err") is None |
605 | 606 |
|
| 607 | + def test_connect_breaks_reference_cycle_when_a_later_address_succeeds(self): |
| 608 | + """ |
| 609 | + When an address fails but a later one connects, _connect must not |
| 610 | + return while still holding the caught exception. |
| 611 | + The exception's traceback references the frame, so a retained local |
| 612 | + would form a cycle only reclaimable by the GC. |
| 613 | + """ |
| 614 | + conn = Connection(host="localhost", port=6379) |
| 615 | + addr_infos = [ |
| 616 | + (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("127.0.0.1", 6379)), |
| 617 | + (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("127.0.0.2", 6379)), |
| 618 | + ] |
| 619 | + |
| 620 | + # _connect calls getaddrinfo, so hook that as a way to peek the caller |
| 621 | + connect_frames = [] |
| 622 | + |
| 623 | + def capturing_getaddrinfo(*args, **kwargs): |
| 624 | + connect_frames.append(sys._getframe(1)) |
| 625 | + return addr_infos |
| 626 | + |
| 627 | + failing_sock, working_sock = MagicMock(), MagicMock() |
| 628 | + failing_sock.connect.side_effect = OSError("refused") |
| 629 | + |
| 630 | + with ( |
| 631 | + patch.object(socket, "getaddrinfo", capturing_getaddrinfo), |
| 632 | + patch.object(socket, "socket", side_effect=[failing_sock, working_sock]), |
| 633 | + ): |
| 634 | + assert conn._connect() is working_sock |
| 635 | + |
| 636 | + # Error should be cleared in the connect frame |
| 637 | + assert len(connect_frames) == 1 |
| 638 | + (connect_frame,) = connect_frames |
| 639 | + assert connect_frame.f_locals.get("err") is None |
| 640 | + |
606 | 641 | @pytest.mark.parametrize( |
607 | 642 | "connection_kwargs", |
608 | 643 | [ |
|
0 commit comments