Remove user session loop. Instead send ressource updates directly on change

This commit is contained in:
Patrick Jentsch 2021-02-01 12:51:07 +01:00
parent ee9fdd1017
commit 996ed1c790
11 changed files with 513 additions and 379 deletions

View File

@ -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':

View File

@ -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)

View File

@ -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,15 +29,21 @@ 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)
else:
db.session.commit()
flash('Corpus "{}" added!'.format(corpus.title), 'corpus') 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 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')

View 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):
return
socketio.start_background_task(user_session,
current_app._get_current_object(),
user_id, request.sid)
def user_session(app, user_id, session_id):
'''
' Sends initial user data to the client. Afterwards it checks every 3s if
' changes to the initial values appeared. If changes are detected, a
' RFC 6902 compliant JSON patch gets send.
'''
init_event = 'user_{}_init'.format(user_id)
patch_event = 'user_{}_patch'.format(user_id)
with app.app_context():
# Gather current values from database.
user = User.query.get(user_id) user = User.query.get(user_id)
user_dict = user.to_dict() if user is None:
# Send initial values to the client. response = {'code': 404, 'msg': 'Not found'}
socketio.emit(init_event, json.dumps(user_dict), room=session_id) socketio.emit('start_user_session', response, room=request.sid)
while session_id in connected_sessions: elif not (user == current_user or current_user.is_administrator):
# Get new values from the database response = {'code': 403, 'msg': 'Forbidden'}
db.session.refresh(user) socketio.emit('start_user_session', response, room=request.sid)
new_user_dict = user.to_dict() else:
# Compute JSON patches. response = {'code': 200, 'msg': 'OK'}
user_patch = jsonpatch.JsonPatch.from_diff(user_dict, socketio.emit('start_user_session', response, room=request.sid)
new_user_dict) socketio.emit('user_{}_init'.format(user.id), user.to_dict(),
# In case there are patches, send them to the client. room=request.sid)
if user_patch: room = 'user_{}'.format(user.id)
socketio.emit(patch_event, user_patch.to_string(), join_room(room)
room=session_id)
# Set new values as references for the next iteration.
user_dict = new_user_dict @socketio.on('stop_user_session')
socketio.sleep(3) @socketio_login_required
def socketio_stop_user_session(user_id):
user = User.query.get(user_id)
if user is None:
response = {'code': 404, 'msg': 'Not found'}
socketio.emit('stop_user_session', response, room=request.sid)
elif not (user == current_user or current_user.is_administrator):
response = {'code': 403, 'msg': 'Forbidden'}
socketio.emit('stop_user_session', response, room=request.sid)
else:
response = {'code': 200, 'msg': 'OK'}
socketio.emit('stop_user_session', response, room=request.sid)
room = 'user_{}'.format(user.id)
leave_room(room)

View File

@ -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))
try:
job.restart() job.restart()
except Exception:
pass
else:
db.session.commit() 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)

View File

@ -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),

View File

@ -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"));

View File

@ -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):
def check_corpora(): self.check_corpora()
corpora = Corpus.query.all() self.check_jobs()
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() 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_jobs(): task_runner = TaskRunner()
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()

View File

