From 44856a3f75c3eb6c53aadf3bfe6cc047ffd30f82 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 16 Apr 2019 10:20:39 -0400 Subject: [PATCH] Report a couple common errors in dataset/job report. Report if empty inputs were used or if the same input was used more than once, these were identified as common sources of potential errors across many tools. Neither of these are definite problems, but both could be depending on the tool and both happen frequently. Outline a few more ideas as TODOs in the code. --- .../scripts/mvc/dataset/dataset-error.js | 54 ++++++++++++++++--- lib/galaxy/webapps/galaxy/api/jobs.py | 24 +++++++++ lib/galaxy/webapps/galaxy/buildapp.py | 1 + test/api/test_jobs.py | 28 ++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/client/galaxy/scripts/mvc/dataset/dataset-error.js b/client/galaxy/scripts/mvc/dataset/dataset-error.js index 9f49399c604..94499260574 100644 --- a/client/galaxy/scripts/mvc/dataset/dataset-error.js +++ b/client/galaxy/scripts/mvc/dataset/dataset-error.js @@ -32,6 +32,7 @@ var View = Backbone.View.extend({ url: job_url, success: job => { this.render_error_page(dataset, job); + this.find_common_problems(job); }, error: response => { var error_response = { @@ -56,6 +57,46 @@ var View = Backbone.View.extend({ }); }, + find_common_problems: function(job) { + var job_url = `${getAppRoot()}api/jobs/${job.id}/common_problems`; + Utils.get({ + url: job_url, + success: common_problems => { + this.render_common_problems(job, common_problems); + }, + error: response => { + console.log("error"); + console.log(response); + } + }); + return; + }, + + render_common_problems: function(job, common_problems) { + const has_duplicate_inputs = common_problems.has_duplicate_inputs; + const has_empty_inputs = common_problems.has_empty_inputs; + if (has_duplicate_inputs || has_empty_inputs) { + const reportEl = this.$el.find(".common_problems"); + reportEl.text("Detected Common Potential Problems"); + if (has_empty_inputs) { + reportEl.after(` +

+ The tool was executed with one or more empty input datasets, this isn't + always a problem and may not have been but frequently this is a source + of error. +

`); + } + if (has_duplicate_inputs) { + reportEl.after(` +

+ The tool was executed with one or more duplicate input datasets, this isn't + always a problem and may not have been but frequently this is a source + of error. +

`); + } + } + }, + /** Render the view */ render_error_page: function(dataset, job) { this.$el.empty().append(` @@ -88,23 +129,24 @@ var View = Backbone.View.extend({ const job_stderr = job.job_stderr; const job_messages = job.job_messages; if (!tool_stderr && !job_stderr && !job_messages) { - return ''; + return '

'; } - var message = '

Error Details

'; + var message = "

Error Details

"; if (job_messages) { - message += '

Execution resulted in the following messages:

'; + message += "

Execution resulted in the following messages:

"; for (let job_message of job_messages) { - message += `

${_.escape(job_message['desc'])}

`; + message += `

${_.escape(job_message["desc"])}

`; } } if (tool_stderr) { - message += '

Tool generated the following standard error:

'; + message += "

Tool generated the following standard error:

"; message += `
${_.escape(tool_stderr)}
`; } if (job_stderr) { - message += '

Galaxy job runner generated the following standard error:

'; + message += "

Galaxy job runner generated the following standard error:

"; message += `
${_.escape(job_stderr)}
`; } + message += `

`; return message; }, diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 9254ca9630a..6a42001ba51 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -167,6 +167,30 @@ class JobController(BaseAPIController, UsesLibraryMixinItems): job_dict['job_metrics'] = [metric_to_dict(metric) for metric in job.metrics] return job_dict + @expose_api + def common_problems(self, trans, id, **kwd): + """ + * GET /api/jobs/{id}/common_problems + check inputs and job for common potential problems to aid in error reporting + """ + job = self.__get_job(trans, id) + seen_ids = set() + has_empty_inputs = False + has_duplicate_inputs = False + for job_input_assoc in job.input_datasets: + input_dataset_instance = job_input_assoc.dataset + if input_dataset_instance.get_total_size() == 0: + has_empty_inputs = True + input_instance_id = input_dataset_instance.id + if input_instance_id in seen_ids: + has_duplicate_inputs = True + else: + seen_ids.add(input_instance_id) + # TODO: check percent of failing jobs around a window on job.update_time for handler - report if high. + # TODO: check percent of failing jobs around a window on job.update_time for destination_id - report if high. + # TODO: sniff inputs (add flag to allow checking files?) + return {"has_empty_inputs": has_empty_inputs, "has_duplicate_inputs": has_duplicate_inputs} + @expose_api def inputs(self, trans, id, **kwd): """ diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index b10c23002f4..6eb8fa7a76c 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -888,6 +888,7 @@ def populate_api_routes(webapp, app): webapp.mapper.connect('build_for_rerun', '/api/jobs/{id}/build_for_rerun', controller='jobs', action='build_for_rerun', conditions=dict(method=['GET'])) webapp.mapper.connect('resume', '/api/jobs/{id}/resume', controller='jobs', action='resume', conditions=dict(method=['PUT'])) webapp.mapper.connect('job_error', '/api/jobs/{id}/error', controller='jobs', action='error', conditions=dict(method=['POST'])) + webapp.mapper.connect('common_problems', '/api/jobs/{id}/common_problems', controller='jobs', action='common_problems', conditions=dict(method=['GET'])) # Job files controllers. Only for consumption by remote job runners. webapp.mapper.resource('file', diff --git a/test/api/test_jobs.py b/test/api/test_jobs.py index 22dc29d1d51..e3a5908dacd 100644 --- a/test/api/test_jobs.py +++ b/test/api/test_jobs.py @@ -190,6 +190,34 @@ class JobsApiTestCase(api.ApiTestCase): assert_ok=False) assert not dataset['visible'] + @skip_without_tool('empty_output') + def test_common_problems(self): + with self.dataset_populator.test_history() as history_id: + empty_run_response = self.dataset_populator.run_tool( + tool_id='empty_output', + inputs={}, + history_id=history_id, + ) + empty_hda = empty_run_response["outputs"][0] + cat_empty_twice_run_response = self.dataset_populator.run_tool( + tool_id='cat1', + inputs={ + 'input1': {'src': 'hda', 'id': empty_hda['id']}, + 'queries_0|input2': {'src': 'hda', 'id': empty_hda['id']} + }, + history_id=history_id, + ) + empty_output_job = empty_run_response["jobs"][0] + cat_empty_job = cat_empty_twice_run_response["jobs"][0] + empty_output_common_problems_response = self._get('jobs/%s/common_problems' % empty_output_job["id"]).json() + cat_empty_common_problems_response = self._get('jobs/%s/common_problems' % cat_empty_job["id"]).json() + self._assert_has_keys(empty_output_common_problems_response, "has_empty_inputs", "has_duplicate_inputs") + self._assert_has_keys(cat_empty_common_problems_response, "has_empty_inputs", "has_duplicate_inputs") + assert not empty_output_common_problems_response["has_empty_inputs"] + assert cat_empty_common_problems_response["has_empty_inputs"] + assert not empty_output_common_problems_response["has_duplicate_inputs"] + assert cat_empty_common_problems_response["has_duplicate_inputs"] + @skip_without_tool('detect_errors_aggressive') def test_report_error(self): with self.dataset_populator.test_history() as history_id: