Skip to content

Commit c8314a8

Browse files
authored
Merge pull request #198 from pauldg/feat/explain-on-failure
Add a explain on failure option to log scheduling trace for job mapping failures
2 parents 30454de + 29adcbd commit c8314a8

3 files changed

Lines changed: 82 additions & 12 deletions

File tree

tests/test_explain.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
from unittest.mock import MagicMock
44

55
import yaml
6+
from galaxy.jobs import JobDestination
67
from galaxy.jobs.mapper import JobMappingException
78

89
from tpv.commands.dryrunner import TPVDryRunner
10+
from tpv.commands.test import mock_galaxy
911
from tpv.core.entities import SchedulingTags
1012
from tpv.core.explain import ExplainCollector, ExplainPhase
13+
from tpv.rules import gateway
1114

1215

1316
class TestExplainCollectorUnit(unittest.TestCase):
@@ -496,3 +499,59 @@ def test_from_params_without_tool_id_sets_tool_none(self):
496499
tpv_confs=[self._fixture_path("mapping-basic.yml")],
497500
)
498501
self.assertIsNone(runner.tool)
502+
503+
504+
505+
class TestGatewayExplainOnFailure(unittest.TestCase):
506+
"""Tests for the tpv_explain_on_failure gateway config option."""
507+
508+
@staticmethod
509+
def _fixture_path(name):
510+
return os.path.join(os.path.dirname(__file__), f"fixtures/{name}")
511+
512+
def _call_gateway(self, tool_id, referrer=None, explain_collector=None):
513+
gateway.ACTIVE_DESTINATION_MAPPERS = {}
514+
app = mock_galaxy.App(job_conf=self._fixture_path("job_conf.yml"))
515+
tool = mock_galaxy.Tool(tool_id)
516+
job = mock_galaxy.Job()
517+
user = mock_galaxy.User("gargravarr", "fairycake@vortex.org")
518+
return gateway.map_tool_to_destination(
519+
app, job, tool, user,
520+
referrer=referrer,
521+
tpv_config_files=[self._fixture_path("mapping-basic.yml")],
522+
explain_collector=explain_collector,
523+
)
524+
525+
def test_no_log_when_param_not_set(self):
526+
"""No warning is logged on failure when tpv_explain_on_failure is not configured."""
527+
with self.assertNoLogs("tpv.rules.gateway", level="WARNING"):
528+
with self.assertRaises(JobMappingException):
529+
self._call_gateway("unschedulable_tool")
530+
531+
def test_logs_trace_on_mapping_failure(self):
532+
"""A WARNING containing the scheduling trace is logged when tpv_explain_on_failure=true and mapping fails."""
533+
referrer = JobDestination(id="tpv_dispatcher", params={"tpv_explain_on_failure": True})
534+
with self.assertLogs("tpv.rules.gateway", level="WARNING") as cm:
535+
with self.assertRaises(JobMappingException):
536+
self._call_gateway("unschedulable_tool", referrer=referrer)
537+
self.assertEqual(len(cm.records), 1)
538+
trace = cm.records[0].getMessage()
539+
self.assertIn("TPV SCHEDULING DECISION TRACE", trace)
540+
self.assertIn("REJECTED", trace)
541+
self.assertIn("tag mismatch", trace)
542+
self.assertIn("No destinations", trace)
543+
544+
def test_no_log_on_successful_mapping(self):
545+
"""No warning is logged when tpv_explain_on_failure=true but mapping succeeds."""
546+
referrer = JobDestination(id="tpv_dispatcher", params={"tpv_explain_on_failure": True})
547+
with self.assertNoLogs("tpv.rules.gateway", level="WARNING"):
548+
destination = self._call_gateway("bwa", referrer=referrer)
549+
self.assertIsNotNone(destination)
550+
551+
def test_explicit_collector_not_logged_by_gateway(self):
552+
"""When an explicit explain_collector is passed (dry-run path), the gateway does not log on failure."""
553+
collector = ExplainCollector()
554+
with self.assertNoLogs("tpv.rules.gateway", level="WARNING"):
555+
with self.assertRaises(JobMappingException):
556+
self._call_gateway("unschedulable_tool", explain_collector=collector)
557+
self.assertTrue(len(collector.steps) > 0)

tpv/core/helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,5 @@ def get_dataset_attributes(
167167
"size": get_dataset_size(i.dataset.dataset),
168168
}
169169
for i in datasets or {}
170+
if i.dataset
170171
}

tpv/rules/gateway.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from galaxy.app import UniverseApplication
88
from galaxy.jobs import JobDestination, JobWrapper
9+
from galaxy.jobs.mapper import JobMappingException
910
from galaxy.model import Job
1011
from galaxy.model import User as GalaxyUser
1112
from galaxy.tools import Tool as GalaxyTool
@@ -117,18 +118,27 @@ def map_tool_to_destination(
117118
raise ValueError("One of tpv_configs or tpv_config_files must be specified in execution environment.")
118119
referrer_id = referrer.id if referrer else None
119120
destination_mapper = lock_and_load_mapper(app, referrer_id or "tpv_dispatcher", resolved_tpv_configs)
120-
if explain_collector:
121+
explain_on_failure = bool(referrer.params.get("tpv_explain_on_failure", False)) if referrer else False
122+
log_on_failure = explain_collector is None and explain_on_failure
123+
collector = explain_collector or (ExplainCollector() if log_on_failure else None)
124+
if collector:
121125
configs_list = listify(resolved_tpv_configs)
122126
for config_source in configs_list:
123127
source_name = config_source if isinstance(config_source, str) else "<inline config>"
124-
explain_collector.add_step(ExplainPhase.CONFIG_LOADING, f"Loaded config: {source_name}")
125-
return destination_mapper.map_to_destination(
126-
app,
127-
tool,
128-
user,
129-
job,
130-
job_wrapper,
131-
resource_params,
132-
workflow_invocation_uuid,
133-
explain_collector=explain_collector,
134-
)
128+
collector.add_step(ExplainPhase.CONFIG_LOADING, f"Loaded config: {source_name}")
129+
try:
130+
return destination_mapper.map_to_destination(
131+
app,
132+
tool,
133+
user,
134+
job,
135+
job_wrapper,
136+
resource_params,
137+
workflow_invocation_uuid,
138+
explain_collector=collector,
139+
)
140+
except JobMappingException:
141+
if log_on_failure:
142+
assert collector is not None
143+
log.warning("Job mapping failed. TPV scheduling trace:\n%s", collector.render())
144+
raise

0 commit comments

Comments
 (0)