Files
galaxy/test/unit/jobs/test_runner_local.py
T
John Chilton 4bf47d0877 Improved home and temp directory handling.
Home Directory Handling
-----------------------

- For profile < 18.01 tools:
  - If use_shared_home="false" is set on the command block - the tool will be given a clean $HOME directory.
  - If ``shared_home_dir`` is set in Galaxy's config or the job destination configuration, Galaxy will set $HOME in the tool's job environment to point at this directory.
- For profile >= 18.01 tools, jobs will be given a clean home directory by default unless ``use_shared_home="true"`` is set. If that is set, the profile < 18.01 behavior is used.

In addition to these changes for Galaxy tools, the tool framework itself has been updated to allow other potential behaviors including the CWL defaults.

Integration tests for each of these cases has been added.

Upgrading to 18.01 - we recommend dropping overriding ``HOME`` in any Galaxy environment configuration (e.g. env directives in job_conf.xml). If any such configuration is found, we recommend instead setting "shared_home_dir" for destinations to the that cluster destination's shared HOME directory. If all destinations share a single HOME directory, this can be set in galaxy.ini instead of job_conf.xml.

Temp Directory Handling
-----------------------

There were serious disagreements with how to proceed here. I felt we should aggressively isolate tool TMP directories and provide deployers options to tweak these through structured arguments. Nicola felt we should defer to the environment variables already being set job_conf.xml - but tweak their meanings by default. Nate fell somewhere in between. I think all sides have merit and could make sense depending on what you want to improve (easing cognative load, reducing out-of-box errors, easing advanced deployer configs, structured reasonsing of paths by Galaxy, etc...).

I've navigated a path here that doesn't particularly reduce out of the box errors as I wanted by setting up a per-job temp directory by default but does make it possible and shouldn't break any existing configurations and doesn't make it anything more difficult to manage these things via environment variables.

The new approach doesn't change any thing by default for Galaxy tools (regardless of tool profile version). The only way to achieve new behaviors is for the deployer to set a job_conf parameter called "tmp_dir". If this is set, it can be set to "True" (to default to a new, clean directory below the job directory) or to a shell expression to allow setting this dynamically at runtime to paths or paths beneath directories as needed on a per-destination basis. If a temp directory is set this way all three variables TMP, TMPDIR, and TEMP are set.

Docker Environment Handling
---------------------------

- Mirror the CWL behavior of mounting an external /tmp by default.
- For newer tools, pass through the HOME, TMP, TMPDIR, and TEMP environment variables into the Docker container in addition to GALAXY_SLOTS.
- Fix the pass through of environment variables into Docker containers.
2018-01-09 09:30:16 -05:00

220 lines
7.2 KiB
Python

