Skip to content

Commit cbe7f6c

Browse files
authored
Merge pull request #2 from data-exp-lab/nb_culling
Nb culling
2 parents d1c9aa7 + 5982800 commit cbe7f6c

File tree

5 files changed

+89
-14
lines changed

5 files changed

+89
-14
lines changed

server/__init__.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33

4-
from girder import events
4+
import datetime
5+
6+
from girder import events, logger
57
from girder.models.model_base import ValidationException
68
from girder.api import access
79
from girder.api.describe import Description, describeRoute
@@ -10,22 +12,36 @@
1012
from .constants import PluginSettings
1113

1214
from girder.utility.model_importer import ModelImporter
13-
from girder.utility import assetstore_utilities
15+
from girder.utility import assetstore_utilities, setting_utilities
1416
from girder.api.rest import getCurrentUser, getApiUrl
1517

1618

19+
_last_culling = datetime.datetime.utcnow()
20+
21+
1722
class Job(Resource):
1823

1924
def __init__(self):
2025
super(Job, self).__init__()
2126

2227

23-
def validateSettings(event):
24-
if event.info['key'] == PluginSettings.TMPNB_URL:
25-
if not event.info['value']:
26-
raise ValidationException(
27-
'TmpNB URL must not be empty.', 'value')
28-
event.preventDefault().stopPropagation()
28+
@setting_utilities.validator(PluginSettings.TMPNB_URL)
29+
def validateTmpNbUrl(doc):
30+
if not doc['value']:
31+
raise ValidationException(
32+
'TmpNB URL must not be empty.', 'value')
33+
34+
35+
@setting_utilities.validator(PluginSettings.CULLING_PERIOD)
36+
def validateCullingPeriod(doc):
37+
try:
38+
float(doc['value'])
39+
except KeyError:
40+
raise ValidationException(
41+
'Culling period must not be empty.', 'value')
42+
except ValueError:
43+
raise ValidationException(
44+
'Culling period must float.', 'value')
2945

3046

3147
class ytHub(Resource):
@@ -158,9 +174,8 @@ def getNotebook(self, notebook, params):
158174
.errorResponse('Write access was denied for the notebook.', 403)
159175
)
160176
def deleteNotebook(self, notebook, params):
161-
notebookModel = self.model('notebook', 'ythub')
162-
notebookModel.deleteNotebook(notebook, self.getCurrentToken())
163-
notebookModel.remove(notebook)
177+
self.model('notebook', 'ythub').deleteNotebook(
178+
notebook, self.getCurrentToken())
164179

165180
@access.user
166181
@loadmodel(model='folder', level=AccessType.READ)
@@ -296,10 +311,20 @@ def folderRootpath(self, folder, params):
296311
folder, user=self.getCurrentUser())
297312

298313

