Skip to content

Commit e2040fd

Browse files
Add mechanism to set CPU and memory limits on Jupyterlab containers based on Magpie group (#608)
## Overview Creates a new variable `JUPYTERHUB_RESOURCE_LIMITS` which sets resource limits for JupyterLab containers per Magpie user or group. The value for this variable is a whitespace delimited string. Each section is delimited by colons (:) where the first element is either `group` or `user` and the second element is the name of the user or group to apply the limits to. The rest are resource limits of the form `limit=amount`. For example: ```sh export JUPYTERHUB_RESOURCE_LIMITS=" user:user1:mem_limit=30G group:group1:mem_limit=10G:cpu_limit=1 group:group2:cpu_limit=3 " ``` Supported limits are: [mem_limit](https://jupyterhub-dockerspawner.readthedocs.io/en/latest/api/index.html#dockerspawner.DockerSpawner.mem_limit) and [cpu_limit](https://jupyterhub-dockerspawner.readthedocs.io/en/latest/api/index.html#dockerspawner.DockerSpawner.cpu_limit). Note that this will not create the groups in Magpie, that must be done by some other means (through configuration files or the Magpie API or UI). Note that if a user belongs to multiple groups, later values in `JUPYTERHUB_RESOURCE_LIMITS` will take precedence. For example, if a user named user1 belongs to group1 and group2 then the following limits will apply: - mem_limit=10G (because group1 is later in the list) - cpu_limit=3 (because group2 is later in the list) ## Changes **Non-breaking changes** - can specify per-group jupyterlab resource limits **Breaking changes** - None ## Related Issue / Discussion These limits are the simplest to implement. Future PRs could use this mechanism to set other limits (if possible) such as GPU/TPU allocations, disk allocations, IO limits, etc. ## Additional Information This only works with the version of the `MagpieAuthenticator` in this PR: Ouranosinc/jupyterhub#35 That PR needs to be merged first and the new `pavics/jupyterhub` docker image needs to be updated before all features introduced here will take effect. ## CI Operations <!-- The test suite can be run using a different DACCS config with ``birdhouse_daccs_configs_branch: branch_name`` in the PR description. To globally skip the test suite regardless of the commit message use ``birdhouse_skip_ci`` set to ``true`` in the PR description. Using ``[<cmd>]`` (with the brackets) where ``<cmd> = skip ci`` in the commit message will override ``birdhouse_skip_ci`` from the PR description. Such commit command can be used to override the PR description behavior for a specific commit update. However, a commit message cannot 'force run' a PR which the description turns off the CI. To run the CI, the PR should instead be updated with a ``true`` value, and a running message can be posted in following PR comments to trigger tests once again. --> birdhouse_daccs_configs_branch: master birdhouse_skip_ci: false
2 parents ee70e56 + d448d1f commit e2040fd

File tree

10 files changed

+117
-18
lines changed

10 files changed

+117
-18
lines changed

.bumpversion.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tool.bumpversion]
2-
current_version = "2.18.11"
2+
current_version = "2.18.12"
33
commit = true
44
tag = false
55
tag_name = "{new_version}"

CHANGES.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,41 @@
1717

