Skip to content

Commit d0778f9

Browse files
authored
Merge pull request #365 from afshin/ensure_last_activity
Culling: ensure last_activity attr exists before use
2 parents c2b2c31 + 29db7f9 commit d0778f9

File tree

8 files changed

+195
-14
lines changed

8 files changed

+195
-14
lines changed

.github/workflows/main.yml renamed to .github/workflows/python-linux.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Jupyter Server Tests
1+
name: Jupyter Server Tests [Linux]
22
on:
33
push:
44
branches: '*'
@@ -10,11 +10,8 @@ jobs:
1010
strategy:
1111
fail-fast: false
1212
matrix:
13-
os: [ubuntu, macos, windows]
13+
os: [ubuntu]
1414
python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy3' ]
15-
exclude:
16-
- os: windows
17-
python-version: pypy3
1815
steps:
1916
- name: Checkout
2017
uses: actions/checkout@v1

.github/workflows/python-macos.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Jupyter Server Tests [Mac OS]
2+
on:
3+
push:
4+
branches: '*'
5+
pull_request:
6+
branches: '*'
7+
jobs:
8+
build:
9+
runs-on: ${{ matrix.os }}-latest
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
os: [macos]
14+
python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy3' ]
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v1
18+
- name: Install Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v1
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
architecture: 'x64'
23+
- name: Upgrade packaging dependencies
24+
run: |
25+
pip install --upgrade pip setuptools wheel --user
26+
- name: Get pip cache dir
27+
id: pip-cache
28+
run: |
29+
echo "::set-output name=dir::$(pip cache dir)"
30+
- name: Cache pip
31+
uses: actions/cache@v1
32+
with:
33+
path: ${{ steps.pip-cache.outputs.dir }}
34+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
35+
restore-keys: |
36+
${{ runner.os }}-pip-${{ matrix.python-version }}-
37+
${{ runner.os }}-pip-
38+
- name: Install the Python dependencies
39+
run: |
40+
pip install -e .[test] codecov
41+
- name: List installed packages
42+
run: |
43+
pip freeze
44+
pip check
45+
- name: Run the tests
46+
run: |
47+
pytest -vv --cov jupyter_server --cov-branch --cov-report term-missing:skip-covered
48+
- name: Install the Python dependencies for the examples
49+
run: |
50+
cd examples/simple && pip install -e .
51+
- name: Run the tests for the examples
52+
run: |
53+
pytest examples/simple/tests/test_handlers.py
54+
- name: Coverage
55+
run: |
56+
codecov

.github/workflows/python-windows.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Jupyter Server Tests [Windows]
2+
on:
3+
push:
4+
branches: '*'
5+
pull_request:
6+
branches: '*'
7+
jobs:
8+
build:
9+
runs-on: ${{ matrix.os }}-latest
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
os: [windows]
14+
python-version: [ '3.6', '3.7', '3.8', '3.9' ]
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v1
18+
- name: Install Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v1
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
architecture: 'x64'
23+
- name: Upgrade packaging dependencies
24+
run: |
25+
pip install --upgrade pip setuptools wheel --user
26+
- name: Get pip cache dir
27+
id: pip-cache
28+
run: |
29+
echo "::set-output name=dir::$(pip cache dir)"
30+
- name: Cache pip
31+
uses: actions/cache@v1
32+
with:
33+
path: ${{ steps.pip-cache.outputs.dir }}
34+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
35+
restore-keys: |
36+
${{ runner.os }}-pip-${{ matrix.python-version }}-
37+
${{ runner.os }}-pip-
38+
- name: Install the Python dependencies
39+
run: |
40+
pip install -e .[test] codecov
41+
- name: List installed packages
42+
run: |
43+
pip freeze
44+
pip check
45+
- name: Run the tests
46+
run: |
47+
pytest -vv
48+
- name: Install the Python dependencies for the examples
49+
run: |
50+
cd examples/simple && pip install -e .
51+
- name: Run the tests for the examples
52+
run: |
53+
pytest examples/simple/tests/test_handlers.py

jupyter_server/services/kernels/kernelmanager.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -468,8 +468,9 @@ async def cull_kernels(self):
468468

469469
async def cull_kernel_if_idle(self, kernel_id):
470470
kernel = self._kernels[kernel_id]
471-
self.log.debug("kernel_id=%s, kernel_name=%s, last_activity=%s", kernel_id, kernel.kernel_name, kernel.last_activity)
472-
if kernel.last_activity is not None:
471+
if hasattr(kernel, 'last_activity'): # last_activity is monkey-patched, so ensure that has occurred
472+
self.log.debug("kernel_id=%s, kernel_name=%s, last_activity=%s",
473+
kernel_id, kernel.kernel_name, kernel.last_activity)
473474
dt_now = utcnow()
474475
dt_idle = dt_now - kernel.last_activity
475476
# Compute idle properties
@@ -482,7 +483,7 @@ async def cull_kernel_if_idle(self, kernel_id):
482483
idle_duration = int(dt_idle.total_seconds())
483484
self.log.warning("Culling '%s' kernel '%s' (%s) with %d connections due to %s seconds of inactivity.",
484485
kernel.execution_state, kernel.kernel_name, kernel_id, connections, idle_duration)
485-
await self.shutdown_kernel(kernel_id)
486+
await ensure_async(self.shutdown_kernel(kernel_id))
486487

