##// END OF EJS Templates
enable graceful restart of kernels in notebook
MinRK -
Show More
@@ -1,324 +1,326 b''
1 1 """A kernel manager for multiple kernels.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 import signal
21 21 import sys
22 22 import uuid
23 23
24 24 import zmq
25 25 from zmq.eventloop.zmqstream import ZMQStream
26 26
27 27 from tornado import web
28 28
29 29 from IPython.config.configurable import LoggingConfigurable
30 30 from IPython.utils.importstring import import_item
31 31 from IPython.utils.traitlets import (
32 32 Instance, Dict, List, Unicode, Float, Integer, Any, DottedObjectName,
33 33 )
34 34 #-----------------------------------------------------------------------------
35 35 # Classes
36 36 #-----------------------------------------------------------------------------
37 37
38 38 class DuplicateKernelError(Exception):
39 39 pass
40 40
41 41
42 42 class MultiKernelManager(LoggingConfigurable):
43 43 """A class for managing multiple kernels."""
44 44
45 45 kernel_manager_class = DottedObjectName(
46 "IPython.zmq.kernelmanager.KernelManager", config=True,
46 "IPython.zmq.blockingkernelmanager.BlockingKernelManager", config=True,
47 47 help="""The kernel manager class. This is configurable to allow
48 48 subclassing of the KernelManager for customized behavior.
49 49 """
50 50 )
51 51 def _kernel_manager_class_changed(self, name, old, new):
52 52 self.kernel_manager_factory = import_item(new)
53 53
54 54 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
55 55 def _kernel_manager_factory_default(self):
56 56 return import_item(self.kernel_manager_class)
57 57
58 58 context = Instance('zmq.Context')
59 59 def _context_default(self):
60 60 return zmq.Context.instance()
61 61
62 62 connection_dir = Unicode('')
63 63
64 64 _kernels = Dict()
65 65
66 66 @property
67 67 def kernel_ids(self):
68 68 """Return a list of the kernel ids of the active kernels."""
69 69 return self._kernels.keys()
70 70
71 71 def __len__(self):
72 72 """Return the number of running kernels."""
73 73 return len(self.kernel_ids)
74 74
75 75 def __contains__(self, kernel_id):
76 76 if kernel_id in self.kernel_ids:
77 77 return True
78 78 else:
79 79 return False
80 80
81 81 def start_kernel(self, **kwargs):
82 82 """Start a new kernel."""
83 83 kernel_id = unicode(uuid.uuid4())
84 84 # use base KernelManager for each Kernel
85 85 km = self.kernel_manager_factory(connection_file=os.path.join(
86 86 self.connection_dir, "kernel-%s.json" % kernel_id),
87 87 config=self.config,
88 88 )
89 89 km.start_kernel(**kwargs)
90 # start the shell channel, needed for graceful restart
91 km.start_channels(shell=True, sub=False, stdin=False, hb=False)
90 92 self._kernels[kernel_id] = km
91 93 return kernel_id
92 94
93 95 def kill_kernel(self, kernel_id):
94 96 """Kill a kernel by its kernel uuid.
95 97
96 98 Parameters
97 99 ==========
98 100 kernel_id : uuid
99 101 The id of the kernel to kill.
100 102 """
101 103 self.get_kernel(kernel_id).kill_kernel()
102 104 del self._kernels[kernel_id]
103 105
104 106 def interrupt_kernel(self, kernel_id):
105 107 """Interrupt (SIGINT) the kernel by its uuid.
106 108
107 109 Parameters
108 110 ==========
109 111 kernel_id : uuid
110 112 The id of the kernel to interrupt.
111 113 """
112 114 return self.get_kernel(kernel_id).interrupt_kernel()
113 115
114 116 def signal_kernel(self, kernel_id, signum):
115 117 """ Sends a signal to the kernel by its uuid.
116 118
117 119 Note that since only SIGTERM is supported on Windows, this function
118 120 is only useful on Unix systems.
119 121
120 122 Parameters
121 123 ==========
122 124 kernel_id : uuid
123 125 The id of the kernel to signal.
124 126 """
125 127 return self.get_kernel(kernel_id).signal_kernel(signum)
126 128
127 129 def get_kernel(self, kernel_id):
128 130 """Get the single KernelManager object for a kernel by its uuid.
129 131
130 132 Parameters
131 133 ==========
132 134 kernel_id : uuid
133 135 The id of the kernel.
134 136 """
135 137 km = self._kernels.get(kernel_id)
136 138 if km is not None:
137 139 return km
138 140 else:
139 141 raise KeyError("Kernel with id not found: %s" % kernel_id)
140 142
141 143 def get_kernel_ports(self, kernel_id):
142 144 """Return a dictionary of ports for a kernel.
143 145
144 146 Parameters
145 147 ==========
146 148 kernel_id : uuid
147 149 The id of the kernel.
148 150
149 151 Returns
150 152 =======
151 153 port_dict : dict
152 154 A dict of key, value pairs where the keys are the names
153 155 (stdin_port,iopub_port,shell_port) and the values are the
154 156 integer port numbers for those channels.
155 157 """
156 158 # this will raise a KeyError if not found:
157 159 km = self.get_kernel(kernel_id)
158 160 return dict(shell_port=km.shell_port,
159 161 iopub_port=km.iopub_port,
160 162 stdin_port=km.stdin_port,
161 163 hb_port=km.hb_port,
162 164 )
163 165
164 166 def get_kernel_ip(self, kernel_id):
165 167 """Return ip address for a kernel.
166 168
167 169 Parameters
168 170 ==========
169 171 kernel_id : uuid
170 172 The id of the kernel.
171 173
172 174 Returns
173 175 =======
174 176 ip : str
175 177 The ip address of the kernel.
176 178 """
177 179 return self.get_kernel(kernel_id).ip
178 180
179 181 def create_connected_stream(self, ip, port, socket_type):
180 182 sock = self.context.socket(socket_type)
181 183 addr = "tcp://%s:%i" % (ip, port)
182 184 self.log.info("Connecting to: %s" % addr)
183 185 sock.connect(addr)
184 186 return ZMQStream(sock)
185 187
186 188 def create_iopub_stream(self, kernel_id):
187 189 ip = self.get_kernel_ip(kernel_id)
188 190 ports = self.get_kernel_ports(kernel_id)
189 191 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
190 192 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
191 193 return iopub_stream
192 194
193 195 def create_shell_stream(self, kernel_id):
194 196 ip = self.get_kernel_ip(kernel_id)
195 197 ports = self.get_kernel_ports(kernel_id)
196 198 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.DEALER)
197 199 return shell_stream
198 200
199 201 def create_hb_stream(self, kernel_id):
200 202 ip = self.get_kernel_ip(kernel_id)
201 203 ports = self.get_kernel_ports(kernel_id)
202 204 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
203 205 return hb_stream
204 206
205 207
206 208 class MappingKernelManager(MultiKernelManager):
207 209 """A KernelManager that handles notebok mapping and HTTP error handling"""
208 210
209 211 kernel_argv = List(Unicode)
210 212
211 213 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
212 214 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
213 215
214 216 max_msg_size = Integer(65536, config=True, help="""
215 217 The max raw message size accepted from the browser
216 218 over a WebSocket connection.
217 219 """)
218 220
219 221 _notebook_mapping = Dict()
220 222
221 223 #-------------------------------------------------------------------------
222 224 # Methods for managing kernels and sessions
223 225 #-------------------------------------------------------------------------
224 226
225 227 def kernel_for_notebook(self, notebook_id):
226 228 """Return the kernel_id for a notebook_id or None."""
227 229 return self._notebook_mapping.get(notebook_id)
228 230
229 231 def set_kernel_for_notebook(self, notebook_id, kernel_id):
230 232 """Associate a notebook with a kernel."""
231 233 if notebook_id is not None:
232 234 self._notebook_mapping[notebook_id] = kernel_id
233 235
234 236 def notebook_for_kernel(self, kernel_id):
235 237 """Return the notebook_id for a kernel_id or None."""
236 238 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
237 239 if len(notebook_ids) == 1:
238 240 return notebook_ids[0]
239 241 else:
240 242 return None
241 243
242 244 def delete_mapping_for_kernel(self, kernel_id):
243 245 """Remove the kernel/notebook mapping for kernel_id."""
244 246 notebook_id = self.notebook_for_kernel(kernel_id)
245 247 if notebook_id is not None:
246 248 del self._notebook_mapping[notebook_id]
247 249
248 250 def start_kernel(self, notebook_id=None):
249 251 """Start a kernel for a notebok an return its kernel_id.
250 252
251 253 Parameters
252 254 ----------
253 255 notebook_id : uuid
254 256 The uuid of the notebook to associate the new kernel with. If this
255 257 is not None, this kernel will be persistent whenever the notebook
256 258 requests a kernel.
257 259 """
258 260 kernel_id = self.kernel_for_notebook(notebook_id)
259 261 if kernel_id is None:
260 262 kwargs = dict()
261 263 kwargs['extra_arguments'] = self.kernel_argv
262 264 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
263 265 self.set_kernel_for_notebook(notebook_id, kernel_id)
264 266 self.log.info("Kernel started: %s" % kernel_id)
265 267 self.log.debug("Kernel args: %r" % kwargs)
266 268 else:
267 269 self.log.info("Using existing kernel: %s" % kernel_id)
268 270 return kernel_id
269 271
270 272 def kill_kernel(self, kernel_id):
271 273 """Kill a kernel and remove its notebook association."""
272 274 self._check_kernel_id(kernel_id)
273 275 super(MappingKernelManager, self).kill_kernel(kernel_id)
274 276 self.delete_mapping_for_kernel(kernel_id)
275 277 self.log.info("Kernel killed: %s" % kernel_id)
276 278
277 279 def interrupt_kernel(self, kernel_id):
278 280 """Interrupt a kernel."""
279 281 self._check_kernel_id(kernel_id)
280 282 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
281 283 self.log.info("Kernel interrupted: %s" % kernel_id)
282 284
283 285 def restart_kernel(self, kernel_id):
284 286 """Restart a kernel while keeping clients connected."""
285 287 self._check_kernel_id(kernel_id)
286 288 km = self.get_kernel(kernel_id)
287 km.restart_kernel(now=True)
289 km.restart_kernel()
288 290 self.log.info("Kernel restarted: %s" % kernel_id)
289 291 return kernel_id
290 292
291 293 # the following remains, in case the KM restart machinery is
292 294 # somehow unacceptable
293 295 # Get the notebook_id to preserve the kernel/notebook association.
294 296 notebook_id = self.notebook_for_kernel(kernel_id)
295 297 # Create the new kernel first so we can move the clients over.
296 298 new_kernel_id = self.start_kernel()
297 299 # Now kill the old kernel.
298 300 self.kill_kernel(kernel_id)
299 301 # Now save the new kernel/notebook association. We have to save it
300 302 # after the old kernel is killed as that will delete the mapping.
301 303 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
302 304 self.log.info("Kernel restarted: %s" % new_kernel_id)
303 305 return new_kernel_id
304 306
305 307 def create_iopub_stream(self, kernel_id):
306 308 """Create a new iopub stream."""
307 309 self._check_kernel_id(kernel_id)
308 310 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
309 311
310 312 def create_shell_stream(self, kernel_id):
311 313 """Create a new shell stream."""
312 314 self._check_kernel_id(kernel_id)
313 315 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
314 316
315 317 def create_hb_stream(self, kernel_id):
316 318 """Create a new hb stream."""
317 319 self._check_kernel_id(kernel_id)
318 320 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
319 321
320 322 def _check_kernel_id(self, kernel_id):
321 323 """Check a that a kernel_id exists and raise 404 if not."""
322 324 if kernel_id not in self:
323 325 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
324 326
General Comments 0
You need to be logged in to leave comments. Login now