From 2f8d4e90f6fc2579e2a419c01f29c315f45d972a Mon Sep 17 00:00:00 2001 From: Markus Scheidgen <markus.scheidgen@gmail.com> Date: Sat, 2 Feb 2019 15:54:07 +0100 Subject: [PATCH] Fixed issues about failed uploads/calcs. Added chasos parser. --- gui/src/components/ArchiveLogView.js | 4 +-- gui/src/components/Upload.js | 37 +++++++++++++++++----- gui/src/components/Uploads.js | 7 +++-- nomad/config.py | 5 ++- nomad/parsing/__init__.py | 3 +- nomad/parsing/artificial.py | 44 ++++++++++++++++++++++++++ nomad/processing/data.py | 46 +++++++++++++++------------- tests/conftest.py | 12 +++++++- 8 files changed, 120 insertions(+), 38 deletions(-) diff --git a/gui/src/components/ArchiveLogView.js b/gui/src/components/ArchiveLogView.js index 9132437ee0..11404c2857 100644 --- a/gui/src/components/ArchiveLogView.js +++ b/gui/src/components/ArchiveLogView.js @@ -61,8 +61,8 @@ class ArchiveLogView extends React.Component { <DownloadIcon /> </Download> { - data - ? <pre>{data}</pre> + data !== null + ? <pre>{data || 'empty log'}</pre> : <LinearProgress variant="query" /> } </div> diff --git a/gui/src/components/Upload.js b/gui/src/components/Upload.js index fedca63335..cc514c6cd8 100644 --- a/gui/src/components/Upload.js +++ b/gui/src/components/Upload.js @@ -12,6 +12,7 @@ import { compose } from 'recompose' import { withErrors } from './errors' import { debug } from '../config' import CalcDialog from './CalcDialog' +import ArchiveLogDialog from './ArchiveLogDialog' class Upload extends React.Component { static propTypes = { @@ -187,7 +188,7 @@ class Upload extends React.Component { renderStepper() { const { classes } = this.props const { upload } = this.state - const { calcs, tasks, current_task, tasks_running, tasks_status, process_running, current_process, errors } = upload + const { calcs, tasks, current_task, tasks_running, tasks_status, process_running, current_process } = upload // map tasks [ uploading, extracting, parse_all, cleanup ] to steps const steps = [ 'upload', 'process', 'publish' ] @@ -273,7 +274,7 @@ class Upload extends React.Component { if (tasks_status === 'FAILURE') { props.optional = ( <Typography variant="caption" color="error"> - {errors.join(' ')} + processing failed </Typography> ) } @@ -353,7 +354,7 @@ class Upload extends React.Component { const processed = tasks_status === 'FAILURE' || tasks_status === 'SUCCESS' const row = ( <TableRow key={index} hover={processed} - onClick={() => this.setState({openCalc: processed ? {uploadId: upload_id, calcId: calc_id} : null})} + onClick={() => this.setState({openCalc: processed ? calc : null})} className={processed ? classes.clickableRow : null} > <TableCell> @@ -387,8 +388,9 @@ class Upload extends React.Component { ) if (tasks_status === 'FAILURE') { + const error_html = `Calculation processing failed with errors: ${errors.join(', ')}` return ( - <Tooltip key={calc_id} title={errors.map((error, index) => (<p key={`${calc_id}-${index}`}>{error}</p>))}> + <Tooltip key={calc_id} title={error_html}> {row} </Tooltip> ) @@ -455,14 +457,30 @@ class Upload extends React.Component { ) } + renderOpenCalc() { + const { openCalc } = this.state + if (openCalc) { + if (openCalc.errors && openCalc.errors.length > 0) { + return <ArchiveLogDialog calcId={openCalc.calc_id} uploadId={openCalc.upload_id} + onClose={() => this.setState({openCalc: null})} /> + } else { + return <CalcDialog calcId={openCalc.calc_id} uploadId={openCalc.upload_id} + onClose={() => this.setState({openCalc: null})} /> + } + } + + return '' + } + render() { - const { classes, raiseError } = this.props - const { upload, openCalc } = this.state + const { classes } = this.props + const { upload } = this.state + const { errors } = upload if (this.state.upload) { return ( <div className={classes.root}> - { openCalc ? <CalcDialog raiseError={raiseError} {...openCalc} onClose={() => this.setState({openCalc: null})} /> : ''} + { this.renderOpenCalc() } <ExpansionPanel> <ExpansionPanelSummary @@ -483,6 +501,11 @@ class Upload extends React.Component { {this.renderTitle()} {this.renderStepper()} </ExpansionPanelSummary> <ExpansionPanelDetails style={{width: '100%'}} classes={{root: classes.details}}> + {errors && errors.length > 0 + ? <Typography className={classes.detailsContent} color="error"> + Upload processing has errors: {errors.join(', ')} + </Typography> : '' + } {upload.calcs ? this.renderCalcTable() : ''} {debug ? <div className={classes.detailsContent}> diff --git a/gui/src/components/Uploads.js b/gui/src/components/Uploads.js index a3ebc24d67..ac1489ccb4 100644 --- a/gui/src/components/Uploads.js +++ b/gui/src/components/Uploads.js @@ -128,9 +128,12 @@ class Uploads extends React.Component { }) } - sortedUploads() { + sortedUploads(order) { + order = order || -1 return this.state.uploads.concat() - .sort((a, b) => (a.gui_upload_id === b.gui_upload_id) ? 0 : ((a.gui_upload_id < b.gui_upload_id) ? -1 : 1)) + .sort((a, b) => (a.gui_upload_id === b.gui_upload_id) + ? 0 + : ((a.gui_upload_id < b.gui_upload_id) ? -1 : 1) * order) } handleDoesNotExist(nonExistingUupload) { diff --git a/nomad/config.py b/nomad/config.py index 1451776950..27df4eabd7 100644 --- a/nomad/config.py +++ b/nomad/config.py @@ -51,7 +51,7 @@ LogstashConfig = namedtuple('LogstashConfig', ['enabled', 'host', 'tcp_port', 'l NomadServicesConfig = namedtuple('NomadServicesConfig', ['api_host', 'api_port', 'api_base_path', 'api_secret', 'admin_password', 'upload_url', 'disable_reset']) """ Used to configure nomad services: worker, handler, api """ -MailConfig = namedtuple('MailConfig', ['enabled', 'host', 'port', 'user', 'password', 'from_address']) +MailConfig = namedtuple('MailConfig', ['host', 'port', 'user', 'password', 'from_address']) """ Used to configure how nomad can send email """ files = FilesConfig( @@ -129,8 +129,7 @@ migration_source_db = RepositoryDBConfig( password=os.environ.get('NOMAD_MIGRATION_SOURCE_PASSWORD', '*') ) mail = MailConfig( - enabled=True, - host=os.environ.get('NOMAD_SMTP_HOST', 'localhost'), + host=os.environ.get('NOMAD_SMTP_HOST', ''), # empty or None host disables email port=int(os.environ.get('NOMAD_SMTP_PORT', 8995)), user=os.environ.get('NOMAD_SMTP_USER', None), password=os.environ.get('NOMAD_SMTP_PASSWORD', None), diff --git a/nomad/parsing/__init__.py b/nomad/parsing/__init__.py index 6c1915c6f4..826dbda17b 100644 --- a/nomad/parsing/__init__.py +++ b/nomad/parsing/__init__.py @@ -58,12 +58,13 @@ based on NOMAD-coe's *python-common* module. from nomad.parsing.backend import AbstractParserBackend, LocalBackend, LegacyLocalBackend, JSONStreamWriter, BadContextURI, WrongContextState from nomad.parsing.parser import Parser, LegacyParser -from nomad.parsing.artificial import TemplateParser, GenerateRandomParser +from nomad.parsing.artificial import TemplateParser, GenerateRandomParser, ChaosParser from nomad.dependencies import dependencies_dict as dependencies parsers = [ GenerateRandomParser(), TemplateParser(), + ChaosParser(), LegacyParser( python_git=dependencies['parsers/vasp'], parser_class_name='vaspparser.VASPRunParserInterface', diff --git a/nomad/parsing/artificial.py b/nomad/parsing/artificial.py index a74ebe4e50..d874940a3e 100644 --- a/nomad/parsing/artificial.py +++ b/nomad/parsing/artificial.py @@ -23,6 +23,8 @@ import numpy as np import random from ase.data import chemical_symbols import numpy +import sys +import time from nomadcore.local_meta_info import loadJsonFile, InfoKindEl import nomad_meta_info @@ -111,6 +113,48 @@ class TemplateParser(ArtificalParser): return self.backend +class ChaosParser(ArtificalParser): + """ + Parser that emulates typical error situations. Files can contain a json string (or + object with key `chaos`) with one of the following string values: + - exit + - deadlock + - consume_ram + - exception + - random + """ + name = 'parsers/chaos' + + def is_mainfile(self, filename: str, open: Callable[[str], IO[Any]]) -> bool: + return filename.endswith('chaos.json') + + def run(self, mainfile: str, logger=None) -> LocalBackend: + self.init_backend() + + chaos_json = json.load(open(mainfile, 'r')) + if isinstance(chaos_json, str): + chaos = chaos_json + elif isinstance(chaos_json, dict): + chaos = chaos_json.get('chaos', None) + else: + chaos = None + + if chaos == 'random': + chaos = random.choice(['exit', 'deadlock', 'consume_ram', 'exception']) + + if chaos == 'exit': + sys.exit(1) + elif chaos == 'deadlock': + while True: + time.sleep(1) + elif chaos == 'consume_ram': + pass + elif chaos == 'exception': + raise Exception('Some chaos happened, muhuha...') + + raise Exception('Unknown chaos') + + class GenerateRandomParser(TemplateParser): name = 'parsers/random' diff --git a/nomad/processing/data.py b/nomad/processing/data.py index 5c1058cfbc..a71937a1db 100644 --- a/nomad/processing/data.py +++ b/nomad/processing/data.py @@ -93,33 +93,30 @@ class Calc(Proc, datamodel.Calc): return self._upload_files def get_logger(self, **kwargs): - logger = super().get_logger() - logger = logger.bind( - upload_id=self.upload_id, mainfile=self.mainfile, calc_id=self.calc_id, **kwargs) - - return logger - - def get_calc_logger(self, **kwargs): """ Returns a wrapped logger that additionally saves all entries to the calculation processing log in the archive. """ - logger = self.get_logger(**kwargs) + logger = super().get_logger() + logger = logger.bind( + upload_id=self.upload_id, mainfile=self.mainfile, calc_id=self.calc_id, **kwargs) - if self._calc_proc_logwriter is None: + if self._calc_proc_logwriter_ctx is None: self._calc_proc_logwriter_ctx = self.upload_files.archive_log_file(self.calc_id, 'wt') self._calc_proc_logwriter = self._calc_proc_logwriter_ctx.__enter__() # pylint: disable=E1101 def save_to_calc_log(logger, method_name, event_dict): - program = event_dict.get('normalizer', 'parser') - event = event_dict.get('event', '') - entry = '[%s] %s: %s' % (method_name, program, event) - if len(entry) > 120: - self._calc_proc_logwriter.write(entry[:120]) - self._calc_proc_logwriter.write('...') - else: - self._calc_proc_logwriter.write(entry) - self._calc_proc_logwriter.write('\n') + if self._calc_proc_logwriter is not None: + program = event_dict.get('normalizer', 'parser') + event = event_dict.get('event', '') + entry = '[%s] %s: %s' % (method_name, program, event) + if len(entry) > 120: + self._calc_proc_logwriter.write(entry[:120]) + self._calc_proc_logwriter.write('...') + else: + self._calc_proc_logwriter.write(entry) + self._calc_proc_logwriter.write('\n') + return event_dict return wrap_logger(logger, processors=[save_to_calc_log]) @@ -149,7 +146,7 @@ class Calc(Proc, datamodel.Calc): @task def parsing(self): context = dict(parser=self.parser, step=self.parser) - logger = self.get_calc_logger(**context) + logger = self.get_logger(**context) parser = parser_dict[self.parser] with utils.timer(logger, 'parser executed', input_size=self.mainfile_file.size): @@ -212,7 +209,7 @@ class Calc(Proc, datamodel.Calc): for normalizer in normalizers: normalizer_name = normalizer.__name__ context = dict(normalizer=normalizer_name, step=normalizer_name) - logger = self.get_calc_logger(**context) + logger = self.get_logger(**context) with utils.timer( logger, 'normalizer executed', input_size=self.mainfile_file.size): @@ -481,8 +478,13 @@ class Upload(Chord, datamodel.Upload): self.name if self.name else '', self.upload_time.isoformat()), 'You can review your data on your upload page: %s' % config.services.upload_url ]) - infrastructure.send_mail( - name=name, email=user.email, message=message, subject='Processing completed') + try: + infrastructure.send_mail( + name=name, email=user.email, message=message, subject='Processing completed') + except Exception as e: + # probably due to email configuration problems + # don't fail or present this error to clients + self.logger.error('could not send after processing email', exc_info=e) @property def processed_calcs(self): diff --git a/tests/conftest.py b/tests/conftest.py index 7fdc116144..df468fa6ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -318,6 +318,16 @@ def smtpd(request): @pytest.fixture(scope='function', autouse=True) -def mails(smtpd): +def mails(smtpd, monkeypatch): smtpd.clear() + + old_config = config.mail + new_config = config.MailConfig( + 'localhost', + old_config.port, + old_config.user, + old_config.password, + old_config.from_address) + + monkeypatch.setattr('nomad.config.mail', new_config) yield smtpd -- GitLab