487488

488489
# AsyncMappingKernelManager inherits as much as possible from MappingKernelManager,
@@ -506,12 +507,14 @@ async def shutdown_kernel(self, kernel_id, now=False, restart=False):
506507
kernel._activity_stream.close()
507508
kernel._activity_stream = None
508509
self.stop_buffering(kernel_id)
509-
self._kernel_connections.pop(kernel_id, None)
510510

511511
# Decrease the metric of number of kernels
512512
# running for the relevant kernel type by 1
513513
KERNEL_CURRENTLY_RUNNING_TOTAL.labels(
514514
type=self._kernels[kernel_id].kernel_name
515515
).dec()
516516

517-
return await self.pinned_superclass.shutdown_kernel(self, kernel_id, now=now, restart=restart)
517+
# Finish shutting down the kernel before clearing state to avoid a race condition.
518+
ret = await self.pinned_superclass.shutdown_kernel(self, kernel_id, now=now, restart=restart)
519+
self._kernel_connections.pop(kernel_id, None)
520+
return ret

tests/services/contents/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def dirs_only(dir_model):
4242

4343

4444
@pytest.fixture(params=["FileContentsManager", "AsyncFileContentsManager"])
45-
def argv(request):
45+
def jp_argv(request):
4646
return ["--ServerApp.contents_manager_class=jupyter_server.services.contents.filemanager." + request.param]
4747

4848

tests/services/kernels/test_api.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
@pytest.fixture(params=["MappingKernelManager", "AsyncMappingKernelManager"])
20-
def argv(request):
20+
def jp_argv(request):
2121
return ["--ServerApp.kernel_manager_class=jupyter_server.services.kernels.kernelmanager." + request.param]
2222

2323

@@ -209,7 +209,6 @@ async def test_connection(jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
209209
model = json.loads(r.body.decode())
210210
assert model['connections'] == 0
211211

212-
time.sleep(1)
213212
# Open a websocket connection.
214213
ws = await jp_ws_fetch(
215214
'api', 'kernels', kid, 'channels'

tests/services/kernels/test_cull.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import asyncio
2+
import json
3+
import platform
4+
import sys
5+
import time
6+
import pytest
7+
from traitlets.config import Config
8+
from tornado.httpclient import HTTPClientError
9+
10+
11+
@pytest.fixture(params=["MappingKernelManager", "AsyncMappingKernelManager"])
12+
def jp_argv(request):
13+
return ["--ServerApp.kernel_manager_class=jupyter_server.services.kernels.kernelmanager." + request.param]
14+
15+
16+
CULL_TIMEOUT = 10 if platform.python_implementation() == 'PyPy' else 2
17+
18+
@pytest.fixture
19+
def jp_server_config():
20+
return Config({
21+
'ServerApp': {
22+
'MappingKernelManager': {
23+
'cull_idle_timeout': CULL_TIMEOUT,
24+
'cull_interval': 1,
25+
'cull_connected': False
26+
}
27+
}
28+
})
29+
30+
31+
async def test_culling(jp_fetch, jp_ws_fetch):
32+
r = await jp_fetch(
33+
'api', 'kernels',
34+
method='POST',
35+
allow_nonstandard_methods=True
36+
)
37+
kernel = json.loads(r.body.decode())
38+
kid = kernel['id']
39+
40+
# Open a websocket connection.
41+
ws = await jp_ws_fetch(
42+
'api', 'kernels', kid, 'channels'
43+
)
44+
45+
r = await jp_fetch(
46+
'api', 'kernels', kid,
47+
method='GET'
48+
)
49+
model = json.loads(r.body.decode())
50+
assert model['connections'] == 1
51+
culled = await get_cull_status(kid, jp_fetch) # connected, should not be culled
52+
assert not culled
53+
ws.close()
54+
culled = await get_cull_status(kid, jp_fetch) # not connected, should be culled
55+
assert culled
56+
57+
58+
async def get_cull_status(kid, jp_fetch):
59+
culled = False
60+
for i in range(20): # Need max of 2x culling PERIOD ensure culling timeout exceeded
61+
try:
62+
r = await jp_fetch(
63+
'api', 'kernels', kid,
64+
method='GET'
65+
)
66+
kernel = json.loads(r.body.decode())
67+
except HTTPClientError as e:
68+
assert e.code == 404
69+
culled = True
70+
break
71+
else:
72+
await asyncio.sleep(CULL_TIMEOUT / 10.)
73+
return culled

tests/services/sessions/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
@pytest.fixture(params=["MappingKernelManager", "AsyncMappingKernelManager"])
20-
def argv(request):
20+
def jp_argv(request):
2121
return ["--ServerApp.kernel_manager_class=jupyter_server.services.kernels.kernelmanager." + request.param]
2222

2323

0 commit comments

Comments
 (0)