Skip to content

Commit 6e9256b

Browse files
authored
Culling: ensure last_activity attr exists before use (#5355)
KernelManager's last_activity attribute is added following the instance's creation. Culling (independently) uses this attribute and should ensure it exists prior to its use, else skip the culling check for that instance. Fixes #5345
1 parent 4da22ce commit 6e9256b

File tree

2 files changed

+50
-3
lines changed

2 files changed

+50
-3
lines changed

notebook/services/kernels/kernelmanager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,9 +459,9 @@ async def cull_kernel_if_idle(self, kernel_id):
459459
except KeyError:
460460
return # KeyErrors are somewhat expected since the kernel can be shutdown as the culling check is made.
461461

462-
self.log.debug("kernel_id=%s, kernel_name=%s, last_activity=%s",
463-
kernel_id, kernel.kernel_name, kernel.last_activity)
464-
if kernel.last_activity is not None:
462+
if hasattr(kernel, 'last_activity'): # last_activity is monkey-patched, so ensure that has occurred
463+
self.log.debug("kernel_id=%s, kernel_name=%s, last_activity=%s",
464+
kernel_id, kernel.kernel_name, kernel.last_activity)
465465
dt_now = utcnow()
466466
dt_idle = dt_now - kernel.last_activity
467467
# Compute idle properties

notebook/services/kernels/tests/test_kernels_api.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import time
66

7+
from requests import HTTPError
78
from traitlets.config import Config
89

910
from tornado.httpclient import HTTPRequest
@@ -234,3 +235,49 @@ class KernelFilterTest(NotebookTestBase):
234235
# Sanity check verifying that the configurable was properly set.
235236
def test_config(self):
236237
self.assertEqual(self.notebook.kernel_manager.allowed_message_types, ['kernel_info_request'])
238+
239+
240+
class KernelCullingTest(NotebookTestBase):
241+
"""Test kernel culling """
242+
243+
@classmethod
244+
def get_argv(cls):
245+
argv = super(KernelCullingTest, cls).get_argv()
246+
247+
# Enable culling with 2s timeout and 1s intervals
248+
argv.extend(['--MappingKernelManager.cull_idle_timeout=2',
249+
'--MappingKernelManager.cull_interval=1',
250+
'--MappingKernelManager.cull_connected=False'])
251+
return argv
252+
253+
def setUp(self):
254+
self.kern_api = KernelAPI(self.request,
255+
base_url=self.base_url(),
256+
headers=self.auth_headers(),
257+
)
258+
259+
def tearDown(self):
260+
for k in self.kern_api.list().json():
261+
self.kern_api.shutdown(k['id'])
262+
263+
def test_culling(self):
264+
kid = self.kern_api.start().json()['id']
265+
ws = self.kern_api.websocket(kid)
266+
model = self.kern_api.get(kid).json()
267+
self.assertEqual(model['connections'], 1)
268+
assert not self.get_cull_status(kid) # connected, should not be culled
269+
ws.close()
270+
assert self.get_cull_status(kid) # not connected, should be culled
271+
272+
def get_cull_status(self, kid):
273+
culled = False
274+
for i in range(15): # Need max of 3s to ensure culling timeout exceeded
275+
try:
276+
self.kern_api.get(kid)
277+
except HTTPError as e:
278+
assert e.response.status_code == 404
279+
culled = True
280+
break
281+
else:
282+
time.sleep(0.2)
283+
return culled

0 commit comments

Comments
 (0)