diff --git a/README.md b/README.md index d8e03d2..4ac8d11 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ in your `jupyterhub_config.py` file: - **[`mem_limit`](#mem_limit)** - **[`cpu_limit`](#cpu_limit)** +- **[`cpu_weight`](#cpu_weight)** - **[`user_workingdir`](#user_workingdir)** - **[`username_template`](#username_template)** - **[`default_shell`](#default_shell)** @@ -214,6 +215,23 @@ This works out perfect for most cases, since this allows users to burst up and use all CPU when nobody else is using CPU & forces them to automatically yield when other users want to use the CPU. +The share of access each user gets can be adjusted with the `cpu_weight` option. + +### `cpu_weight` + +An integer representing the share of CPU time each user can use. A user with +a `cpu_weight` of 200 will get 2x access to the CPU than a user with a `cpu_weight` +of 100, which is the system default. + +```python +c.SystemdSpawner.cpu_weight = 100 +``` + +Defaults to `None`, which implicitly means 100. + +This info is exposed to the single-user server as the environment variable +`CPU_WEIGHT` as an integer. + ### `user_workingdir` The directory to spawn each user's notebook server in. This directory is what users diff --git a/systemdspawner/systemdspawner.py b/systemdspawner/systemdspawner.py index cc2850d..1a27482 100644 --- a/systemdspawner/systemdspawner.py +++ b/systemdspawner/systemdspawner.py @@ -5,7 +5,7 @@ from jupyterhub.spawner import Spawner from jupyterhub.utils import random_port -from traitlets import Bool, Dict, List, Unicode +from traitlets import Bool, Dict, Integer, List, Unicode from systemdspawner import systemd @@ -146,6 +146,18 @@ class SystemdSpawner(Spawner): """, ).tag(config=True) + cpu_weight = Integer( + None, + allow_none=True, + help=""" + Assign a CPU weight to the single-user notebook server. + Available CPU time is allocated in proportion to each user's weight. + + Acceptable value: an integer between 1 to 10000. + System default is 100. + """, + ).tag(config=True) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # All traitlets configurables are configured by now @@ -276,6 +288,9 @@ async def start(self): else: working_dir = self._expand_user_vars(self.user_workingdir) + if self.cpu_weight: + env["CPU_WEIGHT"] = str(self.cpu_weight) + if self.isolate_tmp: properties["PrivateTmp"] = "yes" @@ -304,6 +319,11 @@ async def start(self): properties["CPUAccounting"] = "yes" properties["CPUQuota"] = f"{int(self.cpu_limit * 100)}%" + if self.cpu_weight is not None: + # FIXME: Detect & use proper properties for v1 vs v2 cgroups + properties["CPUAccounting"] = "yes" + properties["CPUWeight"] = str(self.cpu_weight) + if self.disable_user_sudo: properties["NoNewPrivileges"] = "yes"