1818
[//]: # (list changes here, using '-' for each new entry, remove this when items are added)
1919

20+
[2.18.12](https://github.com/bird-house/birdhouse-deploy/tree/2.18.12) (2025-11-25)
21+
------------------------------------------------------------------------------------------------------------------
22+
23+
## Changes
24+
25+
- Add mechanism to set CPU and memory limits on Jupyterlab containers based on Magpie user or group name
26+
27+
Creates a new variable `JUPYTERHUB_RESOURCE_LIMITS` which sets resource limits for JupyterLab containers per
28+
Magpie user or group.
29+
30+
The value for this variable is a whitespace delimited string. Each section is delimited by colons (:)
31+
where the first element is either `group` or `user` and the second element is the name of the user or group
32+
to apply the limits to. The rest are resource limits of the form `limit=amount`. For example:
33+
34+
```sh
35+
export JUPYTERHUB_RESOURCE_LIMITS="
36+
user:user1:mem_limit=30G
37+
group:group1:mem_limit=10G:cpu_limit=1
38+
group:group2:cpu_limit=3
39+
"
40+
```
41+
42+
Supported limits are:
43+
[mem_limit](https://jupyterhub-dockerspawner.readthedocs.io/en/latest/api/index.html#dockerspawner.DockerSpawner.mem_limit)
44+
and [cpu_limit](https://jupyterhub-dockerspawner.readthedocs.io/en/latest/api/index.html#dockerspawner.DockerSpawner.cpu_limit).
45+
46+
Note that this will not create the groups in Magpie, that must be done by some other means (through configuration files or the
47+
Magpie API or UI).
48+
49+
Note that if a user belongs to multiple groups, later values in `JUPYTERHUB_RESOURCE_LIMITS` will take
50+
precedence. For example, if a user named user1 belongs to group1 and group2 then the following limits will apply:
51+
52+
- mem_limit=10G (because group1 is later in the list)
53+
- cpu_limit=3 (because group2 is later in the list)
54+
2055
[2.18.11](https://github.com/bird-house/birdhouse-deploy/tree/2.18.11) (2025-11-13)
2156
------------------------------------------------------------------------------------------------------------------
2257

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ override BIRDHOUSE_MAKE_DIR := $(shell realpath -P $$(dirname $(BIRDHOUSE_MAKE_C
88
# Generic variables
99
override SHELL := bash
1010
override APP_NAME := birdhouse-deploy
11-
override APP_VERSION := 2.18.11
11+
override APP_VERSION := 2.18.12
1212

1313
# utility to remove comments after value of an option variable
1414
override clean_opt = $(shell echo "$(1)" | $(_SED) -r -e "s/[ '$'\t'']+$$//g")

README.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ for a full-fledged production platform.
1818
* - citation
1919
- | |citation|
2020

21-
.. |commits-since| image:: https://img.shields.io/github/commits-since/bird-house/birdhouse-deploy/2.18.11.svg
21+
.. |commits-since| image:: https://img.shields.io/github/commits-since/bird-house/birdhouse-deploy/2.18.12.svg
2222
:alt: Commits since latest release
23-
:target: https://github.com/bird-house/birdhouse-deploy/compare/2.18.11...master
23+
:target: https://github.com/bird-house/birdhouse-deploy/compare/2.18.12...master
2424

25-
.. |latest-version| image:: https://img.shields.io/badge/tag-2.18.11-blue.svg?style=flat
25+
.. |latest-version| image:: https://img.shields.io/badge/tag-2.18.12-blue.svg?style=flat
2626
:alt: Latest Tag
27-
:target: https://github.com/bird-house/birdhouse-deploy/tree/2.18.11
27+
:target: https://github.com/bird-house/birdhouse-deploy/tree/2.18.12
2828

2929
.. |readthedocs| image:: https://readthedocs.org/projects/birdhouse-deploy/badge/?version=latest
3030
:alt: ReadTheDocs Build Status (latest version)

RELEASE.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.18.11 2025-11-13T20:15:43Z
1+
2.18.12 2025-11-25T02:46:23Z

birdhouse/components/canarie-api/docker_configuration.py.template

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ SERVICES = {
108108
# NOTE:
109109
# Below version and release time auto-managed by 'make VERSION=x.y.z bump'.
110110
# Do NOT modify it manually. See 'Tagging policy' in 'birdhouse/README.rst'.
111-
'version': '2.18.11',
112-
'releaseTime': '2025-11-13T20:15:43Z',
111+
'version': '2.18.12',
112+
'releaseTime': '2025-11-25T02:46:23Z',
113113
'institution': '${BIRDHOUSE_INSTITUTION}',
114114
'researchSubject': '${BIRDHOUSE_SUBJECT}',
115115
'supportEmail': '${BIRDHOUSE_SUPPORT_EMAIL}',
@@ -141,8 +141,8 @@ PLATFORMS = {
141141
# NOTE:
142142
# Below version and release time auto-managed by 'make VERSION=x.y.z bump'.
143143
# Do NOT modify it manually. See 'Tagging policy' in 'birdhouse/README.rst'.
144-
'version': '2.18.11',
145-
'releaseTime': '2025-11-13T20:15:43Z',
144+
'version': '2.18.12',
145+
'releaseTime': '2025-11-25T02:46:23Z',
146146
'institution': '${BIRDHOUSE_INSTITUTION}',
147147
'researchSubject': '${BIRDHOUSE_SUBJECT}',
148148
'supportEmail': '${BIRDHOUSE_SUPPORT_EMAIL}',

birdhouse/components/jupyterhub/default.env

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# are applied and must be added to the list of DELAYED_EVAL.
66

77
export JUPYTERHUB_DOCKER=pavics/jupyterhub
8-
export JUPYTERHUB_VERSION=5.2.1-20241114
8+
export JUPYTERHUB_VERSION=5.4.2-20251123
99
export JUPYTERHUB_IMAGE='${JUPYTERHUB_DOCKER}:${JUPYTERHUB_VERSION}'
1010
export JUPYTERHUB_IMAGE_URI='registry.hub.docker.com/${JUPYTERHUB_IMAGE}'
1111

@@ -33,7 +33,6 @@ export JUPYTER_GOOGLE_DRIVE_SETTINGS=""
3333
export JUPYTER_DEMO_USER="demo"
3434
# Changing any limits requires restarting the jupyter user server
3535
export JUPYTER_DEMO_USER_MEM_LIMIT="2G" # ex: 2G, 500M
36-
# CPU limit seems not honored by DockerSpawner
3736
export JUPYTER_DEMO_USER_CPU_LIMIT="0.5" # 50% of 1 CPU
3837

3938
# See config/jupyterhub/custom_templates/login.html.template
@@ -73,6 +72,26 @@ export JUPYTERHUB_AUTHENTICATOR_REFRESH_AGE=60
7372
# Usernames that should be given admin access in jupyterhub
7473
export JUPYTERHUB_ADMIN_USERS='{\"${MAGPIE_ADMIN_USERNAME}\"}' # python set syntax
7574

75+
# Resource limits for JupyterLab containers. Resource limits can be set per Magpie user or group.
76+
# The value for this variable is a whitespace delimited string. Each section is delimited by colons (:)
77+
# where the first element is either `group` or `user` and the second element is the name of the user or group
78+
# to apply the limits to. The rest are resource limits of the form `limit=amount`. For example:
79+
#
80+
# export JUPYTERHUB_RESOURCE_LIMITS="
81+
# user:user1:mem_limit=30G
82+
# group:group1:mem_limit=10G:cpu_limit=1
83+
# group:group2:cpu_limit=3
84+
# "
85+
#
86+
# Supported limits are: `mem_limit` and `cpu_limit`. See the Jupyterhub Dockerspawner documentation
87+
# for details and supported values.
88+
# Note that this will not create the groups in Magpie, that must be done manually.
89+
# Note that if a user belongs to multiple groups, later values in `JUPYTERHUB_RESOURCE_LIMITS` will take
90+
# precedence. For example, if a user named user1 belongs to group1 and group2 then the following limits will apply:
91+
# - mem_limit=10G (because group1 is later in the list)
92+
# - cpu_limit=3 (because group2 is later in the list)
93+
export JUPYTERHUB_RESOURCE_LIMITS=
94+
7695
export DELAYED_EVAL="
7796
$DELAYED_EVAL
7897
JUPYTERHUB_USER_DATA_DIR
@@ -105,6 +124,7 @@ OPTIONAL_VARS="
105124
\$JUPYTER_IDLE_KERNEL_CULL_TIMEOUT
106125
\$JUPYTER_IDLE_KERNEL_CULL_INTERVAL
107126
\$JUPYTERHUB_USER_DATA_DIR
127+
\$JUPYTERHUB_RESOURCE_LIMITS
108128
"
109129

110130
# add any component that this component requires to run

birdhouse/components/jupyterhub/jupyterhub_config.py.template

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ if os.environ['WORKSPACE_DIR'] != jupyterhub_data_dir:
137137
container_gdrive_settings_path = join(container_home_dir, ".jupyter/lab/user-settings/@jupyterlab/google-drive/drive.jupyterlab-settings")
138138
host_gdrive_settings_path = os.environ['JUPYTER_GOOGLE_DRIVE_SETTINGS']
139139

140+
# resource_limits: dict[tuple[Literal["user", "group"], str], dict[Literal["cpu_limit", "mem_limit"], str]]
141+
resource_limits = {tuple(lim[:2]): dict(li.split("=") for li in lim[2:] if "=" in li)
142+
for limit in """${JUPYTERHUB_RESOURCE_LIMITS}""".strip().split()
143+
if (lim := limit.split(":"))}
144+
140145
if len(host_gdrive_settings_path) > 0:
141146
c.DockerSpawner.volumes[host_gdrive_settings_path] = {
142147
"bind": container_gdrive_settings_path,
@@ -169,13 +174,29 @@ def create_dir_hook(spawner):
169174
subprocess.call(["chown", f"{os.environ['USER_WORKSPACE_UID']}:{os.environ['USER_WORKSPACE_GID']}",
170175
workspace_user_dir])
171176

172-
if username == os.environ['JUPYTER_DEMO_USER']:
177+
178+
def limit_resource_hook(spawner):
179+
if spawner.user.name == os.environ['JUPYTER_DEMO_USER']:
173180
# Restrict resources for the public demo user
174181
# CPU limit, seems not honored by DockerSpawner
175182
spawner.cpu_limit = float(os.environ['JUPYTER_DEMO_USER_CPU_LIMIT'])
176183
spawner.mem_limit = os.environ['JUPYTER_DEMO_USER_MEM_LIMIT']
177-
178-
c.Spawner.pre_spawn_hook = create_dir_hook
184+
185+
user_groups = {g.name for g in spawner.user.groups}
186+
for (name_type, name), limits in resource_limits.items():
187+
if (name_type == "user" and name == spawner.user.name) or (name_type == "group" and name in user_groups):
188+
for limit, value in limits.items():
189+
if limit == "cpu_limit":
190+
spawner.cpu_limit = float(value)
191+
elif limit == "mem_limit":
192+
spawner.mem_limit = value
193+
194+
195+
def pre_spawn_hook(spawner):
196+
create_dir_hook(spawner)
197+
limit_resource_hook(spawner)
198+
199+
c.Spawner.pre_spawn_hook = pre_spawn_hook
179200

180201
## Disable per-user configuration of single-user servers.
181202
c.Spawner.disable_user_config = True

birdhouse/env.local.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,28 @@ export GEOSERVER_ADMIN_PASSWORD="${__DEFAULT__GEOSERVER_ADMIN_PASSWORD}"
363363
# JUPYTERHUB_CRYPT_KEY is set.
364364
#export JUPYTERHUB_AUTHENTICATOR_REFRESH_AGE=60
365365

366+
# Resource limits for JupyterLab containers. Resource limits can be set per Magpie user or group.
367+
# The value for this variable is a whitespace delimited string. Each section is delimited by colons (:)
368+
# where the first element is either `group` or `user` and the second element is the name of the user or group
369+
# to apply the limits to. The rest are resource limits of the form `limit=amount`. For example:
370+
#
371+
# export JUPYTERHUB_RESOURCE_LIMITS="
372+
# user:user1:mem_limit=30G
373+
# group:group1:mem_limit=10G:cpu_limit=1
374+
# group:group2:cpu_limit=3
375+
# "
376+
#
377+
# Supported limits are: `mem_limit` and `cpu_limit`. See the Jupyterhub Dockerspawner documentation
378+
# for details and supported values.
379+
# - https://jupyterhub-dockerspawner.readthedocs.io/en/latest/api/index.html#dockerspawner.DockerSpawner.cpu_limit
380+
# - https://jupyterhub-dockerspawner.readthedocs.io/en/latest/api/index.html#dockerspawner.DockerSpawner.mem_limit
381+
# Note that this will not create the groups in Magpie, that must be done manually.
382+
# Note that if a user belongs to multiple groups, later values in `JUPYTERHUB_RESOURCE_LIMITS` will take
383+
# precedence. For example, if a user named user1 belongs to group1 and group2 then the following limits will apply:
384+
# - mem_limit=10G (because group1 is later in the list)
385+
# - cpu_limit=3 (because group2 is later in the list)
386+
#export JUPYTERHUB_RESOURCE_LIMITS=
387+
366388
# Allow for adding new config or override existing config in
367389
# config/jupyterhub/jupyterhub_config.py.template.
368390
#
@@ -574,6 +596,7 @@ export THREDDS_ADDITIONAL_CATALOG=''
574596
#export JUPYTER_DEMO_USER="demo"
575597
# Changing any limits requires restarting the jupyter user server
576598
#export JUPYTER_DEMO_USER_MEM_LIMIT="2G" # ex: 2G, 500M
599+
#export JUPYTER_DEMO_USER_CPU_LIMIT="0.5" # 50% of 1 CPU
577600

578601
# See config/jupyterhub/custom_templates/login.html.template
579602
#export JUPYTER_LOGIN_BANNER_TOP_SECTION=""

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@
6969
# built documents.
7070
#
7171
# The short X.Y version.
72-
version = '2.18.11'
72+
version = '2.18.12'
7373
# The full version, including alpha/beta/rc tags.
74-
release = '2.18.11'
74+
release = '2.18.12'
7575

7676
# The language for content autogenerated by Sphinx. Refer to documentation
7777
# for a list of supported languages.

0 commit comments

Comments
 (0)