Compare commits

...

10 Commits
1.5.0 ... 1.5.1

Author SHA1 Message Date
Min RK
f7903a78d1 release 1.5.1 2022-12-05 13:27:57 +01:00
Erik Sundell
edddacec1d Merge pull request #4234 from minrk/1.x
changelog for 1.5.1
2022-12-01 16:43:44 +01:00
Min RK
c5a33f227f changelog for 1.5.1 2022-11-30 10:56:00 +01:00
Min RK
aaa20e5787 fix flake8 url in pre-commit
repo moved
2022-11-30 10:55:52 +01:00
Min RK
c8ebf1ec90 Merge pull request #4076 from mriedem/1.x-db-fix
Backport PR #3566 to 1.x series
2022-10-12 12:37:22 +02:00
pre-commit-ci[bot]
89745c002b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-10-10 15:44:36 +00:00
Matt Riedemann
3e0ee49ce8 Upgrade black to 22.3.0
Partial cherry-pick of f124f06c2 needed because of
https://github.com/psf/black/issues/2964.
2022-10-10 10:43:36 -05:00
pre-commit-ci[bot]
bbbeffb443 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-10-10 13:49:36 +00:00
Min RK
c29a5ca4ce finish up db rollback checks
- move catch_db_error to utils
- tidy catch/propagate errors in prepare, get_current_user

(cherry picked from commit 3bcc542e27)

Conflicts:
  jupyterhub/handlers/base.py

NOTE(mriedem): The conflict is due to e6845a68f not being
in 1.5.0.
2022-10-10 08:47:00 -05:00
SHAHN3
f5182fe349 add explicit db rollback
add context manager/decorator for db rollback

add db rollback in top level prepare method

Co-authored-by: Sarath Babu <sbreached@gmail.com>
(cherry picked from commit 044fb23a70)
2022-10-10 08:42:42 -05:00
12 changed files with 64 additions and 25 deletions

View File

@@ -4,14 +4,14 @@ repos:
hooks:
- id: reorder-python-imports
- repo: https://github.com/psf/black
rev: 20.8b1
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.2.1
hooks:
- id: prettier
- repo: https://gitlab.com/pycqa/flake8
- repo: https://github.com/pycqa/flake8
rev: "3.8.4"
hooks:
- id: flake8

View File

@@ -3,7 +3,7 @@ swagger: "2.0"
info:
title: JupyterHub
description: The REST API for JupyterHub
version: 1.5.0
version: 1.5.1
license:
name: BSD-3-Clause
schemes: [http, https]

View File

@@ -17,6 +17,22 @@ A few fully backward-compatible features have been backported from 2.0.
[ghsa-cw7p-q79f-m2v7]: https://github.com/jupyterhub/jupyterhub/security/advisories/GHSA-cw7p-q79f-m2v7
### 1.5.1 2022-12-01
This is a patch release, improving db resiliency when certain errors occur, without requiring a jupyterhub restart.
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.5.0...1.5.1))
#### Merged PRs
- Backport db rollback fixes to 1.x [#4076](https://github.com/jupyterhub/jupyterhub/pull/4076) ([@mriedem](https://github.com/mriedem), [@minrk](https://github.com/minrk)), [@nsshah1288](https://github.com/nsshah1288)
#### Contributors to this release
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2022-09-09&to=2022-11-30&type=c))
[@mriedem](https://github.com/mriedem) | [@minrk](https://github.com/minrk) | [@nsshah1288](https://github.com/nsshah1288)
### [1.5.0] 2021-11-04
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0))

View File

@@ -10,7 +10,7 @@ from jupyter_client.localinterfaces import public_ips
def create_dir_hook(spawner):
""" Create directory """
"""Create directory"""
username = spawner.user.name # get the username
volume_path = os.path.join('/volumes/jupyterhub', username)
if not os.path.exists(volume_path):
@@ -20,7 +20,7 @@ def create_dir_hook(spawner):
def clean_dir_hook(spawner):
""" Delete directory """
"""Delete directory"""
username = spawner.user.name # get the username
temp_path = os.path.join('/volumes/jupyterhub', username, 'temp')
if os.path.exists(temp_path) and os.path.isdir(temp_path):

View File

@@ -5,7 +5,7 @@
version_info = (
1,
5,
0,
1,
"", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing for beta/rc/stable releases
)

View File

@@ -42,7 +42,7 @@ class GroupListAPIHandler(_GroupAPIHandler):
@admin_only
async def post(self):
"""POST creates Multiple groups """
"""POST creates Multiple groups"""
model = self.get_json_body()
if not model or not isinstance(model, dict) or not model.get('groups'):
raise web.HTTPError(400, "Must specify at least one group to create")

View File

