Skip to content

Commit 038eb64

Browse files
authored
Merge pull request #773 from MetaCell/feature/netpyne-44
Netpyne-44 Add mechanism to be able to react on kernel status change from the
2 parents 63a2f71 + 3ef3185 commit 038eb64

10 files changed

Lines changed: 370 additions & 21 deletions

File tree

webapp/components/NetPyNE.js

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
Dialog,
1111
ConfirmationDialog,
1212
LaunchDialog,
13-
TutorialObserver
13+
TutorialObserver,
1414
} from 'netpyne/components';
15-
import { loadModel } from '../redux/actions/general';
15+
import { loadModel, openDialog } from '../redux/actions/general';
16+
// import { execPythonMessage } from './general/GeppettoJupyterUtils';
17+
import { replayAll } from './general/CommandRecorder';
1618

1719
const styles = ({ zIndex }) => ({
1820
root: {
@@ -46,6 +48,11 @@ class NetPyNE extends React.Component {
4648
super(props);
4749
this.openPythonCallDialog = this.openPythonCallDialog.bind(this);
4850
this.loaded = false;
51+
this.kernelRestartState = {
52+
state: "idle",
53+
kernelID: undefined,
54+
crashLoop: false
55+
}
4956
}
5057

5158
componentDidMount () {
@@ -66,7 +73,7 @@ class NetPyNE extends React.Component {
6673
return;
6774
}
6875
this.loaded = true;
69-
console.log('Netpyne is ready');
76+
console.log('NetPyNE-UI component is ready');
7077
if (window !== window.parent) {
7178
window.parent.postMessage({
7279
type: 'APP_READY',
@@ -84,7 +91,107 @@ class NetPyNE extends React.Component {
8491
};
8592
// A message from the parent frame can specify the file to load
8693
window.addEventListener('message', loadFromEvent);
87-
// window.load = loadFromEvent
94+
95+
// Logic for kernel reinit
96+
const handleKernelRestart = ({ detail: { kernel, type } }) => {
97+
switch (this.kernelRestartState.state) {
98+
case "restarting":
99+
if (type === "kernel_ready" || type === "kernel_autorestarting") {
100+
console.log("Replaying all commands since the beginning of the session")
101+
replayAll(this.kernelRestartState.kernelID)
102+
this.kernelRestartState = {
103+
...this.kernelRestartState,
104+
state: "idle",
105+
kernelID: undefined,
106+
}
107+
}
108+
case "idle":
109+
if (type === "kernel_connected") {
110+
console.log("Kernel is connecting/starting, being init")
111+
this.kernelRestartState = {
112+
...this.kernelRestartState,
113+
state: "init",
114+
kernelID: kernel.id
115+
}
116+
}
117+
else if (type === "kernel_autorestarting") {
118+
console.log("Kernel restart event caught, trying to re-init the current model")
119+
this.kernelRestartState = {
120+
...this.kernelRestartState,
121+
state: "restarting",
122+
kernelID: kernel.id
123+
}
124+
if (!this.kernelRestartState.crashLoop) {
125+
this.props.dispatchAction(openDialog({
126+
title: "Kernel restart",
127+
message: "An action occured that made the kernel restart. We are reloading your model and all the actions you applied on it."
128+
}))
129+
}
130+
}
131+
else if (type === "kernel_restarting") {
132+
console.log("Kernel restart, perhaps it's a special restart?")
133+
this.kernelRestartState = {
134+
...this.kernelRestartState,
135+
state: "special_restart",
136+
kernelID: kernel.id
137+
}
138+
}
139+
case "init":
140+
if (type === "kernel_ready") {
141+
console.log("Kernel properly initialized")
142+
this.kernelRestartState = {
143+
...this.kernelRestartState,
144+
state: "idle",
145+
kernelID: undefined,
146+
}
147+
}
148+
case "special_restart":
149+
if (type == "kernel_autorestarting") {
150+
console.log("Kernel autorestart after a start, we might not have the ready event, we force it then")
151+
replayAll(this.kernelRestartState.kernelID)
152+
this.kernelRestartState = {
153+
...this.kernelRestartState,
154+
state: 'restarting',
155+
kernelID: kernel.id,
156+
}
157+
} else {
158+
console.log("Regular restart detected")
159+
this.kernelRestartState = {
160+
...this.kernelRestartState,
161+
state: 'idle',
162+
kernelID: undefined,
163+
}
164+
}
165+
}
166+
}
167+
window.addEventListener('kernelstatus', handleKernelRestart)
168+
169+
// Dedicated code to handle crash loops
170+
const kernelRestartLoopHandler = ( ) => {
171+
if (!this.kernelRestartState.crashLoop) {
172+
this.props.dispatchAction(openDialog({
173+
title: "Kernel restart loop stabilization",
174+
message: "One of your actions triggered a kernel restart loop. We are trying to identify the faulty command and to restore your model until this point. Close this window and wait for the kernel stabilization notification."
175+
}))
176+
} else {
177+
clearTimeout(this.kernelRestartState.crashLoop)
178+
}
179+
const taskID = setTimeout((_this) => {
180+
this.props.dispatchAction(openDialog({
181+
title: "Kernel restart loop stabilized",
182+
message: "The kernel is now stabilized."
183+
}));
184+
this.kernelRestartState = {
185+
...this.kernelRestartState,
186+
crashLoop: false
187+
}
188+
}, 8000, this)
189+
this.kernelRestartState = {
190+
...this.kernelRestartState,
191+
crashLoop: taskID
192+
}
193+
}
194+
window.addEventListener('kernelRestartLoop', kernelRestartLoopHandler)
88195
}
89196

90197
componentWillUnmount () {
@@ -115,6 +222,9 @@ class NetPyNE extends React.Component {
115222
<div className={classes.container}>
116223
<div className={classes.topbar}>
117224
<Topbar />
225+
{/* <button onClick={() => {
226+
execPythonMessage("utils.convertToJS(netpyne_geppetto.importCellTemplate(utils.convertToPython('{\"cellArgs\":{},\"fileName\":\"/home/vince/git-repository/metacell/NetPyNE-UI/workspace/cells/FScell.hoc\",\"cellName\":\"FScell\",\"label\":\"CellType1\",\"modFolder\":\"/home/vince/git-repository/metacell/NetPyNE-UI/workspace/mod\",\"importSynMechs\":false,\"compileMod\":false}')))")
227+
}}>CRASH ME</button> */}
118228
</div>
119229
<Box p={1} flex={1} display="flex" alignItems="stretch">
120230
<Grid container spacing={1} className={classes.content} alignItems="stretch">
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { KERNEL_HANDLING } from '../../constants';
2+
import { store } from '../../redux/actiondomainStore'
3+
import { recordCommand, dropLastCommand, dropFromIndex } from '../../redux/actions/actiondomain';
4+
import { execPythonMessage, execPythonMessageWithoutRecording } from './GeppettoJupyterUtils';
5+
6+
7+
const registerKernelListeners = () => {
8+
try {
9+
if(IPython.notebook.kernel == null) {
10+
console.warn("Kernel not initialized. Waiting to register kernel event listeners");
11+
setTimeout(registerKernelListeners, 500);
12+
return;
13+
}
14+
} catch (error) {
15+
console.warn("IPython not initialized. Waiting to register kernel event listeners");
16+
setTimeout(registerKernelListeners, 500);
17+
return
18+
}
19+
20+
const notebook = IPython.notebook;
21+
const handleKernelStatusChange = (event, data) => {
22+
const kernelStatusEvent = new CustomEvent("kernelstatus", {
23+
detail: {
24+
"type": event.type,
25+
...data
26+
},
27+
});
28+
window.dispatchEvent(kernelStatusEvent);
29+
};
30+
31+
const handleExecutionRequest = (event, data) => {
32+
if (data.content.netpyne_ui_triggered) {
33+
return
34+
}
35+
const { kernel, content } = data;
36+
record(kernel.id, content.code);
37+
}
38+
39+
// Kernel lifecycle requests
40+
notebook.events.on('kernel_created.Kernel', handleKernelStatusChange);
41+
notebook.events.on('kernel_reconnecting.Kernel', handleKernelStatusChange);
42+
notebook.events.on('kernel_connected.Kernel', handleKernelStatusChange);
43+
notebook.events.on('kernel_starting.Kernel', handleKernelStatusChange);
44+
notebook.events.on('kernel_restarting.Kernel', handleKernelStatusChange);
45+
notebook.events.on('kernel_autorestarting.Kernel', handleKernelStatusChange);
46+
notebook.events.on('kernel_interrupting.Kernel', handleKernelStatusChange);
47+
notebook.events.on('kernel_disconnected.Kernel', handleKernelStatusChange);
48+
notebook.events.on('kernel_ready.Kernel', handleKernelStatusChange);
49+
notebook.events.on('kernel_killed.Kernel', handleKernelStatusChange);
50+
notebook.events.on('kernel_dead.Kernel', handleKernelStatusChange);
51+
52+
// Execution requests
53+
notebook.events.on('execution_request.Kernel', handleExecutionRequest);
54+
}
55+
registerKernelListeners();
56+
57+
58+
const record = (kernelID, command) => {
59+
store.dispatch(recordCommand(kernelID, command))
60+
}
61+
62+
const TIMEFRAME = 10 * 1000; // 10s
63+
let lastReplayTime = 0
64+
65+
const getCommands = (kernelID) => {
66+
return [
67+
"from jupyter_geppetto import jupyter_geppetto",
68+
"from jupyter_geppetto import utils",
69+
"from netpyne_ui.netpyne_geppetto import netpyne_geppetto",
70+
"netpyne_geppetto.deleteModel({})",
71+
`netpyne_geppetto.loadFromIndexFile("${KERNEL_HANDLING.tmpModelPath}")`,
72+
...store.getState()[kernelID]
73+
]
74+
}
75+
76+
const replayAll = (kernelID, fromRec = false) => {
77+
const currentTimestamp = Date.now();
78+
const commands = getCommands(kernelID);
79+
80+
if (!fromRec && currentTimestamp - lastReplayTime < TIMEFRAME) {
81+
const restartLoop = new CustomEvent("kernelRestartLoop", {
82+
detail: {
83+
"kernel": kernelID,
84+
"state": "looping"
85+
}
86+
});
87+
window.dispatchEvent(restartLoop);
88+
store.dispatch(dropLastCommand(kernelID))
89+
replayAll(kernelID, true)
90+
return
91+
}
92+
93+
const lastCommand = commands.pop() // we drop the last command which is probably the faulty one
94+
const script = commands.join('\n')
95+
console.log("Playing", script)
96+
console.log("Skipping last command", lastCommand)
97+
lastReplayTime = currentTimestamp
98+
execPythonMessageWithoutRecording(script).then(() => {
99+
store.dispatch(dropLastCommand(kernelID))
100+
})
101+
}
102+
103+
export { record, replayAll }

webapp/components/general/Dialog.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import React from 'react';
2-
import Button from '@material-ui/core/Button';
32
import MuiDialog from '@material-ui/core/Dialog';
4-
import DialogActions from '@material-ui/core/DialogActions';
5-
import DialogContent from '@material-ui/core/DialogContent';
6-
import Typography from '@material-ui/core/Typography';
7-
import DialogTitle from '@material-ui/core/DialogTitle';
8-
import Paper from '@material-ui/core/Paper';
9-
import Box from '@material-ui/core/Box';
10-
import Link from '@material-ui/core/Link';
11-
import Icon from '@material-ui/core/Icon';
3+
import { Button,DialogActions, DialogContent, DialogTitle, DialogContentText } from '@material-ui/core';
4+
import { Typography, Paper, Box, Link, Icon } from '@material-ui/core';
125
import { withStyles } from '@material-ui/core/styles';
136
import { secondaryColor, bgLight } from '../../theme';
147
import logoNetpyne from '../../static/netpyne-logo_white.png';
@@ -135,18 +128,31 @@ const ContributeContent = withStyles(styles)(({ classes }) => (
135128
</Paper>
136129
));
137130

131+
132+
const titleContentMapping = {
133+
Contribute: <ContributeContent />,
134+
About: <AboutContent />
135+
}
136+
138137
export default function Dialog ({
139138
open,
140139
title,
141140
message,
142141
handleClose,
143142
}) {
143+
const selectMessageContent = () => {
144+
if (title in titleContentMapping) {
145+
return titleContentMapping[title]
146+
}
147+
return <DialogContentText>{message}</DialogContentText>
148+
}
144149
return (
145150
<div>
146151
<MuiDialog fullWidth maxWidth="sm" open={open} onClose={handleClose}>
147152
<DialogTitle>{title}</DialogTitle>
148153
<DialogContent>
149-
{title === 'Contribute' ? <ContributeContent /> : <AboutContent />}
154+
{/* {title === 'Contribute' ? <ContributeContent /> : <AboutContent />} */}
155+
{selectMessageContent()}
150156
</DialogContent>
151157
<DialogActions>
152158
<Button onClick={handleClose} color="primary" autoFocus>

webapp/components/general/GeppettoJupyterUtils.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { record as recordCommand } from './CommandRecorder';
2+
3+
14
const handle_output = function (data) {
25
// data is the object passed to the callback from the kernel execution
36
switch (data.msg_type) {
@@ -29,9 +32,23 @@ const handle_output = function (data) {
2932
}
3033
};
3134

32-
const execPythonMessage = function (command, callback = handle_output) {
35+
const execPythonMessage = function (command, callback = handle_output, record = true) {
3336
const { kernel } = IPython.notebook;
34-
const messageID = kernel.execute(command, { iopub: { output: callback } }, { silent: false, stop_on_error: true, store_history: true });
37+
if (record) {
38+
recordCommand(kernel.id, command)
39+
}
40+
const messageID = kernel.execute(
41+
command,
42+
{
43+
iopub: { output: callback }
44+
},
45+
{
46+
silent: false,
47+
stop_on_error: true,
48+
store_history: true,
49+
netpyne_ui_triggered: true
50+
});
51+
3552

3653
return new Promise((resolve, reject) => GEPPETTO.on(GEPPETTO.Events.Receive_Python_Message, (data) => {
3754
if (data.data.id == messageID) {
@@ -40,11 +57,15 @@ const execPythonMessage = function (command, callback = handle_output) {
4057
}));
4158
};
4259

60+
const execPythonMessageWithoutRecording = function (command, callback = handle_output) {
61+
return execPythonMessage(command, callback, false)
62+
}
63+
4364
const addslashes = function (str) {
4465
return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0');
4566
}
4667

47-
const evalPythonMessage = function (command, parameters, parse = true) {
68+
const evalPythonMessage = function (command, parameters, parse = true, record = true) {
4869
let parametersString = '';
4970
if (parameters) {
5071
if (parameters.length > 0) {
@@ -58,7 +79,7 @@ const evalPythonMessage = function (command, parameters, parse = true) {
5879
if (parse) {
5980
finalCommand = `utils.convertToJS(${finalCommand})`;
6081
}
61-
return execPythonMessage(finalCommand, handle_output);
82+
return execPythonMessage(finalCommand, handle_output, record);
6283
};
6384

64-
export { execPythonMessage, evalPythonMessage };
85+
export { execPythonMessage, evalPythonMessage, execPythonMessageWithoutRecording };

webapp/components/topbar/dialogs/ActionDialog.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ class ActionDialog extends React.Component {
4646
}
4747
}
4848
}
49-
this.setState({ hide: true });
5049
if (this.props.onAction) {
5150
this.props.onAction();
5251
}
52+
this.setState({ hide: true });
53+
if (this.props.onRequestClose) {
54+
this.props.onRequestClose();
55+
}
5356
};
5457

5558
clearErrorDialogBox () {

webapp/components/topbar/dialogs/ActionValidationDialog.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ const ActionValidationDialog = (props) => {
4646
if (props.onAction) {
4747
props.onAction()
4848
}
49+
if (props.onRequestClose) {
50+
props.onRequestClose()
51+
}
4952
}
5053

5154
const clearErrorDialogBox = () => {

0 commit comments

Comments
 (0)