Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Culling: ensure last_activity attr exists before use #365

Merged
merged 2 commits into from
Dec 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Jupyter Server Tests
name: Jupyter Server Tests [Linux]
on:
push:
branches: '*'
Expand All @@ -10,11 +10,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
os: [ubuntu]
python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy3' ]
exclude:
- os: windows
python-version: pypy3
steps:
- name: Checkout
uses: actions/checkout@v1
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/python-macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Jupyter Server Tests [Mac OS]
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
build:
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os: [macos]
python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy3' ]
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
architecture: 'x64'
- name: Upgrade packaging dependencies
run: |
pip install --upgrade pip setuptools wheel --user
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache pip
uses: actions/cache@v1
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
- name: Install the Python dependencies
run: |
pip install -e .[test] codecov
- name: List installed packages
run: |
pip freeze
pip check
- name: Run the tests
run: |
pytest -vv --cov jupyter_server --cov-branch --cov-report term-missing:skip-covered
- name: Install the Python dependencies for the examples
run: |
cd examples/simple && pip install -e .
- name: Run the tests for the examples
run: |
pytest examples/simple/tests/test_handlers.py
- name: Coverage
run: |
codecov
53 changes: 53 additions & 0 deletions .github/workflows/python-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Jupyter Server Tests [Windows]
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
build:
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os: [windows]
python-version: [ '3.6', '3.7', '3.8', '3.9' ]
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
architecture: 'x64'
- name: Upgrade packaging dependencies
run: |
pip install --upgrade pip setuptools wheel --user
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache pip
uses: actions/cache@v1
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
- name: Install the Python dependencies
run: |
pip install -e .[test] codecov
- name: List installed packages
run: |
pip freeze
pip check
- name: Run the tests
run: |
pytest -vv
- name: Install the Python dependencies for the examples
run: |
cd examples/simple && pip install -e .
- name: Run the tests for the examples
run: |
pytest examples/simple/tests/test_handlers.py
13 changes: 8 additions & 5 deletions jupyter_server/services/kernels/kernelmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,9 @@ async def cull_kernels(self):

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


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

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

return await self.pinned_superclass.shutdown_kernel(self, kernel_id, now=now, restart=restart)
# Finish shutting down the kernel before clearing state to avoid a race condition.
ret = await self.pinned_superclass.shutdown_kernel(self, kernel_id, now=now, restart=restart)
self._kernel_connections.pop(kernel_id, None)
return ret
2 changes: 1 addition & 1 deletion tests/services/contents/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def dirs_only(dir_model):


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


Expand Down
3 changes: 1 addition & 2 deletions tests/services/kernels/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


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


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

time.sleep(1)
# Open a websocket connection.
ws = await jp_ws_fetch(
'api', 'kernels', kid, 'channels'
Expand Down
73 changes: 73 additions & 0 deletions tests/services/kernels/test_cull.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import asyncio
import json
import platform
import sys
import time
import pytest
from traitlets.config import Config
from tornado.httpclient import HTTPClientError


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


CULL_TIMEOUT = 10 if platform.python_implementation() == 'PyPy' else 2

@pytest.fixture
def jp_server_config():
return Config({
'ServerApp': {
'MappingKernelManager': {
'cull_idle_timeout': CULL_TIMEOUT,
'cull_interval': 1,
'cull_connected': False
}
}
})


async def test_culling(jp_fetch, jp_ws_fetch):
r = await jp_fetch(
'api', 'kernels',
method='POST',
allow_nonstandard_methods=True
)
kernel = json.loads(r.body.decode())
kid = kernel['id']

# Open a websocket connection.
ws = await jp_ws_fetch(
'api', 'kernels', kid, 'channels'
)

r = await jp_fetch(
'api', 'kernels', kid,
method='GET'
)
model = json.loads(r.body.decode())
assert model['connections'] == 1
culled = await get_cull_status(kid, jp_fetch) # connected, should not be culled
assert not culled
ws.close()
culled = await get_cull_status(kid, jp_fetch) # not connected, should be culled
assert culled


async def get_cull_status(kid, jp_fetch):
culled = False
for i in range(20): # Need max of 2x culling PERIOD ensure culling timeout exceeded
try:
r = await jp_fetch(
'api', 'kernels', kid,
method='GET'
)
kernel = json.loads(r.body.decode())
except HTTPClientError as e:
assert e.code == 404
culled = True
break
else:
await asyncio.sleep(CULL_TIMEOUT / 10.)
return culled
2 changes: 1 addition & 1 deletion tests/services/sessions/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


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


Expand Down