Skip to content

Commit 5c131da

Browse files
committed
check for leftover zmq resources across tests
1 parent 9a28aea commit 5c131da

File tree

2 files changed

+56
-0
lines changed

2 files changed

+56
-0
lines changed

jupyter_client/tests/conftest.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import asyncio
2+
import gc
23
import os
34
import sys
45

56
import pytest
7+
import zmq
68
from jupyter_core import paths
9+
from zmq.tests import BaseZMQTestCase
710

811
from .utils import test_env
912

@@ -39,3 +42,47 @@ def env():
3942
@pytest.fixture()
4043
def kernel_dir():
4144
return pjoin(paths.jupyter_data_dir(), 'kernels')
45+
46+
47+
def assert_no_zmq():
48+
"""Verify that there are no zmq resources
49+
50+
avoids reference leaks across tests,
51+
which can lead to FD exhaustion
52+
"""
53+
# zmq garbage collection uses a zmq socket in a thread
54+
# we don't want to delete these from the main thread!
55+
from zmq.utils import garbage
56+
57+
garbage.gc.stop()
58+
sockets = [
59+
obj
60+
for obj in gc.get_referrers(zmq.Socket)
61+
if isinstance(obj, zmq.Socket) and not obj.closed
62+
]
63+
if sockets:
64+
message = f"{len(sockets)} unclosed sockets: {sockets}"
65+
for s in sockets:
66+
s.close(linger=0)
67+
raise AssertionError(message)
68+
contexts = [
69+
obj
70+
for obj in gc.get_referrers(zmq.Context)
71+
if isinstance(obj, zmq.Context) and not obj.closed
72+
]
73+
# allow for single zmq.Context.instance()
74+
if contexts and len(contexts) > 1:
75+
message = f"{len(contexts)} unclosed contexts: {contexts}"
76+
for ctx in contexts:
77+
ctx.destroy(linger=0)
78+
raise AssertionError(message)
79+
80+
81+
@pytest.fixture(autouse=True)
82+
def check_zmq(request):
83+
yield
84+
if request.instance and isinstance(request.instance, BaseZMQTestCase):
85+
# can't run this check on old-style TestCases with tearDown methods
86+
# because this check runs before tearDown
87+
return
88+
assert_no_zmq()

jupyter_client/tests/test_multikernelmanager.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def _run_lifecycle(km, test_kid=None):
6969
assert isinstance(k, KernelManager)
7070
km.shutdown_kernel(kid, now=True)
7171
assert kid not in km, f"{kid} not in {km}"
72+
km.context.term()
7273

7374
def _run_cinfo(self, km, transport, ip):
7475
kid = km.start_kernel(stdout=PIPE, stderr=PIPE)
@@ -87,6 +88,7 @@ def _run_cinfo(self, km, transport, ip):
8788
stream = km.connect_hb(kid)
8889
stream.close()
8990
km.shutdown_kernel(kid, now=True)
91+
km.context.term()
9092

9193
# static so picklable for multiprocessing on Windows
9294
@classmethod
@@ -106,6 +108,7 @@ def test_shutdown_all(self):
106108
self.assertNotIn(kid, km)
107109
# shutdown again is okay, because we have no kernels
108110
km.shutdown_all()
111+
km.context.term()
109112

110113
def test_tcp_cinfo(self):
111114
km = self._get_tcp_km()
@@ -217,6 +220,7 @@ def test_subclass_callables(self):
217220
assert km.call_count("cleanup_resources") == 0
218221

219222
assert kid not in km, f"{kid} not in {km}"
223+
km.context.term()
220224

221225

222226
class TestAsyncKernelManager(AsyncTestCase):
@@ -263,6 +267,7 @@ async def _run_lifecycle(km, test_kid=None):
263267
assert isinstance(k, AsyncKernelManager)
264268
await km.shutdown_kernel(kid, now=True)
265269
assert kid not in km, f"{kid} not in {km}"
270+
km.context.term()
266271

267272
async def _run_cinfo(self, km, transport, ip):
268273
kid = await km.start_kernel(stdout=PIPE, stderr=PIPE)
@@ -282,6 +287,7 @@ async def _run_cinfo(self, km, transport, ip):
282287
stream.close()
283288
await km.shutdown_kernel(kid, now=True)
284289
self.assertNotIn(kid, km)
290+
km.context.term()
285291

286292
@gen_test
287293
async def test_tcp_lifecycle(self):
@@ -316,6 +322,7 @@ async def test_use_after_shutdown_all(self):
316322
self.assertNotIn(kid, km)
317323
# shutdown again is okay, because we have no kernels
318324
await km.shutdown_all()
325+
km.context.term()
319326

320327
@gen_test(timeout=20)
321328
async def test_shutdown_all_while_starting(self):
@@ -333,6 +340,7 @@ async def test_shutdown_all_while_starting(self):
333340
self.assertNotIn(kid, km)
334341
# shutdown again is okay, because we have no kernels
335342
await km.shutdown_all()
343+
km.context.term()
336344

337345
@gen_test
338346
async def test_tcp_cinfo(self):
@@ -466,3 +474,4 @@ async def test_subclass_callables(self):
466474
assert mkm.call_count("cleanup_resources") == 0
467475

468476
assert kid not in mkm, f"{kid} not in {mkm}"
477+
mkm.context.term()

0 commit comments

Comments
 (0)