|
14 | 14 |
|
15 | 15 | """System tests for Jupyter/IPython connector.""" |
16 | 16 |
|
| 17 | +import contextlib |
| 18 | +import gc |
17 | 19 | import re |
| 20 | +import time |
18 | 21 |
|
19 | 22 | import pandas |
20 | 23 | import psutil |
21 | 24 | from IPython.testing import globalipapp |
22 | 25 | from IPython.utils import io |
23 | 26 |
|
24 | 27 |
|
| 28 | +@contextlib.contextmanager |
| 29 | +def patch_tracked_requests(): |
| 30 | + """Context manager to patch google-auth requests and track/close their HTTP sessions. |
| 31 | +
|
| 32 | + This prevents socket leaks in system tests that use Workload Identity or metadata server auth. |
| 33 | + """ |
| 34 | + import google.auth.transport.requests |
| 35 | + |
| 36 | + original_init = google.auth.transport.requests.Request.__init__ |
| 37 | + tracked_requests = [] |
| 38 | + |
| 39 | + def patched_init(self, session=None): |
| 40 | + original_init(self, session=session) |
| 41 | + if session is None: |
| 42 | + tracked_requests.append(self) |
| 43 | + |
| 44 | + google.auth.transport.requests.Request.__init__ = patched_init |
| 45 | + try: |
| 46 | + yield tracked_requests |
| 47 | + finally: |
| 48 | + google.auth.transport.requests.Request.__init__ = original_init |
| 49 | + for req in tracked_requests: |
| 50 | + if hasattr(req, "session") and req.session is not None: |
| 51 | + req.session.close() |
| 52 | + |
| 53 | + |
25 | 54 | def test_bigquery_magic(): |
26 | 55 | globalipapp.start_ipython() |
27 | 56 | ip = globalipapp.get_ipython() |
28 | 57 | current_process = psutil.Process() |
| 58 | + |
| 59 | + # GC to ensure clean starting state |
| 60 | + gc.collect() |
29 | 61 | conn_count_start = len(current_process.net_connections()) |
30 | 62 |
|
31 | | - ip.extension_manager.load_extension("bigquery_magics") |
32 | | - sql = """ |
33 | | - SELECT |
34 | | - CONCAT( |
35 | | - 'https://stackoverflow.com/questions/', |
36 | | - CAST(id as STRING)) as url, |
37 | | - view_count |
38 | | - FROM `bigquery-public-data.stackoverflow.posts_questions` |
39 | | - WHERE tags like '%google-bigquery%' |
40 | | - ORDER BY view_count DESC |
41 | | - LIMIT 10 |
42 | | - """ |
43 | | - with io.capture_output() as captured: |
44 | | - result = ip.run_cell_magic("bigquery", "--use_rest_api", sql) |
| 63 | + with patch_tracked_requests(): |
| 64 | + ip.extension_manager.load_extension("bigquery_magics") |
| 65 | + sql = """ |
| 66 | + SELECT |
| 67 | + CONCAT( |
| 68 | + 'https://stackoverflow.com/questions/', |
| 69 | + CAST(id as STRING)) as url, |
| 70 | + view_count |
| 71 | + FROM `bigquery-public-data.stackoverflow.posts_questions` |
| 72 | + WHERE tags like '%google-bigquery%' |
| 73 | + ORDER BY view_count DESC |
| 74 | + LIMIT 10 |
| 75 | + """ |
| 76 | + with io.capture_output() as captured: |
| 77 | + result = ip.run_cell_magic("bigquery", "--use_rest_api", sql) |
| 78 | + |
| 79 | + # Force garbage collection to sweep unreferenced socket objects |
| 80 | + gc.collect() |
45 | 81 |
|
46 | | - conn_count_end = len(current_process.net_connections()) |
| 82 | + # Wait a bit for the asynchronous channel teardown to complete and the socket to be closed. |
| 83 | + for _ in range(30): |
| 84 | + conn_count_end = len(current_process.net_connections()) |
| 85 | + if conn_count_end <= conn_count_start: |
| 86 | + break |
| 87 | + time.sleep(0.1) |
47 | 88 |
|
48 | 89 | lines = re.split("\n|\r", captured.stdout) |
49 | 90 | # Removes blanks & terminal code (result of display clearing) |
|
0 commit comments