mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 21:13:01 +00:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f7903a78d1 | ||
![]() |
edddacec1d | ||
![]() |
c5a33f227f | ||
![]() |
aaa20e5787 | ||
![]() |
c8ebf1ec90 | ||
![]() |
89745c002b | ||
![]() |
3e0ee49ce8 | ||
![]() |
bbbeffb443 | ||
![]() |
c29a5ca4ce | ||
![]() |
f5182fe349 |
@@ -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
|
||||
|
@@ -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]
|
||||
|
@@ -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))
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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")
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
@@ -223,7 +223,7 @@ class User(Base):
|
||||
|
||||
|
||||
class Spawner(Base):
|
||||
""""State about a Spawner"""
|
||||
""" "State about a Spawner"""
|
||||
|
||||
__tablename__ = 'spawners'
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user