0
# Spawner System
1
2
The JupyterHub spawner system is responsible for creating, managing, and stopping single-user Jupyter notebook servers. It provides a pluggable architecture supporting local processes, containers, cloud platforms, and custom deployment scenarios.
3
4
## Capabilities
5
6
### Base Spawner Class
7
8
The foundation class for all spawner implementations in JupyterHub.
9
10
```python { .api }
11
class Spawner(LoggingConfigurable):
12
"""
13
Base class for spawning single-user notebook servers.
14
15
Subclass this to implement custom spawning mechanisms.
16
"""
17
18
# User and server information
19
user: User # The user this spawner belongs to
20
name: str # The name of the server (empty string for default server)
21
server: Server # The server object from the database
22
23
# Configuration attributes
24
start_timeout: int # Timeout for server startup (seconds)
25
http_timeout: int # Timeout for HTTP requests to server
26
poll_interval: int # Interval for polling server status
27
28
# Server connection info
29
ip: str # IP address server should bind to
30
port: int # Port server should bind to
31
url: str # Full URL to the server
32
33
# Environment and resources
34
environment: Dict[str, str] # Environment variables
35
cmd: List[str] # Command to start server
36
args: List[str] # Arguments to the command
37
38
async def start(self):
39
"""
40
Start the single-user server.
41
42
Returns:
43
(ip, port) tuple of where the server is listening,
44
or just port if ip is unchanged
45
"""
46
47
async def stop(self, now=False):
48
"""
49
Stop the single-user server.
50
51
Args:
52
now: If True, force immediate stop without graceful shutdown
53
"""
54
55
async def poll(self):
56
"""
57
Poll the spawned process to see if it is still running.
58
59
Returns:
60
None if still running, integer exit code if stopped
61
"""
62
63
def get_state(self):
64
"""
65
Get the current state of the spawner.
66
67
Returns:
68
Dictionary of state information for persistence
69
"""
70
71
def load_state(self, state):
72
"""
73
Load state from a previous session.
74
75
Args:
76
state: Dictionary of state information
77
"""
78
79
def clear_state(self):
80
"""
81
Clear any stored state about the server.
82
"""
83
84
def get_env(self):
85
"""
86
Get the complete environment for the server.
87
88
Returns:
89
Dictionary of environment variables
90
"""
91
92
def get_args(self):
93
"""
94
Get the complete argument list for starting the server.
95
96
Returns:
97
List of command arguments
98
"""
99
```
100
101
### Local Process Spawners
102
103
Spawners that run single-user servers as local system processes.
104
105
```python { .api }
106
class LocalProcessSpawner(Spawner):
107
"""
108
Spawner that runs single-user servers as local processes.
109
110
The default spawner implementation for JupyterHub.
111
"""
112
113
# Process management
114
proc: subprocess.Popen # The subprocess object
115
pid: int # Process ID of the spawned server
116
117
# Local user management
118
create_system_users: bool # Whether to create system users
119
shell_cmd: List[str] # Shell command for process execution
120
121
async def start(self):
122
"""
123
Start server as local subprocess.
124
125
Returns:
126
(ip, port) tuple where server is listening
127
"""
128
129
async def stop(self, now=False):
130
"""
131
Stop the local process.
132
133
Args:
134
now: If True, send SIGKILL instead of SIGTERM
135
"""
136
137
async def poll(self):
138
"""
139
Poll the local process.
140
141
Returns:
142
None if running, exit code if stopped
143
"""
144
145
def make_preexec_fn(self, name):
146
"""
147
Create preexec function for subprocess.
148
149
Args:
150
name: Username to switch to
151
152
Returns:
153
Function to call before exec in subprocess
154
"""
155
156
class SimpleLocalProcessSpawner(LocalProcessSpawner):
157
"""
158
Simplified local process spawner.
159
160
Doesn't switch users or create system users. Suitable for
161
single-user or development deployments.
162
"""
163
164
# Simplified configuration
165
create_system_users: bool = False
166
shell_cmd: List[str] = [] # No shell wrapper
167
```
168
169
## Usage Examples
170
171
### Basic Local Process Spawner
172
173
```python
174
# jupyterhub_config.py
175
c = get_config()
176
177
# Use local process spawner (default)
178
c.JupyterHub.spawner_class = 'localprocess'
179
180
# Configure spawner settings
181
c.Spawner.start_timeout = 60
182
c.Spawner.http_timeout = 30
183
184
# Set notebook directory
185
c.Spawner.notebook_dir = '/home/{username}/notebooks'
186
187
# Configure environment variables
188
c.Spawner.environment = {
189
'JUPYTER_ENABLE_LAB': 'yes',
190
'JUPYTERHUB_SINGLEUSER_APP': 'jupyter_server.serverapp.ServerApp'
191
}
192
```
193
194
### Custom Spawner Implementation
195
196
```python
197
from jupyterhub.spawner import LocalProcessSpawner
198
import docker
199
200
class DockerSpawner(LocalProcessSpawner):
201
"""Example Docker-based spawner"""
202
203
# Docker configuration
204
image: str = 'jupyter/base-notebook'
205
remove: bool = True
206
207
def __init__(self, **kwargs):
208
super().__init__(**kwargs)
209
self.client = docker.from_env()
210
self.container = None
211
212
async def start(self):
213
"""Start server in Docker container"""
214
# Docker container startup logic
215
self.container = self.client.containers.run(
216
image=self.image,
217
command=self.get_args(),
218
environment=self.get_env(),
219
ports={'8888/tcp': None},
220
detach=True,
221
remove=self.remove,
222
name=f'jupyter-{self.user.name}-{self.name}'
223
)
224
225
# Get container port mapping
226
port_info = self.container.attrs['NetworkSettings']['Ports']['8888/tcp'][0]
227
return (self.ip, int(port_info['HostPort']))
228
229
async def stop(self, now=False):
230
"""Stop Docker container"""
231
if self.container:
232
self.container.stop()
233
self.container = None
234
235
async def poll(self):
236
"""Poll container status"""
237
if not self.container:
238
return 1
239
240
self.container.reload()
241
status = self.container.status
242
243
if status == 'running':
244
return None
245
else:
246
return 1
247
248
# Register the spawner
249
c.JupyterHub.spawner_class = DockerSpawner
250
c.DockerSpawner.image = 'jupyter/scipy-notebook'
251
```
252
253
### Resource Management
254
255
```python
256
class ResourceManagedSpawner(LocalProcessSpawner):
257
"""Spawner with resource limits"""
258
259
mem_limit: str = '1G' # Memory limit
260
cpu_limit: float = 1.0 # CPU limit
261
262
def get_env(self):
263
"""Add resource limits to environment"""
264
env = super().get_env()
265
env.update({
266
'MEM_LIMIT': self.mem_limit,
267
'CPU_LIMIT': str(self.cpu_limit)
268
})
269
return env
270
271
async def start(self):
272
"""Start with resource constraints"""
273
# Set resource limits before starting
274
self.set_resource_limits()
275
return await super().start()
276
277
def set_resource_limits(self):
278
"""Apply resource limits to the process"""
279
# Implementation depends on deployment method
280
pass
281
282
# Configuration with resource management
283
c.JupyterHub.spawner_class = ResourceManagedSpawner
284
c.ResourceManagedSpawner.mem_limit = '2G'
285
c.ResourceManagedSpawner.cpu_limit = 2.0
286
```
287
288
### Profile-based Spawning
289
290
```python
291
class ProfileSpawner(LocalProcessSpawner):
292
"""Spawner with selectable profiles"""
293
294
profiles = [
295
{
296
'display_name': 'Basic Python',
297
'description': 'Basic Python environment',
298
'default': True,
299
'kubespawner_override': {
300
'image': 'jupyter/base-notebook',
301
'mem_limit': '1G',
302
'cpu_limit': 1
303
}
304
},
305
{
306
'display_name': 'Data Science',
307
'description': 'Full data science stack',
308
'kubespawner_override': {
309
'image': 'jupyter/datascience-notebook',
310
'mem_limit': '4G',
311
'cpu_limit': 2
312
}
313
}
314
]
315
316
def options_from_form(self, formdata):
317
"""Process spawner options from form"""
318
profile = formdata.get('profile', [None])[0]
319
return {'profile': profile}
320
321
def load_user_options(self, user_options):
322
"""Load user-selected options"""
323
profile_name = user_options.get('profile')
324
if profile_name:
325
# Apply profile configuration
326
profile = next(p for p in self.profiles if p['display_name'] == profile_name)
327
for key, value in profile.get('kubespawner_override', {}).items():
328
setattr(self, key, value)
329
```
330
331
## Advanced Patterns
332
333
### State Persistence
334
335
```python
336
class StatefulSpawner(LocalProcessSpawner):
337
"""Spawner that persists state across restarts"""
338
339
def get_state(self):
340
"""Get spawner state for persistence"""
341
state = super().get_state()
342
state.update({
343
'custom_setting': self.custom_setting,
344
'last_activity': self.last_activity.isoformat()
345
})
346
return state
347
348
def load_state(self, state):
349
"""Load persisted state"""
350
super().load_state(state)
351
self.custom_setting = state.get('custom_setting')
352
if 'last_activity' in state:
353
self.last_activity = datetime.fromisoformat(state['last_activity'])
354
```
355
356
### Health Monitoring
357
358
```python
359
class MonitoredSpawner(LocalProcessSpawner):
360
"""Spawner with health monitoring"""
361
362
async def poll(self):
363
"""Enhanced polling with health checks"""
364
# Check process status
365
status = await super().poll()
366
if status is not None:
367
return status
368
369
# Additional health checks
370
if not await self.health_check():
371
# Server is running but unhealthy
372
await self.restart_unhealthy_server()
373
374
return None
375
376
async def health_check(self):
377
"""Check if server is healthy"""
378
try:
379
# Make HTTP request to server
380
async with aiohttp.ClientSession() as session:
381
async with session.get(f'{self.url}/api/status') as resp:
382
return resp.status == 200
383
except:
384
return False
385
386
async def restart_unhealthy_server(self):
387
"""Restart an unhealthy server"""
388
await self.stop()
389
await self.start()
390
```
391
392
### Pre/Post Hooks
393
394
```python
395
class HookedSpawner(LocalProcessSpawner):
396
"""Spawner with pre/post hooks"""
397
398
async def start(self):
399
"""Start with pre/post hooks"""
400
await self.pre_spawn_hook()
401
402
try:
403
result = await super().start()
404
await self.post_spawn_hook()
405
return result
406
except Exception as e:
407
await self.spawn_error_hook(e)
408
raise
409
410
async def pre_spawn_hook(self):
411
"""Hook called before spawning"""
412
# Setup user workspace, check resources, etc.
413
pass
414
415
async def post_spawn_hook(self):
416
"""Hook called after successful spawn"""
417
# Register with external systems, send notifications, etc.
418
pass
419
420
async def spawn_error_hook(self, error):
421
"""Hook called on spawn error"""
422
# Log error, cleanup resources, send alerts, etc.
423
pass
424
```