mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-11-15 01:05:42 +00:00
Remove user session loop. Instead send ressource updates directly on change
This commit is contained in:
parent
ee9fdd1017
commit
996ed1c790
@ -4,12 +4,13 @@ from flask_login import current_user
|
|||||||
from socket import gaierror
|
from socket import gaierror
|
||||||
from .. import db, socketio
|
from .. import db, socketio
|
||||||
from ..decorators import socketio_login_required
|
from ..decorators import socketio_login_required
|
||||||
from ..events import connected_sessions
|
from ..events import socketio_sessions
|
||||||
from ..models import Corpus, User
|
from ..models import Corpus, User
|
||||||
import cqi
|
import cqi
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -282,7 +283,7 @@ def corpus_analysis_session_handler(app, corpus_id, user_id, session_id):
|
|||||||
response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload}
|
response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload}
|
||||||
socketio.emit('corpus_analysis_init', response, room=session_id)
|
socketio.emit('corpus_analysis_init', response, room=session_id)
|
||||||
''' Observe analysis session '''
|
''' Observe analysis session '''
|
||||||
while session_id in connected_sessions:
|
while session_id in socketio_sessions:
|
||||||
socketio.sleep(3)
|
socketio.sleep(3)
|
||||||
''' Teardown analysis session '''
|
''' Teardown analysis session '''
|
||||||
if client.status == 'running':
|
if client.status == 'running':
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .. import db
|
from .. import db, socketio
|
||||||
from ..decorators import background
|
from ..decorators import background
|
||||||
from ..models import Corpus, CorpusFile, QueryResult
|
from ..models import Corpus, CorpusFile, QueryResult
|
||||||
|
|
||||||
@ -12,6 +12,11 @@ def build_corpus(corpus_id, *args, **kwargs):
|
|||||||
raise Exception('Corpus {} not found'.format(corpus_id))
|
raise Exception('Corpus {} not found'.format(corpus_id))
|
||||||
corpus.build()
|
corpus.build()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
event = 'user_{}_patch'.format(corpus.user_id)
|
||||||
|
jsonpatch = [{'op': 'replace', 'path': '/corpora/{}/last_edited_date'.format(corpus.id), 'value': corpus.last_edited_date.timestamp()}, # noqa
|
||||||
|
{'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}] # noqa
|
||||||
|
room = 'user_{}'.format(corpus.user_id)
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
@background
|
@background
|
||||||
@ -20,8 +25,12 @@ def delete_corpus(corpus_id, *args, **kwargs):
|
|||||||
corpus = Corpus.query.get(corpus_id)
|
corpus = Corpus.query.get(corpus_id)
|
||||||
if corpus is None:
|
if corpus is None:
|
||||||
raise Exception('Corpus {} not found'.format(corpus_id))
|
raise Exception('Corpus {} not found'.format(corpus_id))
|
||||||
|
event = 'user_{}_patch'.format(corpus.user_id)
|
||||||
|
jsonpatch = [{'op': 'remove', 'path': '/corpora/{}'.format(corpus.id)}]
|
||||||
|
room = 'user_{}'.format(corpus.user_id)
|
||||||
corpus.delete()
|
corpus.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
@background
|
@background
|
||||||
@ -30,8 +39,13 @@ def delete_corpus_file(corpus_file_id, *args, **kwargs):
|
|||||||
corpus_file = CorpusFile.query.get(corpus_file_id)
|
corpus_file = CorpusFile.query.get(corpus_file_id)
|
||||||
if corpus_file is None:
|
if corpus_file is None:
|
||||||
raise Exception('Corpus file {} not found'.format(corpus_file_id))
|
raise Exception('Corpus file {} not found'.format(corpus_file_id))
|
||||||
|
event = 'user_{}_patch'.format(corpus_file.corpus.user_id)
|
||||||
|
jsonpatch = [{'op': 'remove', 'path': '/corpora/{}/files/{}'.format(corpus_file.corpus_id, corpus_file.id)}, # noqa
|
||||||
|
{'op': 'replace', 'path': '/corpora/{}/status'.format(corpus_file.corpus_id), 'value': corpus_file.corpus.status}] # noqa
|
||||||
|
room = 'user_{}'.format(corpus_file.corpus.user_id)
|
||||||
corpus_file.delete()
|
corpus_file.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
@background
|
@background
|
||||||
@ -40,5 +54,9 @@ def delete_query_result(query_result_id, *args, **kwargs):
|
|||||||
query_result = QueryResult.query.get(query_result_id)
|
query_result = QueryResult.query.get(query_result_id)
|
||||||
if query_result is None:
|
if query_result is None:
|
||||||
raise Exception('QueryResult {} not found'.format(query_result_id))
|
raise Exception('QueryResult {} not found'.format(query_result_id))
|
||||||
|
event = 'user_{}_patch'.format(query_result.user_id)
|
||||||
|
jsonpatch = [{'op': 'remove', 'path': '/query_results/{}'.format(query_result.id)}] # noqa
|
||||||
|
room = 'user_{}'.format(query_result.user_id)
|
||||||
query_result.delete()
|
query_result.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
|
@ -8,7 +8,7 @@ from .forms import (AddCorpusFileForm, AddCorpusForm, AddQueryResultForm,
|
|||||||
DisplayOptionsForm, InspectDisplayOptionsForm,
|
DisplayOptionsForm, InspectDisplayOptionsForm,
|
||||||
ImportCorpusForm)
|
ImportCorpusForm)
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from .. import db
|
from .. import db, socketio
|
||||||
from ..models import Corpus, CorpusFile, QueryResult
|
from ..models import Corpus, CorpusFile, QueryResult
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -29,16 +29,22 @@ def add_corpus():
|
|||||||
description=form.description.data,
|
description=form.description.data,
|
||||||
title=form.title.data)
|
title=form.title.data)
|
||||||
db.session.add(corpus)
|
db.session.add(corpus)
|
||||||
db.session.commit()
|
db.session.flush()
|
||||||
|
db.session.refresh(corpus)
|
||||||
try:
|
try:
|
||||||
os.makedirs(corpus.path)
|
os.makedirs(corpus.path)
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.error('Make dir {} led to an OSError!'.format(corpus.path))
|
logging.error('Make dir {} led to an OSError!'.format(corpus.path))
|
||||||
db.session.delete(corpus)
|
db.session.rollback()
|
||||||
db.session.commit()
|
|
||||||
abort(500)
|
abort(500)
|
||||||
flash('Corpus "{}" added!'.format(corpus.title), 'corpus')
|
else:
|
||||||
return redirect(url_for('.corpus', corpus_id=corpus.id))
|
db.session.commit()
|
||||||
|
flash('Corpus "{}" added!'.format(corpus.title), 'corpus')
|
||||||
|
event = 'user_{}_patch'.format(corpus.user_id)
|
||||||
|
jsonpatch = [{'op': 'add', 'path': '/corpora/{}'.format(corpus.id), 'value': corpus.to_dict()}] # noqa
|
||||||
|
room = 'user_{}'.format(corpus.user_id)
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
|
return redirect(url_for('.corpus', corpus_id=corpus.id))
|
||||||
return render_template('corpora/add_corpus.html.j2', form=form,
|
return render_template('corpora/add_corpus.html.j2', form=form,
|
||||||
title='Add corpus')
|
title='Add corpus')
|
||||||
|
|
||||||
@ -54,13 +60,13 @@ def import_corpus():
|
|||||||
description=form.description.data,
|
description=form.description.data,
|
||||||
title=form.title.data)
|
title=form.title.data)
|
||||||
db.session.add(corpus)
|
db.session.add(corpus)
|
||||||
db.session.commit()
|
db.session.flush()
|
||||||
|
db.session.refresh(corpus)
|
||||||
try:
|
try:
|
||||||
os.makedirs(corpus.path)
|
os.makedirs(corpus.path)
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.error('Make dir {} led to an OSError!'.format(corpus.path))
|
logging.error('Make dir {} led to an OSError!'.format(corpus.path))
|
||||||
db.session.delete(corpus)
|
db.session.rollback()
|
||||||
db.session.commit()
|
|
||||||
flash('Internal Server Error', 'error')
|
flash('Internal Server Error', 'error')
|
||||||
return make_response(
|
return make_response(
|
||||||
{'redirect_url': url_for('.import_corpus')}, 500)
|
{'redirect_url': url_for('.import_corpus')}, 500)
|
||||||
@ -100,6 +106,10 @@ def import_corpus():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
os.remove(archive_file)
|
os.remove(archive_file)
|
||||||
flash('Corpus "{}" imported!'.format(corpus.title), 'corpus')
|
flash('Corpus "{}" imported!'.format(corpus.title), 'corpus')
|
||||||
|
event = 'user_{}_patch'.format(corpus.user_id)
|
||||||
|
jsonpatch = [{'op': 'add', 'path': '/corpora/{}'.format(corpus.id), 'value': corpus.to_dict()}] # noqa
|
||||||
|
room = 'user_{}'.format(corpus.user_id)
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
return make_response(
|
return make_response(
|
||||||
{'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201)
|
{'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201)
|
||||||
else:
|
else:
|
||||||
@ -205,6 +215,11 @@ def add_corpus_file(corpus_id):
|
|||||||
corpus.status = 'unprepared'
|
corpus.status = 'unprepared'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Corpus file "{}" added!'.format(corpus_file.filename), 'corpus')
|
flash('Corpus file "{}" added!'.format(corpus_file.filename), 'corpus')
|
||||||
|
event = 'user_{}_patch'.format(corpus.user_id)
|
||||||
|
jsonpatch = [{'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}, # noqa
|
||||||
|
{'op': 'add', 'path': '/corpora/{}/files/{}'.format(corpus.id, corpus_file.id), 'value': corpus_file.to_dict()}] # noqa
|
||||||
|
room = 'user_{}'.format(corpus.user_id)
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
return make_response({'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201) # noqa
|
return make_response({'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201) # noqa
|
||||||
return render_template('corpora/add_corpus_file.html.j2', corpus=corpus,
|
return render_template('corpora/add_corpus_file.html.j2', corpus=corpus,
|
||||||
form=form, title='Add corpus file')
|
form=form, title='Add corpus file')
|
||||||
|
@ -1,73 +1,72 @@
|
|||||||
from flask import current_app, request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from . import db, socketio
|
from flask_socketio import join_room, leave_room
|
||||||
from .decorators import socketio_admin_required, socketio_login_required
|
from . import socketio
|
||||||
|
from .decorators import socketio_login_required
|
||||||
from .models import User
|
from .models import User
|
||||||
import json
|
|
||||||
import jsonpatch
|
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Socket.IO event handlers #
|
||||||
|
###############################################################################
|
||||||
'''
|
'''
|
||||||
' A list containing session ids of connected Socket.IO sessions, to keep track
|
' A list containing session ids of connected Socket.IO sessions, to keep track
|
||||||
' of all connected sessions, which is used to determine the runtimes of
|
' of all connected sessions, which can be used to determine the runtimes of
|
||||||
' associated background tasks.
|
' associated background tasks.
|
||||||
'''
|
'''
|
||||||
connected_sessions = []
|
socketio_sessions = []
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('connect')
|
@socketio.on('connect')
|
||||||
def connect():
|
@socketio_login_required
|
||||||
|
def socketio_connect():
|
||||||
'''
|
'''
|
||||||
' The Socket.IO module creates a session id (sid) for each request.
|
' The Socket.IO module creates a session id (sid) for each request.
|
||||||
' On connect the sid is saved in the connected sessions list.
|
' On connect the sid is saved in the connected sessions list.
|
||||||
'''
|
'''
|
||||||
connected_sessions.append(request.sid)
|
socketio_sessions.append(request.sid)
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('disconnect')
|
@socketio.on('disconnect')
|
||||||
def disconnect():
|
def socketio_disconnect():
|
||||||
'''
|
'''
|
||||||
' On disconnect the session id gets removed from the connected sessions
|
' On disconnect the session id gets removed from the connected sessions
|
||||||
' list.
|
' list.
|
||||||
'''
|
'''
|
||||||
connected_sessions.remove(request.sid)
|
socketio_sessions.remove(request.sid)
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('start_user_session')
|
@socketio.on('start_user_session')
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def start_user_session(user_id):
|
def socketio_start_user_session(user_id):
|
||||||
if not (current_user.id == user_id or current_user.is_administrator):
|
user = User.query.get(user_id)
|
||||||
return
|
if user is None:
|
||||||
socketio.start_background_task(user_session,
|
response = {'code': 404, 'msg': 'Not found'}
|
||||||
current_app._get_current_object(),
|
socketio.emit('start_user_session', response, room=request.sid)
|
||||||
user_id, request.sid)
|
elif not (user == current_user or current_user.is_administrator):
|
||||||
|
response = {'code': 403, 'msg': 'Forbidden'}
|
||||||
|
socketio.emit('start_user_session', response, room=request.sid)
|
||||||
|
else:
|
||||||
|
response = {'code': 200, 'msg': 'OK'}
|
||||||
|
socketio.emit('start_user_session', response, room=request.sid)
|
||||||
|
socketio.emit('user_{}_init'.format(user.id), user.to_dict(),
|
||||||
|
room=request.sid)
|
||||||
|
room = 'user_{}'.format(user.id)
|
||||||
|
join_room(room)
|
||||||
|
|
||||||
|
|
||||||
def user_session(app, user_id, session_id):
|
@socketio.on('stop_user_session')
|
||||||
'''
|
@socketio_login_required
|
||||||
' Sends initial user data to the client. Afterwards it checks every 3s if
|
def socketio_stop_user_session(user_id):
|
||||||
' changes to the initial values appeared. If changes are detected, a
|
user = User.query.get(user_id)
|
||||||
' RFC 6902 compliant JSON patch gets send.
|
if user is None:
|
||||||
'''
|
response = {'code': 404, 'msg': 'Not found'}
|
||||||
init_event = 'user_{}_init'.format(user_id)
|
socketio.emit('stop_user_session', response, room=request.sid)
|
||||||
patch_event = 'user_{}_patch'.format(user_id)
|
elif not (user == current_user or current_user.is_administrator):
|
||||||
with app.app_context():
|
response = {'code': 403, 'msg': 'Forbidden'}
|
||||||
# Gather current values from database.
|
socketio.emit('stop_user_session', response, room=request.sid)
|
||||||
user = User.query.get(user_id)
|
else:
|
||||||
user_dict = user.to_dict()
|
response = {'code': 200, 'msg': 'OK'}
|
||||||
# Send initial values to the client.
|
socketio.emit('stop_user_session', response, room=request.sid)
|
||||||
socketio.emit(init_event, json.dumps(user_dict), room=session_id)
|
room = 'user_{}'.format(user.id)
|
||||||
while session_id in connected_sessions:
|
leave_room(room)
|
||||||
# Get new values from the database
|
|
||||||
db.session.refresh(user)
|
|
||||||
new_user_dict = user.to_dict()
|
|
||||||
# Compute JSON patches.
|
|
||||||
user_patch = jsonpatch.JsonPatch.from_diff(user_dict,
|
|
||||||
new_user_dict)
|
|
||||||
# In case there are patches, send them to the client.
|
|
||||||
if user_patch:
|
|
||||||
socketio.emit(patch_event, user_patch.to_string(),
|
|
||||||
room=session_id)
|
|
||||||
# Set new values as references for the next iteration.
|
|
||||||
user_dict = new_user_dict
|
|
||||||
socketio.sleep(3)
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .. import db
|
from .. import db, socketio
|
||||||
from ..decorators import background
|
from ..decorators import background
|
||||||
from ..models import Job
|
from ..models import Job
|
||||||
|
|
||||||
@ -9,8 +9,12 @@ def delete_job(job_id, *args, **kwargs):
|
|||||||
job = Job.query.get(job_id)
|
job = Job.query.get(job_id)
|
||||||
if job is None:
|
if job is None:
|
||||||
raise Exception('Job {} not found'.format(job_id))
|
raise Exception('Job {} not found'.format(job_id))
|
||||||
|
event = 'user_{}_patch'.format(job.user_id)
|
||||||
|
jsonpatch = [{'op': 'remove', 'path': '/jobs/{}'.format(job.id)}]
|
||||||
|
room = 'user_{}'.format(job.user_id)
|
||||||
job.delete()
|
job.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
@background
|
@background
|
||||||
@ -19,5 +23,14 @@ def restart_job(job_id, *args, **kwargs):
|
|||||||
job = Job.query.get(job_id)
|
job = Job.query.get(job_id)
|
||||||
if job is None:
|
if job is None:
|
||||||
raise Exception('Job {} not found'.format(job_id))
|
raise Exception('Job {} not found'.format(job_id))
|
||||||
job.restart()
|
try:
|
||||||
db.session.commit()
|
job.restart()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
db.session.commit()
|
||||||
|
event = 'user_{}_patch'.format(job.user_id)
|
||||||
|
jsonpatch = [{'op': 'replace', 'path': '/jobs/{}/end_date'.format(job.id), 'value': job.end_date.timestamp()}, # noqa
|
||||||
|
{'op': 'replace', 'path': '/jobs/{}/status'.format(job.id), 'value': job.status}] # noqa
|
||||||
|
room = 'user_{}'.format(job.user_id)
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
|
@ -2,7 +2,7 @@ from flask import abort, flash, make_response, render_template, url_for
|
|||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from . import services
|
from . import services
|
||||||
from .. import db
|
from .. import db, socketio
|
||||||
from ..jobs.forms import AddFileSetupJobForm, AddNLPJobForm, AddOCRJobForm
|
from ..jobs.forms import AddFileSetupJobForm, AddNLPJobForm, AddOCRJobForm
|
||||||
from ..models import Job, JobInput
|
from ..models import Job, JobInput
|
||||||
import json
|
import json
|
||||||
@ -70,6 +70,10 @@ def service(service):
|
|||||||
job.status = 'submitted'
|
job.status = 'submitted'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Job "{}" added'.format(job.title), 'job')
|
flash('Job "{}" added'.format(job.title), 'job')
|
||||||
|
event = 'user_{}_patch'.format(job.user_id)
|
||||||
|
jsonpatch = [{'op': 'add', 'path': '/jobs/{}'.format(job.id), 'value': job.to_dict()}] # noqa
|
||||||
|
room = 'user_{}'.format(job.user_id)
|
||||||
|
socketio.emit(event, jsonpatch, room=room)
|
||||||
return make_response(
|
return make_response(
|
||||||
{'redirect_url': url_for('jobs.job', job_id=job.id)}, 201)
|
{'redirect_url': url_for('jobs.job', job_id=job.id)}, 201)
|
||||||
return render_template('services/{}.html.j2'.format(service),
|
return render_template('services/{}.html.j2'.format(service),
|
||||||
|
@ -9,8 +9,8 @@ class AppClient {
|
|||||||
if (userId in this.users) {return this.users[userId];}
|
if (userId in this.users) {return this.users[userId];}
|
||||||
let user = new User();
|
let user = new User();
|
||||||
this.users[userId] = user;
|
this.users[userId] = user;
|
||||||
this.socket.on(`user_${userId}_init`, msg => user.init(JSON.parse(msg)));
|
this.socket.on(`user_${userId}_init`, msg => user.init(msg));
|
||||||
this.socket.on(`user_${userId}_patch`, msg => user.patch(JSON.parse(msg)));
|
this.socket.on(`user_${userId}_patch`, msg => user.patch(msg));
|
||||||
this.socket.emit('start_user_session', userId);
|
this.socket.emit('start_user_session', userId);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@ -40,40 +40,44 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(data) {
|
init(data) {
|
||||||
|
console.log('### User.init ###');
|
||||||
|
console.log(data);
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
|
||||||
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
|
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
|
||||||
if (corpusId === '*') {
|
if (corpusId === '*') {
|
||||||
for (let eventListener of eventListeners) {eventListener('init', this.data.corpora);}
|
for (let eventListener of eventListeners) {eventListener('init');}
|
||||||
} else {
|
} else {
|
||||||
if (corpusId in this.data.corpora) {
|
if (corpusId in this.data.corpora) {
|
||||||
for (let eventListener of eventListeners) {eventListener('init', this.data.corpora[corpusId]);}
|
for (let eventListener of eventListeners) {eventListener('init');}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
|
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
|
||||||
if (jobId === '*') {
|
if (jobId === '*') {
|
||||||
for (let eventListener of eventListeners) {eventListener('init', this.data.jobs);}
|
for (let eventListener of eventListeners) {eventListener('init');}
|
||||||
} else {
|
} else {
|
||||||
if (jobId in this.data.jobs) {
|
if (jobId in this.data.jobs) {
|
||||||
for (let eventListener of eventListeners) {eventListener('init', this.data.jobs[jobId]);}
|
for (let eventListener of eventListeners) {eventListener('init');}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
|
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
|
||||||
if (queryResultId === '*') {
|
if (queryResultId === '*') {
|
||||||
for (let eventListener of eventListeners) {eventListener('init', this.data.query_results);}
|
for (let eventListener of eventListeners) {eventListener('init');}
|
||||||
} else {
|
} else {
|
||||||
if (queryResultId in this.data.query_results) {
|
if (queryResultId in this.data.query_results) {
|
||||||
for (let eventListener of eventListeners) {eventListener('init', this.data.query_results[queryResultId]);}
|
for (let eventListener of eventListeners) {eventListener('init');}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(patch) {
|
patch(patch) {
|
||||||
|
console.log('### User.patch ###');
|
||||||
|
console.log(patch);
|
||||||
this.data = jsonpatch.apply_patch(this.data, patch);
|
this.data = jsonpatch.apply_patch(this.data, patch);
|
||||||
|
|
||||||
let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
||||||
|
@ -1,31 +1,57 @@
|
|||||||
from .. import db
|
from .corpus_utils import CheckCorporaMixin
|
||||||
from ..models import Corpus, Job
|
from .job_utils import CheckJobsMixin
|
||||||
|
from ..import db, socketio
|
||||||
import docker
|
import docker
|
||||||
|
|
||||||
|
|
||||||
docker_client = docker.from_env()
|
class TaskRunner(CheckCorporaMixin, CheckJobsMixin):
|
||||||
from . import corpus_utils, job_utils # noqa
|
def __init__(self):
|
||||||
|
self.docker = docker.from_env()
|
||||||
|
self._socketio_message_buffer = {}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.check_corpora()
|
||||||
|
self.check_jobs()
|
||||||
|
db.session.commit()
|
||||||
|
self.flush_socketio_messages()
|
||||||
|
|
||||||
|
def buffer_socketio_message(self, event, payload, room,
|
||||||
|
msg_id=None, override_policy='replace'):
|
||||||
|
if room not in self._socketio_message_buffer:
|
||||||
|
self._socketio_message_buffer[room] = {}
|
||||||
|
if event not in self._socketio_message_buffer[room]:
|
||||||
|
self._socketio_message_buffer[room][event] = {}
|
||||||
|
if msg_id is None:
|
||||||
|
msg_id = len(self._socketio_message_buffer[room][event].keys())
|
||||||
|
if override_policy == 'append':
|
||||||
|
if msg_id in self._socketio_message_buffer[room][event]:
|
||||||
|
# If the current message value isn't a list, convert it!
|
||||||
|
if not isinstance(self._socketio_message_buffer[room][event][msg_id], list): # noqa
|
||||||
|
self._socketio_message_buffer[room][event][msg_id] = [self._socketio_message_buffer[room][event][msg_id]] # noqa
|
||||||
|
else:
|
||||||
|
self._socketio_message_buffer[room][event][msg_id] = []
|
||||||
|
self._socketio_message_buffer[room][event][msg_id].append(payload)
|
||||||
|
elif override_policy == 'replace':
|
||||||
|
self._socketio_message_buffer[room][event][msg_id] = payload
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown override policy: {}'.format(override_policy)) # noqa
|
||||||
|
return msg_id
|
||||||
|
|
||||||
|
def buffer_user_patch_operation(self, ressource, patch_operation):
|
||||||
|
self.buffer_socketio_message('user_{}_patch'.format(ressource.user_id),
|
||||||
|
patch_operation,
|
||||||
|
'user_{}'.format(ressource.user_id),
|
||||||
|
msg_id='_', override_policy='append')
|
||||||
|
|
||||||
|
def clear_socketio_message_buffer(self):
|
||||||
|
self._socketio_message_buffer = {}
|
||||||
|
|
||||||
|
def flush_socketio_messages(self):
|
||||||
|
for room in self._socketio_message_buffer:
|
||||||
|
for event in self._socketio_message_buffer[room]:
|
||||||
|
for message in self._socketio_message_buffer[room][event]:
|
||||||
|
socketio.emit(event, self._socketio_message_buffer[room][event][message], room=room) # noqa
|
||||||
|
self.clear_socketio_message_buffer()
|
||||||
|
|
||||||
|
|
||||||
def check_corpora():
|
task_runner = TaskRunner()
|
||||||
corpora = Corpus.query.all()
|
|
||||||
for corpus in filter(lambda corpus: corpus.status == 'submitted', corpora):
|
|
||||||
corpus_utils.create_build_corpus_service(corpus)
|
|
||||||
for corpus in filter(lambda corpus: corpus.status in ['queued', 'running'], corpora): # noqa
|
|
||||||
corpus_utils.checkout_build_corpus_service(corpus)
|
|
||||||
for corpus in filter(lambda corpus: corpus.status == 'start analysis', corpora): # noqa
|
|
||||||
corpus_utils.create_cqpserver_container(corpus)
|
|
||||||
for corpus in filter(lambda corpus: corpus.status == 'stop analysis', corpora): # noqa
|
|
||||||
corpus_utils.remove_cqpserver_container(corpus)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def check_jobs():
|
|
||||||
jobs = Job.query.all()
|
|
||||||
for job in filter(lambda job: job.status == 'submitted', jobs):
|
|
||||||
job_utils.create_job_service(job)
|
|
||||||
for job in filter(lambda job: job.status in ['queued', 'running'], jobs):
|
|
||||||
job_utils.checkout_job_service(job)
|
|
||||||
for job in filter(lambda job: job.status == 'canceling', jobs):
|
|
||||||
job_utils.remove_job_service(job)
|
|
||||||
db.session.commit()
|
|
||||||
|
@ -1,174 +1,204 @@
|
|||||||
from . import docker_client
|
from ..models import Corpus
|
||||||
import docker
|
import docker
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
def create_build_corpus_service(corpus):
|
class CheckCorporaMixin:
|
||||||
corpus_data_dir = os.path.join(corpus.path, 'data')
|
def check_corpora(self):
|
||||||
shutil.rmtree(corpus_data_dir, ignore_errors=True)
|
corpora = Corpus.query.all()
|
||||||
os.mkdir(corpus_data_dir)
|
queued_corpora = list(filter(lambda corpus: corpus.status == 'queued', corpora))
|
||||||
corpus_registry_dir = os.path.join(corpus.path, 'registry')
|
running_corpora = list(filter(lambda corpus: corpus.status == 'running', corpora))
|
||||||
shutil.rmtree(corpus_registry_dir, ignore_errors=True)
|
start_analysis_corpora = list(filter(lambda corpus: corpus.status == 'start analysis', corpora))
|
||||||
os.mkdir(corpus_registry_dir)
|
stop_analysis_corpora = list(filter(lambda corpus: corpus.status == 'stop analysis', corpora))
|
||||||
corpus_file = os.path.join(corpus.path, 'merged', 'corpus.vrt')
|
submitted_corpora = list(filter(lambda corpus: corpus.status == 'submitted', corpora))
|
||||||
service_kwargs = {
|
for corpus in submitted_corpora:
|
||||||
'command': 'docker-entrypoint.sh build-corpus',
|
self.create_build_corpus_service(corpus)
|
||||||
'constraints': ['node.role==worker'],
|
for corpus in queued_corpora + running_corpora:
|
||||||
'labels': {'origin': 'nopaque',
|
self.checkout_build_corpus_service(corpus)
|
||||||
'type': 'corpus.build',
|
for corpus in start_analysis_corpora:
|
||||||
'corpus_id': str(corpus.id)},
|
self.create_cqpserver_container(corpus)
|
||||||
'mounts': [corpus_file + ':/root/files/corpus.vrt:ro',
|
for corpus in stop_analysis_corpora:
|
||||||
corpus_data_dir + ':/corpora/data:rw',
|
self.remove_cqpserver_container(corpus)
|
||||||
corpus_registry_dir + ':/usr/local/share/cwb/registry:rw'],
|
|
||||||
'name': 'build-corpus_{}'.format(corpus.id),
|
|
||||||
'restart_policy': docker.types.RestartPolicy()
|
|
||||||
}
|
|
||||||
service_image = \
|
|
||||||
'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest'
|
|
||||||
try:
|
|
||||||
docker_client.services.create(service_image, **service_kwargs)
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Create "{}" service raised '.format(service_kwargs['name'])
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
corpus.status = 'queued'
|
|
||||||
|
|
||||||
|
def create_build_corpus_service(self, corpus):
|
||||||
|
corpus_data_dir = os.path.join(corpus.path, 'data')
|
||||||
|
shutil.rmtree(corpus_data_dir, ignore_errors=True)
|
||||||
|
os.mkdir(corpus_data_dir)
|
||||||
|
corpus_registry_dir = os.path.join(corpus.path, 'registry')
|
||||||
|
shutil.rmtree(corpus_registry_dir, ignore_errors=True)
|
||||||
|
os.mkdir(corpus_registry_dir)
|
||||||
|
corpus_file = os.path.join(corpus.path, 'merged', 'corpus.vrt')
|
||||||
|
service_kwargs = {
|
||||||
|
'command': 'docker-entrypoint.sh build-corpus',
|
||||||
|
'constraints': ['node.role==worker'],
|
||||||
|
'labels': {'origin': 'nopaque',
|
||||||
|
'type': 'corpus.build',
|
||||||
|
'corpus_id': str(corpus.id)},
|
||||||
|
'mounts': [corpus_file + ':/root/files/corpus.vrt:ro',
|
||||||
|
corpus_data_dir + ':/corpora/data:rw',
|
||||||
|
corpus_registry_dir + ':/usr/local/share/cwb/registry:rw'],
|
||||||
|
'name': 'build-corpus_{}'.format(corpus.id),
|
||||||
|
'restart_policy': docker.types.RestartPolicy()
|
||||||
|
}
|
||||||
|
service_image = \
|
||||||
|
'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest'
|
||||||
|
try:
|
||||||
|
self.docker.services.create(service_image, **service_kwargs)
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Create "{}" service raised '.format(service_kwargs['name'])
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
corpus.status = 'queued'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
|
|
||||||
def checkout_build_corpus_service(corpus):
|
def checkout_build_corpus_service(self, corpus):
|
||||||
service_name = 'build-corpus_{}'.format(corpus.id)
|
service_name = 'build-corpus_{}'.format(corpus.id)
|
||||||
try:
|
try:
|
||||||
service = docker_client.services.get(service_name)
|
service = self.docker.services.get(service_name)
|
||||||
except docker.errors.NotFound:
|
except docker.errors.NotFound:
|
||||||
logging.error(
|
logging.error(
|
||||||
'Get "{}" service raised '.format(service_name)
|
'Get "{}" service raised '.format(service_name)
|
||||||
+ '"docker.errors.NotFound" The service does not exist. '
|
+ '"docker.errors.NotFound" The service does not exist. '
|
||||||
+ '(corpus.status: {} -> failed)'.format(corpus.status)
|
+ '(corpus.status: {} -> failed)'.format(corpus.status)
|
||||||
)
|
)
|
||||||
corpus.status = 'failed'
|
corpus.status = 'failed'
|
||||||
except docker.errors.APIError as e:
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
logging.error(
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
'Get "{}" service raised '.format(service_name)
|
except docker.errors.APIError as e:
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
logging.error(
|
||||||
+ 'Details: {}'.format(e)
|
'Get "{}" service raised '.format(service_name)
|
||||||
)
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
except docker.errors.InvalidVersion:
|
+ 'Details: {}'.format(e)
|
||||||
logging.error(
|
)
|
||||||
'Get "{}" service raised '.format(service_name)
|
except docker.errors.InvalidVersion:
|
||||||
+ '"docker.errors.InvalidVersion" One of the arguments is '
|
logging.error(
|
||||||
+ 'not supported with the current API version.'
|
'Get "{}" service raised '.format(service_name)
|
||||||
)
|
+ '"docker.errors.InvalidVersion" One of the arguments is '
|
||||||
else:
|
+ 'not supported with the current API version.'
|
||||||
service_tasks = service.tasks()
|
)
|
||||||
if not service_tasks:
|
else:
|
||||||
|
service_tasks = service.tasks()
|
||||||
|
if not service_tasks:
|
||||||
|
return
|
||||||
|
task_state = service_tasks[0].get('Status').get('State')
|
||||||
|
if corpus.status == 'queued' and task_state != 'pending':
|
||||||
|
corpus.status = 'running'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
|
elif (corpus.status == 'running'
|
||||||
|
and task_state in ['complete', 'failed']):
|
||||||
|
try:
|
||||||
|
service.remove()
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Remove "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
corpus.status = 'prepared' if task_state == 'complete' \
|
||||||
|
else 'failed'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
|
|
||||||
|
def create_cqpserver_container(self, corpus):
|
||||||
|
corpus_data_dir = os.path.join(corpus.path, 'data')
|
||||||
|
corpus_registry_dir = os.path.join(corpus.path, 'registry')
|
||||||
|
container_kwargs = {
|
||||||
|
'command': 'cqpserver',
|
||||||
|
'detach': True,
|
||||||
|
'volumes': [corpus_data_dir + ':/corpora/data:rw',
|
||||||
|
corpus_registry_dir + ':/usr/local/share/cwb/registry:rw'],
|
||||||
|
'name': 'cqpserver_{}'.format(corpus.id),
|
||||||
|
'network': 'nopaque_default'
|
||||||
|
}
|
||||||
|
container_image = \
|
||||||
|
'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest'
|
||||||
|
# Check if a cqpserver container already exists. If this is the case,
|
||||||
|
# remove it and create a new one
|
||||||
|
try:
|
||||||
|
container = self.docker.containers.get(container_kwargs['name'])
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
pass
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Get "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
task_state = service_tasks[0].get('Status').get('State')
|
else:
|
||||||
if corpus.status == 'queued' and task_state != 'pending':
|
|
||||||
corpus.status = 'running'
|
|
||||||
elif (corpus.status == 'running'
|
|
||||||
and task_state in ['complete', 'failed']):
|
|
||||||
try:
|
try:
|
||||||
service.remove()
|
container.remove(force=True)
|
||||||
except docker.errors.APIError as e:
|
except docker.errors.APIError as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
'Remove "{}" service raised '.format(service_name)
|
'Remove "{}" container raised '.format(container_kwargs['name'])
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
+ 'Details: {}'.format(e)
|
+ 'Details: {}'.format(e)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
corpus.status = 'prepared' if task_state == 'complete' \
|
|
||||||
else 'failed'
|
|
||||||
|
|
||||||
|
|
||||||
def create_cqpserver_container(corpus):
|
|
||||||
corpus_data_dir = os.path.join(corpus.path, 'data')
|
|
||||||
corpus_registry_dir = os.path.join(corpus.path, 'registry')
|
|
||||||
container_kwargs = {
|
|
||||||
'command': 'cqpserver',
|
|
||||||
'detach': True,
|
|
||||||
'volumes': [corpus_data_dir + ':/corpora/data:rw',
|
|
||||||
corpus_registry_dir + ':/usr/local/share/cwb/registry:rw'],
|
|
||||||
'name': 'cqpserver_{}'.format(corpus.id),
|
|
||||||
'network': 'nopaque_default'
|
|
||||||
}
|
|
||||||
container_image = \
|
|
||||||
'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest'
|
|
||||||
# Check if a cqpserver container already exists. If this is the case,
|
|
||||||
# remove it and create a new one
|
|
||||||
try:
|
|
||||||
container = docker_client.containers.get(container_kwargs['name'])
|
|
||||||
except docker.errors.NotFound:
|
|
||||||
pass
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Get "{}" container raised '.format(container_kwargs['name'])
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
container.remove(force=True)
|
self.docker.containers.run(container_image, **container_kwargs)
|
||||||
|
except docker.errors.ContainerError:
|
||||||
|
# This case should not occur, because detach is True.
|
||||||
|
logging.error(
|
||||||
|
'Run "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '"docker.errors.ContainerError" The container exits with a '
|
||||||
|
+ 'non-zero exit code and detach is False.'
|
||||||
|
)
|
||||||
|
corpus.status = 'failed'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
|
except docker.errors.ImageNotFound:
|
||||||
|
logging.error(
|
||||||
|
'Run "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '"docker.errors.ImageNotFound" The specified image does not '
|
||||||
|
+ 'exist.'
|
||||||
|
)
|
||||||
|
corpus.status = 'failed'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
except docker.errors.APIError as e:
|
except docker.errors.APIError as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
'Remove "{}" container raised '.format(container_kwargs['name']) # noqa
|
'Run "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
corpus.status = 'analysing'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
|
|
||||||
|
def remove_cqpserver_container(self, corpus):
|
||||||
|
container_name = 'cqpserver_{}'.format(corpus.id)
|
||||||
|
try:
|
||||||
|
container = self.docker.containers.get(container_name)
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
pass
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Get "{}" container raised '.format(container_name)
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
+ 'Details: {}'.format(e)
|
+ 'Details: {}'.format(e)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
try:
|
else:
|
||||||
docker_client.containers.run(container_image, **container_kwargs)
|
try:
|
||||||
except docker.errors.ContainerError:
|
container.remove(force=True)
|
||||||
# This case should not occur, because detach is True.
|
except docker.errors.APIError as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
'Run "{}" container raised '.format(container_kwargs['name'])
|
'Remove "{}" container raised '.format(container_name)
|
||||||
+ '"docker.errors.ContainerError" The container exits with a '
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
+ 'non-zero exit code and detach is False.'
|
+ 'Details: {}'.format(e)
|
||||||
)
|
)
|
||||||
corpus.status = 'failed'
|
return
|
||||||
except docker.errors.ImageNotFound:
|
corpus.status = 'prepared'
|
||||||
logging.error(
|
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
|
||||||
'Run "{}" container raised '.format(container_kwargs['name'])
|
self.buffer_user_patch_operation(corpus, patch_operation)
|
||||||
+ '"docker.errors.ImageNotFound" The specified image does not '
|
|
||||||
+ 'exist.'
|
|
||||||
)
|
|
||||||
corpus.status = 'failed'
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Run "{}" container raised '.format(container_kwargs['name'])
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
corpus.status = 'analysing'
|
|
||||||
|
|
||||||
|
|
||||||
def remove_cqpserver_container(corpus):
|
|
||||||
container_name = 'cqpserver_{}'.format(corpus.id)
|
|
||||||
try:
|
|
||||||
container = docker_client.containers.get(container_name)
|
|
||||||
except docker.errors.NotFound:
|
|
||||||
pass
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Get "{}" container raised '.format(container_name)
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
container.remove(force=True)
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Remove "{}" container raised '.format(container_name)
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
corpus.status = 'prepared'
|
|
||||||
|
@ -1,82 +1,161 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from . import docker_client
|
|
||||||
from .. import db, mail
|
from .. import db, mail
|
||||||
from ..email import create_message
|
from ..email import create_message
|
||||||
from ..models import JobResult
|
from ..models import Job, JobResult
|
||||||
import docker
|
import docker
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def create_job_service(job):
|
class CheckJobsMixin:
|
||||||
cmd = '{} -i /files -o /files/output'.format(job.service)
|
def check_jobs(self):
|
||||||
if job.service == 'file-setup':
|
jobs = Job.query.all()
|
||||||
cmd += ' -f {}'.format(secure_filename(job.title))
|
canceling_jobs = list(filter(lambda job: job.status == 'canceling', jobs))
|
||||||
cmd += ' --log-dir /files'
|
queued_jobs = list(filter(lambda job: job.status == 'queued', jobs))
|
||||||
cmd += ' --zip [{}]_{}'.format(job.service, secure_filename(job.title))
|
running_jobs = list(filter(lambda job: job.status == 'running', jobs))
|
||||||
cmd += ' ' + ' '.join(json.loads(job.service_args))
|
submitted_jobs = list(filter(lambda job: job.status == 'submitted', jobs))
|
||||||
service_kwargs = {'command': cmd,
|
for job in submitted_jobs:
|
||||||
'constraints': ['node.role==worker'],
|
self.create_job_service(job)
|
||||||
'labels': {'origin': 'nopaque',
|
for job in queued_jobs + running_jobs:
|
||||||
'type': 'service.{}'.format(job.service),
|
self.checkout_job_service(job)
|
||||||
'job_id': str(job.id)},
|
for job in canceling_jobs:
|
||||||
'mounts': [job.path + ':/files:rw'],
|
self.remove_job_service(job)
|
||||||
'name': 'job_{}'.format(job.id),
|
|
||||||
'resources': docker.types.Resources(
|
|
||||||
cpu_reservation=job.n_cores * (10 ** 9),
|
|
||||||
mem_reservation=job.mem_mb * (10 ** 6)
|
|
||||||
),
|
|
||||||
'restart_policy': docker.types.RestartPolicy()}
|
|
||||||
service_image = 'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/{}:{}'.format(
|
|
||||||
job.service, job.service_version)
|
|
||||||
try:
|
|
||||||
docker_client.services.create(service_image, **service_kwargs)
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Create "{}" service raised '.format(service_kwargs['name'])
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
job.status = 'queued'
|
|
||||||
finally:
|
|
||||||
send_notification(job)
|
|
||||||
|
|
||||||
|
def create_job_service(self, job):
|
||||||
def checkout_job_service(job):
|
cmd = '{} -i /files -o /files/output'.format(job.service)
|
||||||
service_name = 'job_{}'.format(job.id)
|
if job.service == 'file-setup':
|
||||||
try:
|
cmd += ' -f {}'.format(secure_filename(job.title))
|
||||||
service = docker_client.services.get(service_name)
|
cmd += ' --log-dir /files'
|
||||||
except docker.errors.NotFound:
|
cmd += ' --zip [{}]_{}'.format(job.service, secure_filename(job.title))
|
||||||
logging.error('Get "{}" service raised '.format(service_name)
|
cmd += ' ' + ' '.join(json.loads(job.service_args))
|
||||||
+ '"docker.errors.NotFound" The service does not exist. '
|
service_kwargs = {'command': cmd,
|
||||||
+ '(job.status: {} -> failed)'.format(job.status))
|
'constraints': ['node.role==worker'],
|
||||||
job.status = 'failed'
|
'labels': {'origin': 'nopaque',
|
||||||
except docker.errors.APIError as e:
|
'type': 'service.{}'.format(job.service),
|
||||||
logging.error(
|
'job_id': str(job.id)},
|
||||||
'Get "{}" service raised '.format(service_name)
|
'mounts': [job.path + ':/files:rw'],
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
'name': 'job_{}'.format(job.id),
|
||||||
+ 'Details: {}'.format(e)
|
'resources': docker.types.Resources(
|
||||||
)
|
cpu_reservation=job.n_cores * (10 ** 9),
|
||||||
return
|
mem_reservation=job.mem_mb * (10 ** 6)
|
||||||
except docker.errors.InvalidVersion:
|
),
|
||||||
logging.error(
|
'restart_policy': docker.types.RestartPolicy()}
|
||||||
'Get "{}" service raised '.format(service_name)
|
service_image = 'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/{}:{}'.format(job.service, job.service_version)
|
||||||
+ '"docker.errors.InvalidVersion" One of the arguments is '
|
try:
|
||||||
+ 'not supported with the current API version.'
|
self.docker.services.create(service_image, **service_kwargs)
|
||||||
)
|
except docker.errors.APIError as e:
|
||||||
return
|
logging.error(
|
||||||
else:
|
'Create "{}" service raised '.format(service_kwargs['name'])
|
||||||
service_tasks = service.tasks()
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
if not service_tasks:
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
task_state = service_tasks[0].get('Status').get('State')
|
else:
|
||||||
if job.status == 'queued' and task_state != 'pending':
|
job.status = 'queued'
|
||||||
job.status = 'running'
|
patch_operation = {'op': 'replace', 'path': '/jobs/{}/status'.format(job.id), 'value': job.status}
|
||||||
elif job.status == 'running' and task_state in ['complete', 'failed']:
|
self.buffer_user_patch_operation(job, patch_operation)
|
||||||
|
finally:
|
||||||
|
self.send_job_notification(job)
|
||||||
|
|
||||||
|
def checkout_job_service(self, job):
|
||||||
|
service_name = 'job_{}'.format(job.id)
|
||||||
|
try:
|
||||||
|
service = self.docker.services.get(service_name)
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
logging.error('Get "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.NotFound" The service does not exist. '
|
||||||
|
+ '(job.status: {} -> failed)'.format(job.status))
|
||||||
|
job.status = 'failed'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/jobs/{}/status'.format(job.id), 'value': job.status}
|
||||||
|
self.buffer_user_patch_operation(job, patch_operation)
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Get "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except docker.errors.InvalidVersion:
|
||||||
|
logging.error(
|
||||||
|
'Get "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.InvalidVersion" One of the arguments is '
|
||||||
|
+ 'not supported with the current API version.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
service_tasks = service.tasks()
|
||||||
|
if not service_tasks:
|
||||||
|
return
|
||||||
|
task_state = service_tasks[0].get('Status').get('State')
|
||||||
|
if job.status == 'queued' and task_state != 'pending':
|
||||||
|
job.status = 'running'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/jobs/{}/status'.format(job.id), 'value': job.status}
|
||||||
|
self.buffer_user_patch_operation(job, patch_operation)
|
||||||
|
elif job.status == 'running' and task_state in ['complete', 'failed']:
|
||||||
|
try:
|
||||||
|
service.remove()
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Remove "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if task_state == 'complete':
|
||||||
|
results_dir = os.path.join(job.path, 'output')
|
||||||
|
result_files = filter(lambda x: x.endswith('.zip'),
|
||||||
|
os.listdir(results_dir))
|
||||||
|
for result_file in result_files:
|
||||||
|
job_result = JobResult(filename=result_file, job=job)
|
||||||
|
db.session.add(job_result)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.refresh(job_result)
|
||||||
|
patch_operation = {'op': 'add', 'path': '/jobs/{}/results/{}'.format(job.id, job_result.id), 'value': job_result.to_dict()}
|
||||||
|
self.buffer_user_patch_operation(job, patch_operation)
|
||||||
|
job.end_date = datetime.utcnow()
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/jobs/{}/end_date'.format(job.id), 'value': job.end_date.timestamp()}
|
||||||
|
self.buffer_user_patch_operation(job, patch_operation)
|
||||||
|
job.status = task_state
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/jobs/{}/status'.format(job.id), 'value': job.status}
|
||||||
|
self.buffer_user_patch_operation(job, patch_operation)
|
||||||
|
finally:
|
||||||
|
self.send_job_notification(job)
|
||||||
|
|
||||||
|
def remove_job_service(self, job):
|
||||||
|
service_name = 'job_{}'.format(job.id)
|
||||||
|
try:
|
||||||
|
service = self.docker.services.get(service_name)
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
job.status = 'canceled'
|
||||||
|
patch_operation = {'op': 'replace', 'path': '/jobs/{}/status'.format(job.id), 'value': job.status}
|
||||||
|
self.buffer_user_patch_operation(job, patch_operation)
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Get "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except docker.errors.InvalidVersion:
|
||||||
|
logging.error(
|
||||||
|
'Get "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.InvalidVersion" One of the arguments is '
|
||||||
|
+ 'not supported with the current API version.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
service.update(mounts=None)
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error(
|
||||||
|
'Update "{}" service raised '.format(service_name)
|
||||||
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e)
|
||||||
|
)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
service.remove()
|
service.remove()
|
||||||
except docker.errors.APIError as e:
|
except docker.errors.APIError as e:
|
||||||
@ -85,68 +164,14 @@ def checkout_job_service(job):
|
|||||||
+ '"docker.errors.APIError" The server returned an error. '
|
+ '"docker.errors.APIError" The server returned an error. '
|
||||||
+ 'Details: {}'.format(e)
|
+ 'Details: {}'.format(e)
|
||||||
)
|
)
|
||||||
return
|
|
||||||
else:
|
|
||||||
if task_state == 'complete':
|
|
||||||
job_results_dir = os.path.join(job.path, 'output')
|
|
||||||
job_results = filter(lambda x: x.endswith('.zip'),
|
|
||||||
os.listdir(job_results_dir))
|
|
||||||
for job_result in job_results:
|
|
||||||
job_result = JobResult(filename=job_result, job=job)
|
|
||||||
db.session.add(job_result)
|
|
||||||
job.end_date = datetime.utcnow()
|
|
||||||
job.status = task_state
|
|
||||||
finally:
|
|
||||||
send_notification(job)
|
|
||||||
|
|
||||||
|
def send_job_notification(self, job):
|
||||||
def remove_job_service(job):
|
if job.creator.setting_job_status_mail_notifications == 'none':
|
||||||
service_name = 'job_{}'.format(job.id)
|
|
||||||
try:
|
|
||||||
service = docker_client.services.get(service_name)
|
|
||||||
except docker.errors.NotFound:
|
|
||||||
job.status = 'canceled'
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Get "{}" service raised '.format(service_name)
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except docker.errors.InvalidVersion:
|
|
||||||
logging.error(
|
|
||||||
'Get "{}" service raised '.format(service_name)
|
|
||||||
+ '"docker.errors.InvalidVersion" One of the arguments is '
|
|
||||||
+ 'not supported with the current API version.'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
service.update(mounts=None)
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logging.error(
|
|
||||||
'Update "{}" service raised '.format(service_name)
|
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
|
||||||
+ 'Details: {}'.format(e)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
try:
|
if (job.creator.setting_job_status_mail_notifications == 'end'
|
||||||
service.remove()
|
and job.status not in ['complete', 'failed']):
|
||||||
except docker.errors.APIError as e:
|
return
|
||||||
logging.error(
|
msg = create_message(job.creator.email,
|
||||||
'Remove "{}" service raised '.format(service_name)
|
'Status update for your Job "{}"'.format(job.title),
|
||||||
+ '"docker.errors.APIError" The server returned an error. '
|
'tasks/email/notification', job=job)
|
||||||
+ 'Details: {}'.format(e)
|
mail.send(msg)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_notification(job):
|
|
||||||
if job.creator.setting_job_status_mail_notifications == 'none':
|
|
||||||
return
|
|
||||||
if (job.creator.setting_job_status_mail_notifications == 'end'
|
|
||||||
and job.status not in ['complete', 'failed']):
|
|
||||||
return
|
|
||||||
msg = create_message(job.creator.email,
|
|
||||||
'Status update for your Job "{}"'.format(job.title),
|
|
||||||
'tasks/email/notification', job=job)
|
|
||||||
mail.send(msg)
|
|
||||||
|
@ -50,9 +50,8 @@ def deploy():
|
|||||||
|
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
def tasks():
|
def tasks():
|
||||||
from app.tasks import check_corpora, check_jobs
|
from app.tasks import task_runner
|
||||||
check_corpora()
|
task_runner.run()
|
||||||
check_jobs()
|
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
|
Loading…
Reference in New Issue
Block a user