@ -1,11 +1,28 @@
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:
def check_corpora(self):
corpora = Corpus.query.all()
queued_corpora = list(filter(lambda corpus: corpus.status == 'queued', corpora))
running_corpora = list(filter(lambda corpus: corpus.status == 'running', corpora))
start_analysis_corpora = list(filter(lambda corpus: corpus.status == 'start analysis', corpora))
stop_analysis_corpora = list(filter(lambda corpus: corpus.status == 'stop analysis', corpora))
submitted_corpora = list(filter(lambda corpus: corpus.status == 'submitted', corpora))
for corpus in submitted_corpora:
self.create_build_corpus_service(corpus)
for corpus in queued_corpora + running_corpora:
self.checkout_build_corpus_service(corpus)
for corpus in start_analysis_corpora:
self.create_cqpserver_container(corpus)
for corpus in stop_analysis_corpora:
self.remove_cqpserver_container(corpus)
def create_build_corpus_service(self, corpus):
corpus_data_dir = os.path.join(corpus.path, 'data') corpus_data_dir = os.path.join(corpus.path, 'data')
shutil.rmtree(corpus_data_dir, ignore_errors=True) shutil.rmtree(corpus_data_dir, ignore_errors=True)
os.mkdir(corpus_data_dir) os.mkdir(corpus_data_dir)
@ -28,7 +45,7 @@ def create_build_corpus_service(corpus):
service_image = \ service_image = \
'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest' 'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest'
try: try:
docker_client.services.create(service_image, **service_kwargs) self.docker.services.create(service_image, **service_kwargs)
except docker.errors.APIError as e: except docker.errors.APIError as e:
logging.error( logging.error(
'Create "{}" service raised '.format(service_kwargs['name']) 'Create "{}" service raised '.format(service_kwargs['name'])
@ -37,12 +54,13 @@ def create_build_corpus_service(corpus):
) )
else: else:
corpus.status = 'queued' 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(self, corpus):
def checkout_build_corpus_service(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)
@ -50,6 +68,8 @@ def checkout_build_corpus_service(corpus):
+ '(corpus.status: {} -> failed)'.format(corpus.status) + '(corpus.status: {} -> failed)'.format(corpus.status)
) )
corpus.status = 'failed' 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(
'Get "{}" service raised '.format(service_name) 'Get "{}" service raised '.format(service_name)
@ -69,6 +89,8 @@ def checkout_build_corpus_service(corpus):
task_state = service_tasks[0].get('Status').get('State') task_state = service_tasks[0].get('Status').get('State')
if corpus.status == 'queued' and task_state != 'pending': if corpus.status == 'queued' and task_state != 'pending':
corpus.status = 'running' 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' elif (corpus.status == 'running'
and task_state in ['complete', 'failed']): and task_state in ['complete', 'failed']):
try: try:
@ -83,9 +105,10 @@ def checkout_build_corpus_service(corpus):
else: else:
corpus.status = 'prepared' if task_state == 'complete' \ corpus.status = 'prepared' if task_state == 'complete' \
else 'failed' 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):
def create_cqpserver_container(corpus):
corpus_data_dir = os.path.join(corpus.path, 'data') corpus_data_dir = os.path.join(corpus.path, 'data')
corpus_registry_dir = os.path.join(corpus.path, 'registry') corpus_registry_dir = os.path.join(corpus.path, 'registry')
container_kwargs = { container_kwargs = {
@ -101,7 +124,7 @@ def create_cqpserver_container(corpus):
# Check if a cqpserver container already exists. If this is the case, # Check if a cqpserver container already exists. If this is the case,
# remove it and create a new one # remove it and create a new one
try: try:
container = docker_client.containers.get(container_kwargs['name']) container = self.docker.containers.get(container_kwargs['name'])
except docker.errors.NotFound: except docker.errors.NotFound:
pass pass
except docker.errors.APIError as e: except docker.errors.APIError as e:
@ -116,13 +139,13 @@ def create_cqpserver_container(corpus):
container.remove(force=True) container.remove(force=True)
except docker.errors.APIError as e: except docker.errors.APIError as e:
logging.error( logging.error(
'Remove "{}" container raised '.format(container_kwargs['name']) # noqa '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
try: try:
docker_client.containers.run(container_image, **container_kwargs) self.docker.containers.run(container_image, **container_kwargs)
except docker.errors.ContainerError: except docker.errors.ContainerError:
# This case should not occur, because detach is True. # This case should not occur, because detach is True.
logging.error( logging.error(
@ -131,6 +154,8 @@ def create_cqpserver_container(corpus):
+ 'non-zero exit code and detach is False.' + 'non-zero exit code and detach is False.'
) )
corpus.status = 'failed' 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: except docker.errors.ImageNotFound:
logging.error( logging.error(
'Run "{}" container raised '.format(container_kwargs['name']) 'Run "{}" container raised '.format(container_kwargs['name'])
@ -138,6 +163,8 @@ def create_cqpserver_container(corpus):
+ 'exist.' + 'exist.'
) )
corpus.status = 'failed' 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(
'Run "{}" container raised '.format(container_kwargs['name']) 'Run "{}" container raised '.format(container_kwargs['name'])
@ -146,12 +173,13 @@ def create_cqpserver_container(corpus):
) )
else: else:
corpus.status = 'analysing' 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):
def remove_cqpserver_container(corpus):
container_name = 'cqpserver_{}'.format(corpus.id) container_name = 'cqpserver_{}'.format(corpus.id)
try: try:
container = docker_client.containers.get(container_name) container = self.docker.containers.get(container_name)
except docker.errors.NotFound: except docker.errors.NotFound:
pass pass
except docker.errors.APIError as e: except docker.errors.APIError as e:
@ -172,3 +200,5 @@ def remove_cqpserver_container(corpus):
) )
return return
corpus.status = 'prepared' corpus.status = 'prepared'
patch_operation = {'op': 'replace', 'path': '/corpora/{}/status'.format(corpus.id), 'value': corpus.status}
self.buffer_user_patch_operation(corpus, patch_operation)

View File