@@ -5,7 +5,6 @@
import asyncio
import atexit
import binascii
import json
import logging
import os
import re
@@ -90,6 +89,7 @@ from .pagination import Pagination
from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix, Command, EntryPointType, Callable
from .utils import (
catch_db_error,
maybe_future,
url_path_join,
print_stacks,
@@ -2000,6 +2000,7 @@ class JupyterHub(Application):
# purge expired tokens hourly
purge_expired_tokens_interval = 3600
@catch_db_error
def purge_expired_tokens(self):
"""purge all expiring token objects from the database
@@ -2015,7 +2016,7 @@ class JupyterHub(Application):
await self._add_tokens(self.service_tokens, kind='service')
await self._add_tokens(self.api_tokens, kind='user')
self.purge_expired_tokens()
await self.purge_expired_tokens()
# purge expired tokens hourly
# we don't need to be prompt about this
# because expired tokens cannot be used anyway
@@ -2663,6 +2664,7 @@ class JupyterHub(Application):
with open(self.config_file, mode='w') as f:
f.write(config_text)
@catch_db_error
async def update_last_activity(self):
"""Update User.last_activity timestamps from the proxy"""
routes = await self.proxy.get_all_routes()

View File

@@ -81,9 +81,14 @@ class BaseHandler(RequestHandler):
"""
try:
await self.get_current_user()
except Exception:
self.log.exception("Failed to get current user")
except Exception as e:
# ensure get_current_user is never called again for this handler,
# since it failed
self._jupyterhub_user = None
self.log.exception("Failed to get current user")
if isinstance(e, SQLAlchemyError):
self.log.error("Rolling back session due to database error")
self.db.rollback()
return await maybe_future(super().prepare())
@@ -426,7 +431,8 @@ class BaseHandler(RequestHandler):
except Exception:
# don't let errors here raise more than once
self._jupyterhub_user = None
self.log.exception("Error getting current user")
# but still raise, which will get handled in .prepare()
raise
return self._jupyterhub_user
@property
@@ -1499,14 +1505,10 @@ class UserUrlHandler(BaseHandler):
# if request is expecting JSON, assume it's an API request and fail with 503
# because it won't like the redirect to the pending page
if (
get_accepted_mimetype(
self.request.headers.get('Accept', ''),
choices=['application/json', 'text/html'],
)
== 'application/json'
or 'api' in user_path.split('/')
):
if get_accepted_mimetype(
self.request.headers.get('Accept', ''),
choices=['application/json', 'text/html'],
) == 'application/json' or 'api' in user_path.split('/'):
self._fail_api_request(user_name, server_name)
return
@@ -1588,7 +1590,7 @@ class UserUrlHandler(BaseHandler):
if redirects:
self.log.warning("Redirect loop detected on %s", self.request.uri)
# add capped exponential backoff where cap is 10s
await asyncio.sleep(min(1 * (2 ** redirects), 10))
await asyncio.sleep(min(1 * (2**redirects), 10))
# rewrite target url with new `redirects` query value
url_parts = urlparse(target)
query_parts = parse_qs(url_parts.query)

View File

@@ -223,7 +223,7 @@ class User(Base):
class Spawner(Base):
""""State about a Spawner"""
""" "State about a Spawner"""
__tablename__ = 'spawners'

View File

@@ -452,7 +452,7 @@ class SingleUserNotebookAppMixin(Configurable):
i,
RETRIES,
)
await asyncio.sleep(min(2 ** i, 16))
await asyncio.sleep(min(2**i, 16))
else:
break
else:

View File

@@ -4,9 +4,9 @@
import asyncio
import concurrent.futures
import errno
import functools
import hashlib
import inspect
import os
import random
import secrets
import socket
@@ -22,6 +22,7 @@ from hmac import compare_digest
from operator import itemgetter
from async_generator import aclosing
from sqlalchemy.exc import SQLAlchemyError
from tornado import ioloop
from tornado import web
from tornado.httpclient import AsyncHTTPClient
@@ -642,3 +643,21 @@ def get_accepted_mimetype(accept_header, choices=None):
else:
return mime
return None
def catch_db_error(f):
"""Catch and rollback database errors"""
@functools.wraps(f)
async def catching(self, *args, **kwargs):
try:
r = f(self, *args, **kwargs)
if inspect.isawaitable(r):
r = await r
except SQLAlchemyError:
self.log.exception("Rolling back session due to database error")
self.db.rollback()
else:
return r
return catching

View File

@@ -26,8 +26,8 @@ import os
import pipes
import shutil
from contextlib import contextmanager
from distutils.version import LooseVersion as V
from distutils.version import LooseVersion as V
from invoke import run as invoke_run
from invoke import task