import os
import threading
import time
from unittest import TestCase
import psutil
from galaxy import model
from galaxy.jobs import metrics
from galaxy.jobs.runners import local
from galaxy.util import bunch
from ..tools_support import (
UsesApp,
UsesTools
)
class TestLocalJobRunner(TestCase, UsesApp, UsesTools):
def setUp(self):
self.setup_app()
self._init_tool()
self.app.job_metrics = metrics.JobMetrics()
self.job_wrapper = MockJobWrapper(self.app, self.test_directory, self.tool)
def tearDown(self):
self.tear_down_app()
def test_run(self):
self.job_wrapper.command_line = "echo HelloWorld"
runner = local.LocalJobRunner(self.app, 1)
runner.queue_job(self.job_wrapper)
assert self.job_wrapper.stdout.strip() == "HelloWorld"
def test_galaxy_lib_on_path(self):
self.job_wrapper.command_line = '''python -c "import galaxy.util"'''
runner = local.LocalJobRunner(self.app, 1)
runner.queue_job(self.job_wrapper)
assert self.job_wrapper.exit_code == 0
def test_default_slots(self):
self.job_wrapper.command_line = '''echo $GALAXY_SLOTS'''
runner = local.LocalJobRunner(self.app, 1)
runner.queue_job(self.job_wrapper)
assert self.job_wrapper.stdout.strip() == "1"
def test_slots_override(self):
# Set local_slots in job destination to specify slots for
# local job runner.
self.job_wrapper.job_destination.params["local_slots"] = 3
self.job_wrapper.command_line = '''echo $GALAXY_SLOTS'''
runner = local.LocalJobRunner(self.app, 1)
runner.queue_job(self.job_wrapper)
assert self.job_wrapper.stdout.strip() == "3"
def test_exit_code(self):
self.job_wrapper.command_line = '''sh -c "exit 4"'''
runner = local.LocalJobRunner(self.app, 1)
runner.queue_job(self.job_wrapper)
assert self.job_wrapper.exit_code == 4
def test_metadata_gets_set(self):
runner = local.LocalJobRunner(self.app, 1)
runner.queue_job(self.job_wrapper)
assert os.path.exists(self.job_wrapper.mock_metadata_path)
def test_metadata_gets_set_if_embedded(self):
self.job_wrapper.job_destination.params["embed_metadata_in_job"] = "True"
# Kill off cruft for _handle_metadata_externally and make sure job stil works...
self.job_wrapper.external_output_metadata = None
self.app.datatypes_registry.set_external_metadata_tool = None
runner = local.LocalJobRunner(self.app, 1)
runner.queue_job(self.job_wrapper)
assert os.path.exists(self.job_wrapper.mock_metadata_path)
def test_stopping_job(self):
self.job_wrapper.command_line = '''python -c "import time; time.sleep(15)"'''
runner = local.LocalJobRunner(self.app, 1)
def queue():
runner.queue_job(self.job_wrapper)
t = threading.Thread(target=queue)
t.start()
external_id = self.job_wrapper.wait_for_external_id()
mock_job = bunch.Bunch(
get_external_output_metadata=lambda: None,
get_job_runner_external_id=lambda: str(external_id),
get_id=lambda: 1
)
assert psutil.pid_exists(external_id)
runner.stop_job(mock_job)
t.join(1)
assert not psutil.pid_exists(external_id)
def test_shutdown_no_jobs(self):
self.app.config.monitor_thread_join_timeout = 5
runner = local.LocalJobRunner(self.app, 1)
runner.shutdown()
def test_stopping_job_at_shutdown(self):
self.job_wrapper.command_line = '''python -c "import time; time.sleep(15)"'''
runner = local.LocalJobRunner(self.app, 1)
self.app.config.monitor_thread_join_timeout = 15
def queue():
runner.queue_job(self.job_wrapper)
t = threading.Thread(target=queue)
t.start()
external_id = self.job_wrapper.wait_for_external_id()
assert psutil.pid_exists(external_id)
runner.shutdown()
t.join(1)
assert not psutil.pid_exists(external_id)
assert "job terminated by Galaxy shutdown" in self.job_wrapper.fail_message
class MockJobWrapper(object):
def __init__(self, app, test_directory, tool):
working_directory = os.path.join(test_directory, "workdir")
tool_working_directory = os.path.join(working_directory, "working")
os.makedirs(tool_working_directory)
self.app = app
self.tool = tool
self.requires_containerization = False
self.state = model.Job.states.QUEUED
self.command_line = "echo HelloWorld"
self.environment_variables = []
self.commands_in_new_shell = False
self.prepare_called = False
self.write_version_cmd = None
self.dependency_shell_commands = None
self.working_directory = working_directory
self.tool_working_directory = tool_working_directory
self.requires_setting_metadata = True
self.job_destination = bunch.Bunch(id="default", params={})
self.galaxy_lib_dir = os.path.abspath("lib")
self.job_id = 1
self.external_id = None
self.output_paths = ['/tmp/output1.dat']
self.mock_metadata_path = os.path.abspath(os.path.join(test_directory, "METADATA_SET"))
self.metadata_command = "touch %s" % self.mock_metadata_path
self.galaxy_virtual_env = None
self.shell = "/bin/bash"
self.cleanup_job = "never"
self.tmp_dir_creation_statement = ""
# Cruft for setting metadata externally, axe at some point.
self.external_output_metadata = bunch.Bunch(
set_job_runner_external_pid=lambda pid, session: None
)
self.app.datatypes_registry.set_external_metadata_tool = bunch.Bunch(
build_dependency_shell_commands=lambda: []
)
def check_tool_output(*args, **kwds):
return "ok"
def wait_for_external_id(self):
"""Test method for waiting til an external id has been registered."""
external_id = None
for i in range(50):
external_id = self.external_id
if external_id:
break
time.sleep(.1)
return external_id
def prepare(self):
self.prepare_called = True
def set_job_destination(self, job_destination, external_id):
self.external_id = external_id
def get_command_line(self):
return self.command_line
def get_id_tag(self):
return "1"
def get_state(self):
return self.state
def change_state(self, state):
self.state = state
def get_output_fnames(self):
return []
def get_job(self):
return model.Job()
def setup_external_metadata(self, **kwds):
return self.metadata_command
def get_env_setup_clause(self):
return ""
def has_limits(self):
return False
def fail(self, message, exception):
self.fail_message = message
self.fail_exception = exception
def finish(self, stdout, stderr, exit_code, **kwds):
self.stdout = stdout
self.stderr = stderr
self.exit_code = exit_code
def tmp_directory(self):
return None
def home_directory(self):
return None