@ -1,16 +1,29 @@
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:
def check_jobs(self):
jobs = Job.query.all()
canceling_jobs = list(filter(lambda job: job.status == 'canceling', jobs))
queued_jobs = list(filter(lambda job: job.status == 'queued', jobs))
running_jobs = list(filter(lambda job: job.status == 'running', jobs))
submitted_jobs = list(filter(lambda job: job.status == 'submitted', jobs))
for job in submitted_jobs:
self.create_job_service(job)
for job in queued_jobs + running_jobs:
self.checkout_job_service(job)
for job in canceling_jobs:
self.remove_job_service(job)
def create_job_service(self, job):
cmd = '{} -i /files -o /files/output'.format(job.service) cmd = '{} -i /files -o /files/output'.format(job.service)
if job.service == 'file-setup': if job.service == 'file-setup':
cmd += ' -f {}'.format(secure_filename(job.title)) cmd += ' -f {}'.format(secure_filename(job.title))
@ -29,10 +42,9 @@ def create_job_service(job):
mem_reservation=job.mem_mb * (10 ** 6) mem_reservation=job.mem_mb * (10 ** 6)
), ),
'restart_policy': docker.types.RestartPolicy()} 'restart_policy': docker.types.RestartPolicy()}
service_image = 'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/{}:{}'.format( service_image = 'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/{}:{}'.format(job.service, job.service_version)
job.service, job.service_version)
try: try:
docker_client.services.create(service_image, **service_kwargs) self.docker.services.create(service_image, **service_kwargs)
except docker.errors.APIError as e: except docker.errors.APIError as e:
logging.error( logging.error(
'Create "{}" service raised '.format(service_kwargs['name']) 'Create "{}" service raised '.format(service_kwargs['name'])
@ -42,19 +54,22 @@ def create_job_service(job):
return return
else: else:
job.status = 'queued' job.status = 'queued'
patch_operation = {'op': 'replace', 'path': '/jobs/{}/status'.format(job.id), 'value': job.status}
self.buffer_user_patch_operation(job, patch_operation)
finally: finally:
send_notification(job) self.send_job_notification(job)
def checkout_job_service(self, job):
def checkout_job_service(job):
service_name = 'job_{}'.format(job.id) service_name = 'job_{}'.format(job.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('Get "{}" service raised '.format(service_name) logging.error('Get "{}" service raised '.format(service_name)
+ '"docker.errors.NotFound" The service does not exist. ' + '"docker.errors.NotFound" The service does not exist. '
+ '(job.status: {} -> failed)'.format(job.status)) + '(job.status: {} -> failed)'.format(job.status))
job.status = 'failed' 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: except docker.errors.APIError as e:
logging.error( logging.error(
'Get "{}" service raised '.format(service_name) 'Get "{}" service raised '.format(service_name)
@ -76,6 +91,8 @@ def checkout_job_service(job):
task_state = service_tasks[0].get('Status').get('State') task_state = service_tasks[0].get('Status').get('State')
if job.status == 'queued' and task_state != 'pending': if job.status == 'queued' and task_state != 'pending':
job.status = 'running' 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']: elif job.status == 'running' and task_state in ['complete', 'failed']:
try: try:
service.remove() service.remove()
@ -88,24 +105,33 @@ def checkout_job_service(job):
return return
else: else:
if task_state == 'complete': if task_state == 'complete':
job_results_dir = os.path.join(job.path, 'output') results_dir = os.path.join(job.path, 'output')
job_results = filter(lambda x: x.endswith('.zip'), result_files = filter(lambda x: x.endswith('.zip'),
os.listdir(job_results_dir)) os.listdir(results_dir))
for job_result in job_results: for result_file in result_files:
job_result = JobResult(filename=job_result, job=job) job_result = JobResult(filename=result_file, job=job)
db.session.add(job_result) 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() 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 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: finally:
send_notification(job) self.send_job_notification(job)
def remove_job_service(self, job):
def remove_job_service(job):
service_name = 'job_{}'.format(job.id) service_name = 'job_{}'.format(job.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:
job.status = 'canceled' 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: except docker.errors.APIError as e:
logging.error( logging.error(
'Get "{}" service raised '.format(service_name) 'Get "{}" service raised '.format(service_name)
@ -139,8 +165,7 @@ def remove_job_service(job):
+ 'Details: {}'.format(e) + 'Details: {}'.format(e)
) )
def send_job_notification(self, job):
def send_notification(job):
if job.creator.setting_job_status_mail_notifications == 'none': if job.creator.setting_job_status_mail_notifications == 'none':
return return
if (job.creator.setting_job_status_mail_notifications == 'end' if (job.creator.setting_job_status_mail_notifications == 'end'

View File

@ -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()