314+
def cullNotebooks(event):
315+
global _last_culling
316+
logger.info('Got heartbeat in cullNotebooks')
317+
culling_freq = datetime.timedelta(minutes=1)
318+
if datetime.datetime.utcnow() - culling_freq > _last_culling:
319+
logger.info('Performing culling')
320+
ModelImporter.model('notebook', 'ythub').cullNotebooks()
321+
_last_culling = datetime.datetime.utcnow()
322+
323+
299324
def load(info):
300-
events.bind('model.setting.validate', 'ythub', validateSettings)
301325
events.bind('filesystem_assetstore_imported', 'ythub',
302326
saveImportPathToMeta)
327+
events.bind('heartbeat', 'ythub', cullNotebooks)
303328
info['apiRoot'].ythub = ytHub()
304329
info['apiRoot'].notebook = Notebook()
305330
info['apiRoot'].folder.route('GET', (':id', 'contents'),

server/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
class PluginSettings:
9+
CULLING_PERIOD = 'ythub.culling_period'
910
TMPNB_URL = 'ythub.tmpnb_url'
1011

1112

server/models/notebook.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import json
66
import requests
77
import six
8+
import dateutil.parser
89

10+
from girder import logger
911
from ..constants import PluginSettings
1012
from girder.api.rest import RestException
1113
from girder.constants import AccessType, SortDir
@@ -74,6 +76,46 @@ def deleteNotebook(self, notebook, token):
7476
}
7577
requests.delete(self.model('setting').get(PluginSettings.TMPNB_URL),
7678
json=payload)
79+
# TODO: handle error
80+
self.remove(notebook)
81+
82+
def cullNotebooks(self):
83+
resp = requests.get(
84+
self.model('setting').get(PluginSettings.TMPNB_URL))
85+
content = resp.content
86+
if isinstance(content, six.binary_type):
87+
content = content.decode('utf8')
88+
try:
89+
resp.raise_for_status()
90+
except requests.HTTPError:
91+
raise RestException(
92+
'Got %s code from tmpnb, response="%s"/' % (
93+
resp.status_code, content
94+
), code=502)
95+
try:
96+
activity = json.loads(content)
97+
except ValueError:
98+
raise RestException('Non-JSON response: %s' % content, code=502)
99+
100+
admin = next(_ for _ in self.model('user').getAdmins())
101+
token = self.model('token').createToken(user=admin, days=1)
102+
103+
# Iterate over all notebooks, not the prettiest way...
104+
cull_period = self.model('setting').get(
105+
PluginSettings.CULLING_PERIOD, '4')
106+
cull_time = datetime.datetime.utcnow() - \
107+
datetime.timedelta(hours=float(cull_period))
108+
for nb in self.find({}):
109+
try:
110+
last_activity = dateutil.parser.parse(
111+
activity[nb['containerId']], ignoretz=True)
112+
except KeyError:
113+
# proxy is not aware of such container, kill it...
114+
logger.info('deleting nb %s' % nb['_id'])
115+
self.deleteNotebook(nb, token)
116+
if last_activity < cull_time:
117+
logger.info('deleting nb %s' % nb['_id'])
118+
self.deleteNotebook(nb, token)
77119

78120
def createNotebook(self, folder, user, token, when=None, save=True):
79121
existing = self.findOne({

web_client/js/ConfigView.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ girder.views.ythub_ConfigView = girder.View.extend({
1010
this._saveSettings([{
1111
key: 'ythub.tmpnb_url',
1212
value: this.$('#ythub_tmpnb').val().trim()
13+
}, {
14+
key: 'ythub.culling_period',
15+
value: this.$('#ythub_culling').val().trim()
1316
}]);
1417
}
1518
},
@@ -19,13 +22,14 @@ girder.views.ythub_ConfigView = girder.View.extend({
1922
path: 'system/setting',
2023
data: {
2124
list: JSON.stringify([
22-
'ythub.tmpnb_url'
25+
'ythub.tmpnb_url',
26+
'ythub.culling_period'
2327
])
2428
}
2529
}).done(_.bind(function (resp) {
2630
this.render();
2731
this.$('#ythub_tmpnb').val(resp['ythub.tmpnb_url']);
28-
32+
this.$('#ythub_culling').val(resp['ythub.culling_period']);
2933
}, this));
3034
},
3135

web_client/templates/ythub_config.jade

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ form#g-ythub-config-form(role="form")
44
label.control-label(for="ythub_tmpnb") tmpnb URL
55
input.input-sm.form-control#ythub_tmpnb(
66
type="text", placeholder="Temporary notebook server's URL")
7+
label.control-label(for="ythub_culling") Cull notebooks that are inactive for that many hours
8+
input.input-sm.form-control#ythub_culling(
9+
type="text", placeholder="Maximum inactivity period (hours) before notebook is deleted")
710

811
p#g-ythub-error-message.g-validation-failed-message
912
input.btn.btn-sm.btn-primary(type="submit", value="Save")

0 commit comments

Comments
 (0)