Implement workflow scheduling 'plugin' framework.

Models:

Workflow invocations have been augmented with significantly more state - inputs, parameters, runtime step state, are all being tracked now. Workflow invocations have a state that can be changed over time, the UUIDs generated for workflow invocations in Pull Request #465 have to be persisted so they can be reused when scheduling new jobs for theworkflow invocation. Workflow invocation steps now have an action parameter for persisting state provided by users during the execution of the workflow (see forthcoming PauseModule for further details).

Some initial elements of these model changes were based on model changes in Kyle Ellrott's Galaxy farm work (https://bitbucket.org/kellrott/galaxy-farm/branch/workflow_migrate). I made heavy modifications to the model to enforce referential integrity on parameter to workflow step mappings and made some cosmetic changes various other details.

Scheduling Plugins:

Used the pattern setup with dependency resolvers and job metrics to build a dynamic plugin infrastructure for defining workflow schedulers. I hesistate calling anything with only one implementation a plugin infrastructure, but I am confident enough that the combination of persisted workflow request combined with scheduler tag could be used to build a galaxy-farm plugin that would wait for another Galaxy instance to become available and it would pull the workflow down and

This work piggy backs on Galaxy job handlers to have workflow scheduled in the background (i.e. during submission each workflow being scheduled in the background is assigned a unique job handler and only that job handler thread will process the workflow). It should be pretty easy to allow the definition of a new kind of handler - that is a workflow handler instead of a job handler if that is of interest.

I will probably move a bunch of stuff that is happening in workflow/scheduling_manager.py more into the scheduler itself so that it can be more configurable and closer to a true plugin.

API:

There are a number of new API points here for flushing out dealing with workflow invocations (called usages in existing parlance).

 - POST /api/workflows/{encoded_workflow_id}/usage

   Schedule a worklfow to be run in the background and return just the workflow invocation information.

   RESTfully speaking this should be plural but the matching GET endpoint is likewise usage and not usages - so I am favoring consistency over RESTful correctness here. Also, likewise creating a 'usage' feel like odd - I would like to make all of the usage endpoints aliases to a more RESTfully correct invocations endpoints.

   The existing workflow run API endpoints still work and still work the way they use usually - but the output now includes all of the workflow invocation to_dict stuff as well as the list of outputs it initially used. Once everything is scheduled this way - that list of outputs is going to have to disappear but hopefully people can start using the invocation stuff now to help the transition.

 - DELETE /api/workflows/{workflow_id}/usage/{usage_id}

   Cancel a scheduled workflow invocation.

 - GET /api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}

   Get information about a workflow invocation step.

 - PUT /api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}

   Update a workflow invocation step - for ones with modifiable state. Extension point added to workflow modules to support this but it is unused by all existing worklfow modules. A subsequent PauseModule will use this to either continue or cancel a workflow invocation at a particular step.

Modules:

Workflow modules can now define new methods for dealing with recovering state and interacting with user requests.

Testing:

One can issue a workflow request by running the following test.

./run_tests.sh -api test/api/test_workflows.py:WorkflowsApiTestCase.test_workflow_request
This commit is contained in:
John Chilton
2014-11-13 13:47:38 -05:00
parent 4a6b5a7464
commit d1cd4ab4d2
23 changed files with 1224 additions and 39 deletions
+1
View File
@@ -67,6 +67,7 @@ data_manager_conf.xml
shed_data_manager_conf.xml
object_store_conf.xml
job_metrics_conf.xml
workflow_schedulers_conf.xml
config/*
static/welcome.html.*
static/welcome.html
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!-- If workflow_schedulers_conf.xml exists it defines the workflow scheduling
plugins to load and how to configure them. Currently only the core workflow
scheduling is available.
-->
<workflow_schedulers default="core">
<!-- Each element in this file corresponds to a workflow scheduling plugin
in lib/galaxy/workflow/schedulers. -->
<!-- Core plugin schedules whole workflow at outset inside Galaxy and doesn't
require any external dependencies. -->
<core id="core" />
</workflow_schedulers>
+9
View File
@@ -142,6 +142,11 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
self.proxy_manager = ProxyManager( self.config )
# Initialize the external service types
self.external_service_types = external_service_types.ExternalServiceTypesCollection( self.config.external_service_type_config_file, self.config.external_service_type_path, self )
from galaxy.workflow import scheduling_manager
# Must be initialized after job_config.
self.workflow_scheduling_manager = scheduling_manager.WorkflowSchedulingManager( self )
self.model.engine.dispose()
self.control_worker = GalaxyQueueWorker(self,
galaxy.queues.control_queue_from_config(self.config),
@@ -150,6 +155,7 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
self.control_worker.start()
def shutdown( self ):
self.workflow_scheduling_manager.shutdown()
self.job_manager.shutdown()
self.object_store.shutdown()
if self.heartbeat:
@@ -171,3 +177,6 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
self.trace_logger = FluentTraceLogger( 'galaxy', self.config.fluent_host, self.config.fluent_port )
else:
self.trace_logger = None
def is_job_handler( self ):
return (self.config.track_jobs_in_database and self.job_config.is_handler(self.config.server_name)) or not self.config.track_jobs_in_database
+1
View File
@@ -139,6 +139,7 @@ class Configuration( object ):
self.collect_outputs_from = [ x.strip() for x in kwargs.get( 'collect_outputs_from', 'new_file_path,job_working_directory' ).lower().split(',') ]
self.template_path = resolve_path( kwargs.get( "template_path", "templates" ), self.root )
self.template_cache = resolve_path( kwargs.get( "template_cache_path", "database/compiled_templates" ), self.root )
self.workflow_schedulers_config_file = resolve_path( kwargs.get( 'workflow_schedulers_config_file', 'config/workflow_schedulers_conf.xml' ), self.root )
self.local_job_queue_workers = int( kwargs.get( "local_job_queue_workers", "5" ) )
self.cluster_job_queue_workers = int( kwargs.get( "cluster_job_queue_workers", "3" ) )
self.job_queue_cleanup_interval = int( kwargs.get("job_queue_cleanup_interval", "5") )
+1 -1
View File
@@ -18,7 +18,7 @@ class JobManager( object ):
"""
def __init__( self, app ):
self.app = app
if (self.app.config.track_jobs_in_database and self.app.job_config.is_handler(self.app.config.server_name)) or not self.app.config.track_jobs_in_database:
if self.app.is_job_handler():
log.debug("Starting job handler")
self.job_handler = handler.JobHandler( app )
self.job_queue = self.job_handler.job_queue
+41
View File
@@ -1,5 +1,6 @@
from galaxy import model
from galaxy import exceptions
from galaxy.workflow import modules
class WorkflowsManager( object ):
@@ -49,6 +50,46 @@ class WorkflowsManager( object ):
self.check_security( trans, workflow_invocation, check_ownership=True, check_accessible=False )
return workflow_invocation
def cancel_invocation( self, trans, decoded_invocation_id ):
workflow_invocation = self.get_invocation( trans, decoded_invocation_id )
cancelled = workflow_invocation.cancel()
if cancelled:
trans.sa_session.add( workflow_invocation )
trans.sa_session.flush()
else:
# TODO: More specific exception?
raise exceptions.MessageException( "Cannot cancel an inactive workflow invocation." )
return workflow_invocation
def get_invocation_step( self, trans, decoded_workflow_invocation_step_id ):
try:
workflow_invocation_step = trans.sa_session.query(
model.WorkflowInvocationStep
).get( decoded_workflow_invocation_step_id )
except Exception:
raise exceptions.ObjectNotFound()
self.check_security( trans, workflow_invocation_step.workflow_invocation, check_ownership=True, check_accessible=False )
return workflow_invocation_step
def update_invocation_step( self, trans, decoded_workflow_invocation_step_id, action ):
if action is None:
raise exceptions.RequestParameterMissingException( "Updating workflow invocation step requires an action parameter. " )
workflow_invocation_step = self.get_invocation_step( trans, decoded_workflow_invocation_step_id )
workflow_invocation = workflow_invocation_step.workflow_invocation
if not workflow_invocation.active:
raise exceptions.RequestParameterInvalidException( "Attempting to modify the state of an completed workflow invocation." )
step = workflow_invocation_step.workflow_step
module = modules.module_factory.from_workflow_step( trans, step )
performed_action = module.do_invocation_step_action( step, action )
workflow_invocation_step.action = performed_action
trans.sa_session.add( workflow_invocation_step )
trans.sa_session.flush()
return workflow_invocation_step
def build_invocations_query( self, trans, decoded_stored_workflow_id ):
try:
stored_workflow = trans.sa_session.query(
+132 -4
View File
@@ -39,6 +39,7 @@ from galaxy.web.framework.helpers import to_unicode
from galaxy.web.form_builder import (AddressField, CheckboxField, HistoryField,
PasswordField, SelectField, TextArea, TextField, WorkflowField,
WorkflowMappingField)
from galaxy.model.orm import and_, or_
from sqlalchemy.orm import object_session
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import func
@@ -3096,8 +3097,74 @@ class StoredWorkflowMenuEntry( object ):
class WorkflowInvocation( object, Dictifiable ):
dict_collection_visible_keys = ( 'id', 'update_time', 'workflow_id' )
dict_element_visible_keys = ( 'id', 'update_time', 'workflow_id' )
dict_collection_visible_keys = ( 'id', 'update_time', 'workflow_id', 'history_id', 'uuid', 'state' )
dict_element_visible_keys = ( 'id', 'update_time', 'workflow_id', 'history_id', 'uuid', 'state' )
states = Bunch(
NEW='new', # Brand new workflow invocation... maybe this should be same as READY
READY='ready', # Workflow ready for another iteration of scheduling.
SCHEDULED='scheduled', # Workflow has been scheduled.
CANCELLED='cancelled',
FAILED='failed',
)
@property
def active( self ):
""" Indicates the workflow invocation is somehow active - and in
particular valid actions may be performed on its
``WorkflowInvocationStep``s.
"""
states = WorkflowInvocation.states
return self.state in [ states.NEW, states.READY ]
def cancel( self ):
if not self.active:
return False
else:
self.state = WorkflowInvocation.states.CANCELLED
return True
def fail( self ):
self.state = WorkflowInvocation.states.FAILED
def step_states_by_step_id( self ):
step_states = {}
for step_state in self.step_states:
step_id = step_state.workflow_step_id
step_states[ step_id ] = step_state
return step_states
def step_invocations_by_step_id( self ):
step_invocations = {}
for invocation_step in self.steps:
step_id = invocation_step.workflow_step_id
if step_id not in step_invocations:
step_invocations[ step_id ] = []
step_invocations[ step_id ].append( invocation_step )
return step_invocations
@staticmethod
def poll_active_workflow_ids(
sa_session,
scheduler=None,
handler=None
):
and_conditions = [
or_(
WorkflowInvocation.state == WorkflowInvocation.states.NEW,
WorkflowInvocation.state == WorkflowInvocation.states.READY
),
]
if scheduler is not None:
and_conditions.append( WorkflowInvocation.scheduler == scheduler )
if handler is not None:
and_conditions.append( WorkflowInvocation.handler == handler )
query = sa_session.query(
WorkflowInvocation
).filter( and_( *and_conditions ) )
# Immediately just load all ids into memory so time slicing logic
# is relatively intutitive.
return map( lambda wi: wi.id, query.all() )
def to_dict( self, view='collection', value_mapper=None ):
rval = super( WorkflowInvocation, self ).to_dict( view=view, value_mapper=value_mapper )
@@ -3123,14 +3190,63 @@ class WorkflowInvocation( object, Dictifiable ):
class WorkflowInvocationStep( object, Dictifiable ):
dict_collection_visible_keys = ( 'id', 'update_time', 'job_id', 'workflow_step_id' )
dict_element_visible_keys = ( 'id', 'update_time', 'job_id', 'workflow_step_id' )
dict_collection_visible_keys = ( 'id', 'update_time', 'job_id', 'workflow_step_id', 'action' )
dict_element_visible_keys = ( 'id', 'update_time', 'job_id', 'workflow_step_id', 'action' )
def to_dict( self, view='collection', value_mapper=None ):
rval = super( WorkflowInvocationStep, self ).to_dict( view=view, value_mapper=value_mapper )
rval['order_index'] = self.workflow_step.order_index
return rval
class WorkflowRequest( object, Dictifiable ):
dict_collection_visible_keys = [ 'id', 'name', 'type', 'state', 'history_id', 'workflow_id' ]
dict_element_visible_keys = [ 'id', 'name', 'type', 'state', 'history_id', 'workflow_id' ]
def to_dict( self, view='collection', value_mapper=None ):
rval = super( WorkflowRequest, self ).to_dict( view=view, value_mapper=value_mapper )
return rval
class WorkflowRequestInputParameter(object, Dictifiable):
""" Workflow-related parameters not tied to steps or inputs.
"""
dict_collection_visible_keys = ['id', 'name', 'value', 'type']
types = Bunch(
REPLACEMENT_PARAMETERS='replacements',
META_PARAMETERS='meta', #
)
def __init__( self, name=None, value=None, type=None ):
self.name = name
self.value = value
self.type = type
class WorkflowRequestStepState(object, Dictifiable):
""" Workflow step value parameters.
"""
dict_collection_visible_keys = ['id', 'name', 'value', 'workflow_step_id']
def __init__( self, workflow_step=None, name=None, value=None ):
self.workflow_step = workflow_step
self.name = name
self.value = value
self.type = type
class WorkflowRequestToInputDatasetAssociation(object, Dictifiable):
""" Workflow step input dataset parameters.
"""
dict_collection_visible_keys = ['id', 'workflow_invocation_id', 'workflow_step_id', 'dataset_id', 'name' ]
class WorkflowRequestToInputDatasetCollectionAssociation(object, Dictifiable):
""" Workflow step input dataset collection parameters.
"""
dict_collection_visible_keys = ['id', 'workflow_invocation_id', 'workflow_step_id', 'dataset_collection_id', 'name' ]
class MetadataFile( object ):
def __init__( self, dataset=None, name=None ):
@@ -4038,6 +4154,18 @@ class ToolTagAssociation( ItemTagAssociation ):
self.value = None
self.user_value = None
class WorkRequestTagAssociation( ItemTagAssociation ):
def __init__( self, id=None, user=None, workflow_request_id=None, tag_id=None, user_tname=None, value=None ):
self.id = id
self.user = user
self.workflow_request_id = workflow_request_id
self.tag_id = tag_id
self.user_tname = user_tname
self.value = None
self.user_value = None
# Item annotation classes.
class HistoryAnnotationAssociation( object ):
+82 -4
View File
@@ -710,6 +710,46 @@ model.WorkflowStep.table = Table( "workflow_step", metadata,
## Column( "input_connections", JSONType )
)
model.WorkflowRequestStepState.table = Table(
"workflow_request_step_states", metadata,
Column( "id", Integer, primary_key=True ),
Column( "workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id", onupdate="CASCADE", ondelete="CASCADE" )),
Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id" )),
Column( "value", JSONType ),
)
model.WorkflowRequestInputParameter.table = Table(
"workflow_request_input_parameters", metadata,
Column( "id", Integer, primary_key=True ),
Column( "workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id", onupdate="CASCADE", ondelete="CASCADE" )),
Column( "name", Unicode(255) ),
Column( "value", TEXT ),
Column( "type", Unicode(255) ),
)
model.WorkflowRequestToInputDatasetAssociation.table = Table(
"workflow_request_to_input_dataset", metadata,
Column( "id", Integer, primary_key=True ),
Column( "name", String(255) ),
Column( "workflow_invocation_id", Integer, ForeignKey( "workflow_invocation.id" ), index=True ),
Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id") ),
Column( "dataset_id", Integer, ForeignKey( "history_dataset_association.id" ), index=True ),
)
model.WorkflowRequestToInputDatasetCollectionAssociation.table = Table(
"workflow_request_to_input_collection_dataset", metadata,
Column( "id", Integer, primary_key=True ),
Column( "name", String(255) ),
Column( "workflow_invocation_id", Integer, ForeignKey( "workflow_invocation.id" ), index=True ),
Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id") ),
Column( "dataset_collection_id", Integer, ForeignKey( "history_dataset_collection_association.id" ), index=True ),
)
model.WorkflowStepConnection.table = Table( "workflow_step_connection", metadata,
Column( "id", Integer, primary_key=True ),
Column( "output_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ),
@@ -728,8 +768,13 @@ model.WorkflowInvocation.table = Table( "workflow_invocation", metadata,
Column( "id", Integer, primary_key=True ),
Column( "create_time", DateTime, default=now ),
Column( "update_time", DateTime, default=now, onupdate=now ),
Column( "workflow_id", Integer, ForeignKey( "workflow.id" ), index=True, nullable=False )
)
Column( "workflow_id", Integer, ForeignKey( "workflow.id" ), index=True, nullable=False ),
Column( "state", TrimmedString( 64 ), index=True ),
Column( "scheduler", TrimmedString( 255 ), index=True ),
Column( "handler", TrimmedString( 255 ), index=True ),
Column( 'uuid', UUIDType() ),
Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ),
)
model.WorkflowInvocationStep.table = Table( "workflow_invocation_step", metadata,
Column( "id", Integer, primary_key=True ),
@@ -737,8 +782,9 @@ model.WorkflowInvocationStep.table = Table( "workflow_invocation_step", metadata
Column( "update_time", DateTime, default=now, onupdate=now ),
Column( "workflow_invocation_id", Integer, ForeignKey( "workflow_invocation.id" ), index=True, nullable=False ),
Column( "workflow_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True, nullable=False ),
Column( "job_id", Integer, ForeignKey( "job.id" ), index=True, nullable=True )
)
Column( "job_id", Integer, ForeignKey( "job.id" ), index=True, nullable=True ),
Column( "action", JSONType, nullable=True ),
)
model.StoredWorkflowUserShareAssociation.table = Table( "stored_workflow_user_share_connection", metadata,
Column( "id", Integer, primary_key=True ),
@@ -1968,6 +2014,11 @@ mapper( model.StoredWorkflowMenuEntry, model.StoredWorkflowMenuEntry.table,
mapper( model.WorkflowInvocation, model.WorkflowInvocation.table,
properties=dict(
history=relation( model.History ),
input_parameters=relation( model.WorkflowRequestInputParameter ),
step_states=relation( model.WorkflowRequestStepState ),
input_datasets=relation( model.WorkflowRequestToInputDatasetAssociation ),
input_dataset_collections=relation( model.WorkflowRequestToInputDatasetCollectionAssociation ),
steps=relation( model.WorkflowInvocationStep, backref='workflow_invocation', lazy=False ),
workflow=relation( model.Workflow ) ) )
@@ -1976,6 +2027,33 @@ mapper( model.WorkflowInvocationStep, model.WorkflowInvocationStep.table,
workflow_step = relation( model.WorkflowStep ),
job = relation( model.Job, backref=backref( 'workflow_invocation_step', uselist=False ) ) ) )
simple_mapping(
model.WorkflowRequestInputParameter,
workflow_invocation=relation( model.WorkflowInvocation ),
)
simple_mapping(
model.WorkflowRequestStepState,
workflow_invocation=relation( model.WorkflowInvocation ),
workflow_step=relation( model.WorkflowStep ),
)
simple_mapping(
model.WorkflowRequestToInputDatasetAssociation,
workflow_invocation=relation( model.WorkflowInvocation ),
workflow_step=relation( model.WorkflowStep ),
dataset=relation( model.HistoryDatasetAssociation ),
)
simple_mapping(
model.WorkflowRequestToInputDatasetCollectionAssociation,
workflow_invocation=relation( model.WorkflowInvocation ),
workflow_step=relation( model.WorkflowStep ),
dataset_collection=relation( model.HistoryDatasetCollectionAssociation ),
)
mapper( model.MetadataFile, model.MetadataFile.table,
properties=dict( history_dataset=relation( model.HistoryDatasetAssociation ), library_dataset=relation( model.LibraryDatasetDatasetAssociation ) ) )
@@ -0,0 +1,144 @@
"""
Migration script for workflow request tables.
"""
from sqlalchemy import *
from sqlalchemy.orm import *
from migrate import *
from migrate.changeset import *
from galaxy.model.custom_types import *
import datetime
now = datetime.datetime.utcnow
import logging
log = logging.getLogger( __name__ )
metadata = MetaData()
WorkflowRequestInputParameter_table = Table(
"workflow_request_input_parameters", metadata,
Column( "id", Integer, primary_key=True ),
Column( "workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id", onupdate="CASCADE", ondelete="CASCADE" )),
Column( "name", Unicode(255) ),
Column( "type", Unicode(255) ),
Column( "value", TEXT ),
)
WorkflowRequestStepState_table = Table(
"workflow_request_step_states", metadata,
Column( "id", Integer, primary_key=True ),
Column( "workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id", onupdate="CASCADE", ondelete="CASCADE" )),
Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id" )),
Column( "value", JSONType ),
)
WorkflowRequestToInputDatasetAssociation_table = Table(
"workflow_request_to_input_dataset", metadata,
Column( "id", Integer, primary_key=True ),
Column( "name", String(255) ),
Column( "workflow_invocation_id", Integer, ForeignKey( "workflow_invocation.id" ), index=True ),
Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id") ),
Column( "dataset_id", Integer, ForeignKey( "history_dataset_association.id" ), index=True ),
)
WorkflowRequestToInputDatasetCollectionAssociation_table = Table(
"workflow_request_to_input_collection_dataset", metadata,
Column( "id", Integer, primary_key=True ),
Column( "name", String(255) ),
Column( "workflow_invocation_id", Integer, ForeignKey( "workflow_invocation.id" ), index=True ),
Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id") ),
Column( "dataset_collection_id", Integer, ForeignKey( "history_dataset_collection_association.id" ), index=True ),
)
TABLES = [
WorkflowRequestInputParameter_table,
WorkflowRequestStepState_table,
WorkflowRequestToInputDatasetAssociation_table,
WorkflowRequestToInputDatasetCollectionAssociation_table,
]
def upgrade(migrate_engine):
metadata.bind = migrate_engine
print __doc__
metadata.reflect()
for table in TABLES:
__create(table)
History_column = Column( "history_id", Integer, ForeignKey( "history.id" ), nullable=True )
State_column = Column( "state", TrimmedString( 64 ) )
# TODO: Handle indexes correctly
SchedulerId_column = Column( "scheduler", TrimmedString(255) )
HandlerId_column = Column( "handler", TrimmedString(255) )
WorkflowUUID_column = Column( "uuid", UUIDType, nullable=True )
__add_column( History_column, "workflow_invocation", metadata )
__add_column( State_column, "workflow_invocation", metadata )
__add_column( SchedulerId_column, "workflow_invocation", metadata, index_nane="id_workflow_invocation_scheduler" )
__add_column( HandlerId_column, "workflow_invocation", metadata, index_name="id_workflow_invocation_handler" )
__add_column( WorkflowUUID_column, "workflow_invocation", metadata )
# All previous invocations have been scheduled...
cmd = "UPDATE workflow_invocation SET state = 'scheduled'"
try:
migrate_engine.execute( cmd )
except Exception, e:
log.debug( "failed to update past workflow invocation states: %s" % ( str( e ) ) )
WorkflowInvocationStepAction_column = Column( "action", JSONType, nullable=True )
__add_column( WorkflowInvocationStepAction_column, "workflow_invocation_step", metadata )
def downgrade(migrate_engine):
metadata.bind = migrate_engine
metadata.reflect()
for table in TABLES:
__drop(table)
__drop_column( "state", "workflow_invocation", metadata )
__drop_column( "scheduler_id", "workflow_invocation", metadata )
__drop_column( "uuid", "workflow_invocation", metadata )
__drop_column( "history_id", "workflow_invocation", metadata )
__drop_column( "handler_id", "workflow_invocation", metadata )
__drop_column( "action", "workflow_invocation_step", metadata )
def __add_column(column, table_name, metadata, **kwds):
try:
table = Table( table_name, metadata, autoload=True )
column.create( table, **kwds )
except Exception as e:
print str(e)
log.exception( "Adding column %s column failed." % column)
def __drop_column( column_name, table_name, metadata ):
try:
table = Table( table_name, metadata, autoload=True )
getattr( table.c, column_name ).drop()
except Exception as e:
print str(e)
log.exception( "Dropping column %s failed." % column_name )
def __create(table):
try:
table.create()
except Exception as e:
print str(e)
log.exception("Creating %s table failed: %s" % (table.name, str( e ) ) )
def __drop(table):
try:
table.drop()
except Exception as e:
print str(e)
log.exception("Dropping %s table failed: %s" % (table.name, str( e ) ) )
+116 -4
View File
@@ -16,7 +16,7 @@ from galaxy.web.base.controller import BaseAPIController, url_for, UsesStoredWor
from galaxy.web.base.controller import UsesHistoryMixin
from galaxy.web.base.controller import SharableMixin
from galaxy.workflow.extract import extract_workflow
from galaxy.workflow.run import invoke
from galaxy.workflow.run import invoke, queue_invoke
from galaxy.workflow.run_request import build_workflow_run_config
log = logging.getLogger(__name__)
@@ -224,7 +224,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis
# invoke may throw MessageExceptions on tool erors, failure
# to match up inputs, etc...
outputs = invoke(
outputs, invocation = invoke(
trans=trans,
workflow=workflow,
workflow_run_config=run_config,
@@ -235,14 +235,19 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis
# Build legacy output - should probably include more information from
# outputs.
rval = {}
rval['history'] = trans.security.encode_id(history.id)
rval['history'] = trans.security.encode_id( history.id )
rval['outputs'] = []
for step in workflow.steps:
if step.type == 'tool' or step.type is None:
for v in outputs[ step.id ].itervalues():
rval[ 'outputs' ].append( trans.security.encode_id( v.id ) )
return rval
# Newer version of this API just returns the invocation as a dict, to
# facilitate migration - produce the newer style response and blend in
# the older information.
invocation_response = self.__encode_invocation( trans, invocation )
invocation_response.update( rval )
return invocation_response
@expose_api
def workflow_dict( self, trans, workflow_id, **kwd ):
@@ -369,6 +374,32 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis
item['url'] = url_for('workflow', id=encoded_id)
return item
@expose_api
def workflow_request( self, trans, workflow_id, payload, **kwd ):
"""
POST /api/workflows/{encoded_workflow_id}/usage
Schedule the workflow specified by `workflow_id` to run.
"""
# /usage is awkward in this context but is consistent with the rest of
# this module. Would prefer to redo it all to use /invocation(s).
# Get workflow + accessibility check.
stored_workflow = self.__get_stored_accessible_workflow( trans, workflow_id )
workflow = stored_workflow.latest_workflow
run_config = build_workflow_run_config( trans, workflow, payload )
workflow_scheduler_id = payload.get( "scheduler", None )
# TODO: workflow scheduler hints
work_request_params = dict( scheduler=workflow_scheduler_id )
workflow_invocation = queue_invoke(
trans=trans,
workflow=workflow,
workflow_run_config=run_config,
request_params=work_request_params
)
return self.encode_all_ids( trans, workflow_invocation.to_dict(), recursive=True )
@expose_api
def workflow_usage(self, trans, workflow_id, **kwd):
"""
@@ -407,6 +438,87 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis
return self.__encode_invocation( trans, workflow_invocation )
return None
@expose_api
def cancel_workflow_invocation(self, trans, workflow_id, usage_id, **kwd):
"""
DELETE /api/workflows/{workflow_id}/usage/{usage_id}
Cancel the specified workflow invocation.
:param workflow_id: the workflow id (required)
:type workflow_id: str
:param usage_id: the usage id (required)
:type usage_id: str
:raises: exceptions.MessageException, exceptions.ObjectNotFound
"""
decoded_workflow_invocation_id = self.__decode_id( trans, usage_id )
workflow_invocation = self.workflow_manager.cancel_invocation( trans, decoded_workflow_invocation_id )
return self.__encode_invocation( trans, workflow_invocation )
@expose_api
def workflow_invocation_step(self, trans, workflow_id, usage_id, step_id, **kwd):
"""
GET /api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}
:param workflow_id: the workflow id (required)
:type workflow_id: str
:param usage_id: the usage id (required)
:type usage_id: str
:param step_id: encoded id of the WorkflowInvocationStep (required)
:type step_id: str
:param payload: payload containing update action information
for running workflow.
:raises: exceptions.MessageException, exceptions.ObjectNotFound
"""
decoded_invocation_step_id = self.__decode_id( trans, step_id )
invocation_step = self.workflow_manager.get_invocation_step(
trans,
decoded_invocation_step_id
)
return self.__encode_invocation_step( trans, invocation_step )
@expose_api
def workflow_invocation_step_update(self, trans, workflow_id, usage_id, step_id, payload, **kwd):
"""
PUT /api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}
Update state of running workflow step invocation - still very nebulous
but this would be for stuff like confirming paused steps can proceed
etc....
:param workflow_id: the workflow id (required)
:type workflow_id: str
:param usage_id: the usage id (required)
:type usage_id: str
:param step_id: encoded id of the WorkflowInvocationStep (required)
:type step_id: str
:raises: exceptions.MessageException, exceptions.ObjectNotFound
"""
decoded_invocation_step_id = self.__decode_id( trans, step_id )
action = payload.get( "action", None )
invocation_step = self.workflow_manager.update_invocation_step(
trans,
decoded_invocation_step_id,
action=action,
)
return self.__encode_invocation_step( trans, invocation_step )
def __encode_invocation_step( self, trans, invocation_step ):
return self.encode_all_ids(
trans,
invocation_step.to_dict( 'element' ),
True
)
def __get_stored_accessible_workflow( self, trans, workflow_id ):
stored_workflow = self.__get_stored_workflow( trans, workflow_id )
+5
View File
@@ -227,7 +227,12 @@ def populate_api_routes( webapp, app ):
webapp.mapper.connect( 'import_shared_workflow_deprecated', '/api/workflows/import', controller='workflows', action='import_shared_workflow_deprecated', conditions=dict( method=['POST'] ) )
webapp.mapper.connect( 'workflow_usage', '/api/workflows/{workflow_id}/usage', controller='workflows', action='workflow_usage', conditions=dict(method=['GET']))
webapp.mapper.connect( 'workflow_usage_contents', '/api/workflows/{workflow_id}/usage/{usage_id}', controller='workflows', action='workflow_usage_contents', conditions=dict(method=['GET']))
webapp.mapper.connect( 'cancel_workflow_invocation', '/api/workflows/{workflow_id}/usage/{usage_id}', controller='workflows', action='cancel_workflow_invocation', conditions=dict(method=['DELETE']))
webapp.mapper.connect( 'workflow_invocation_step', '/api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}', controller='workflows', action='workflow_invocation_step', conditions=dict(method=['GET']))
webapp.mapper.connect( 'workflow_invocation_step_update', '/api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}', controller='workflows', action='workflow_invocation_step_update', conditions=dict(method=['PUT']))
webapp.mapper.connect( 'workflow_request', '/api/workflows/{workflow_id}/usage', controller='workflows', action='workflow_request', conditions=dict( method=['POST'] ) )
# ============================
# ===== AUTHENTICATE API =====
# ============================
@@ -1295,7 +1295,7 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix
copy_inputs_to_history=new_history is not None
)
outputs = invoke(
outputs, invocation = invoke(
trans=trans,
workflow=workflow,
workflow_run_config=run_config
View File
+46
View File
@@ -0,0 +1,46 @@
from galaxy.managers.context import (
ProvidesAppContext,
ProvidesUserContext,
ProvidesHistoryContext
)
class WorkRequestContext( ProvidesAppContext, ProvidesUserContext, ProvidesHistoryContext ):
""" Stripped down implementation of Galaxy web transaction god object for
work request handling outside of web threads - uses mix-ins shared with
GalaxyWebTransaction to provide app, user, and history context convience
methods - but nothing related to HTTP handling, mako views, etc....
Things that only need app shouldn't be consuming trans - but there is a
need for actions potentially tied to users and histories and hopefully
this can define that stripped down interface providing access to user and
history information - but not dealing with web request and response
objects.
"""
def __init__( self, app, user=None, history=None ):
self.app = app
self.security = app.security
self.__user = user
self.__history = history
self.api_inherit_admin = False
def get_history( self, create=False ):
if create:
raise NotImplementedError( "Cannot create histories from a work request context." )
return self.__history
def set_history( self ):
raise NotImplementedError( "Cannot change histories from a work request context." )
history = property( get_history, set_history )
def get_user( self ):
"""Return the current user if logged in or None."""
return self.__user
def set_user( self, user ):
"""Set the current user."""
raise NotImplementedError( "Cannot change users from a work request context." )
user = property( get_user, set_user )
+69 -1
View File
@@ -171,6 +171,21 @@ class WorkflowModule( object ):
"""
raise TypeError( "Abstract method" )
def do_invocation_step_action( self, step, action ):
""" Update or set the workflow invocation state action - generic
extension point meant to allows users to interact with interactive
workflow modules. The action object returned from this method will
be attached to the WorkflowInvocationStep and be available the next
time the workflow scheduler visits the workflow.
"""
raise exceptions.RequestParameterInvalidException( "Attempting to perform invocation step action on module that does not support actions." )
def recover_mapping( self, step, step_invocations, progress ):
""" Re-populate progress object with information about connections
from previously executed steps recorded via step_invocations.
"""
raise TypeError( "Abstract method" )
class InputModule( WorkflowModule ):
@@ -240,6 +255,19 @@ class InputModule( WorkflowModule ):
state.inputs = dict( input=None )
return state
def recover_runtime_state( self, runtime_state ):
""" Take secure runtime state from persisted invocation and convert it
into a DefaultToolState object for use during workflow invocation.
"""
fake_tool = Bunch( inputs=self.get_runtime_inputs() )
state = galaxy.tools.DefaultToolState()
state.decode( runtime_state, fake_tool, self.trans.app, secure=False )
return state
def normalize_runtime_state( self, runtime_state ):
fake_tool = Bunch( inputs=self.get_runtime_inputs() )
return runtime_state.encode( fake_tool, self.trans.app, secure=False )
def encode_runtime_state( self, trans, state ):
fake_tool = Bunch( inputs=self.get_runtime_inputs() )
return state.encode( fake_tool, trans.app )
@@ -291,6 +319,9 @@ class InputModule( WorkflowModule ):
progress.set_outputs_for_input( step, step_outputs )
return job
def recover_mapping( self, step, step_invocations, progress ):
progress.set_outputs_for_input( step )
class InputDataModule( InputModule ):
type = "data_input"
@@ -439,6 +470,18 @@ class ToolModule( WorkflowModule ):
)
self.state.inputs = self.tool.params_from_strings( state, app, **params_from_kwds )
def recover_runtime_state( self, runtime_state ):
""" Take secure runtime state from persisted invocation and convert it
into a DefaultToolState object for use during workflow invocation.
"""
state = galaxy.tools.DefaultToolState()
app = self.trans.app
state.decode( runtime_state, self.tool, app, secure=False )
return state
def normalize_runtime_state( self, runtime_state ):
return runtime_state.encode( self.tool, self.trans.app, secure=False )
@classmethod
def __get_tool_version( cls, trans, tool_id ):
# Return a ToolVersion if one exists for tool_id.
@@ -664,7 +707,7 @@ class ToolModule( WorkflowModule ):
param_combinations=param_combinations,
history=invocation.history,
collection_info=collection_info,
workflow_invocation_uuid=invocation.uuid
workflow_invocation_uuid=invocation.uuid.hex
)
if collection_info:
step_outputs = dict( execution_tracker.created_collections )
@@ -731,6 +774,23 @@ class ToolModule( WorkflowModule ):
visit_input_values( self.tool.inputs, self.state.inputs, callback )
def recover_mapping( self, step, step_invocations, progress ):
# Grab a job representing this invocation - for normal workflows
# there will be just one job but if this step was mapped over there
# may be many.
job_0 = step_invocations[ 0 ].job
outputs = {}
for job_output in job_0.output_datasets:
replacement_name = job_output.name
replacement_value = job_output.dataset
# If was a mapping step, grab the output mapped collection for
# replacement instead.
if replacement_value.hidden_beneath_collection_instance:
replacement_value = replacement_value.hidden_beneath_collection_instance
outputs[ replacement_name ] = replacement_value
progress.set_step_outputs( step, outputs )
class WorkflowModuleFactory( object ):
@@ -796,6 +856,14 @@ class MissingToolException( Exception ):
module is missing. """
class DelayedWorkflowEvaluation(Exception):
pass
class CancelWorkflowEvaluation(Exception):
pass
class WorkflowModuleInjector(object):
""" Injects workflow step objects from the ORM with appropriate module and
module generated/influenced state. """
+105 -22
View File
@@ -6,54 +6,110 @@ from galaxy import util
from galaxy.util.odict import odict
from galaxy.workflow import modules
from galaxy.workflow.run_request import WorkflowRunConfig
from galaxy.workflow.run_request import workflow_run_config_to_request
import logging
log = logging.getLogger( __name__ )
def invoke( trans, workflow, workflow_run_config, populate_state=False ):
def invoke( trans, workflow, workflow_run_config, workflow_invocation=None, populate_state=False ):
""" Run the supplied workflow in the supplied target_history.
"""
if populate_state:
modules.populate_module_and_state( trans, workflow, workflow_run_config.param_map )
return WorkflowInvoker(
invoker = WorkflowInvoker(
trans,
workflow,
workflow_run_config,
).invoke()
workflow_invocation=workflow_invocation,
)
try:
outputs = invoker.invoke()
except modules.CancelWorkflowEvaluation:
if workflow_invocation:
if workflow_invocation.cancel():
trans.sa_session.add( workflow_invocation )
outputs = []
except Exception:
log.exception("Failed to execute scheduled workflow.")
if workflow_invocation:
# Running workflow invocation in background, just mark
# persistent workflow invocation as failed.
workflow_invocation.fail()
trans.sa_session.add( workflow_invocation )
else:
# Running new transient workflow invocation in legacy
# controller action - propage the exception up.
raise
outputs = []
if workflow_invocation:
# Be sure to update state of workflow_invocation.
trans.sa_session.flush()
return outputs, invoker.workflow_invocation
def queue_invoke( trans, workflow, workflow_run_config, request_params ):
modules.populate_module_and_state( trans, workflow, workflow_run_config.param_map )
workflow_invocation = workflow_run_config_to_request( trans, workflow_run_config, workflow )
workflow_invocation.workflow = workflow
return trans.app.workflow_scheduling_manager.queue(
workflow_invocation,
request_params
)
class WorkflowInvoker( object ):
def __init__( self, trans, workflow, workflow_run_config ):
def __init__( self, trans, workflow, workflow_run_config, workflow_invocation=None ):
self.trans = trans
self.workflow = workflow
workflow_invocation = model.WorkflowInvocation()
workflow_invocation.workflow = self.workflow
self.workflow_invocation = workflow_invocation
self.progress = WorkflowProgress( self.workflow_invocation, workflow_run_config.inputs )
if workflow_invocation is None:
invocation_uuid = uuid.uuid1()
invocation_uuid = uuid.uuid1().hex
workflow_invocation = model.WorkflowInvocation()
workflow_invocation.workflow = self.workflow
# In one way or another, following attributes will become persistent
# so they are available during delayed/revisited workflow scheduling.
workflow_invocation.uuid = invocation_uuid
workflow_invocation.history = workflow_run_config.target_history
self.workflow_invocation = workflow_invocation
else:
self.workflow_invocation = workflow_invocation
# In one way or another, following attributes will become persistent
# so they are available during delayed/revisited workflow scheduling.
self.workflow_invocation.uuid = invocation_uuid
self.workflow_invocation.history = workflow_run_config.target_history
self.workflow_invocation.copy_inputs_to_history = workflow_run_config.copy_inputs_to_history
self.workflow_invocation.replacement_dict = workflow_run_config.replacement_dict
module_injector = modules.WorkflowModuleInjector( trans )
self.progress = WorkflowProgress( self.workflow_invocation, workflow_run_config.inputs, module_injector )
def invoke( self ):
workflow_invocation = self.workflow_invocation
remaining_steps = self.progress.remaining_steps()
delayed_steps = False
for step in remaining_steps:
jobs = self._invoke_step( step )
for job in util.listify( jobs ):
# Record invocation
workflow_invocation_step = model.WorkflowInvocationStep()
workflow_invocation_step.workflow_invocation = workflow_invocation
workflow_invocation_step.workflow_step = step
workflow_invocation_step.job = job
jobs = None
try:
jobs = self._invoke_step( step )
for job in (util.listify( jobs ) or [None]):
# Record invocation
workflow_invocation_step = model.WorkflowInvocationStep()
workflow_invocation_step.workflow_invocation = workflow_invocation
workflow_invocation_step.workflow_step = step
workflow_invocation_step.job = job
except modules.DelayedWorkflowEvaluation:
delayed_steps = True
self.progress.mark_step_outputs_delayed( step )
if delayed_steps:
state = model.WorkflowInvocation.states.READY
else:
state = model.WorkflowInvocation.states.SCHEDULED
workflow_invocation.state = state
# All jobs ran successfully, so we can save now
self.trans.sa_session.add( workflow_invocation )
@@ -66,18 +122,35 @@ class WorkflowInvoker( object ):
jobs = step.module.execute( self.trans, self.progress, self.workflow_invocation, step )
return jobs
STEP_OUTPUT_DELAYED = object()
class WorkflowProgress( object ):
def __init__( self, workflow_invocation, inputs_by_step_id ):
def __init__( self, workflow_invocation, inputs_by_step_id, module_injector ):
self.outputs = odict()
self.module_injector = module_injector
self.workflow_invocation = workflow_invocation
self.inputs_by_step_id = inputs_by_step_id
def remaining_steps(self):
# Previously computed and persisted step states.
step_states = self.workflow_invocation.step_states_by_step_id()
steps = self.workflow_invocation.workflow.steps
remaining_steps = []
step_invocations_by_id = self.workflow_invocation.step_invocations_by_step_id()
for step in steps:
if not hasattr( step, 'module' ):
self.module_injector.inject( step )
runtime_state = step_states[ step.id ].value
step.state = step.module.recover_runtime_state( runtime_state )
return steps
invocation_steps = step_invocations_by_id.get( step.id, None )
if invocation_steps:
self._recover_mapping( step, invocation_steps )
else:
remaining_steps.append( step )
return remaining_steps
def replacement_for_tool_input( self, step, input, prefixed_name ):
""" For given workflow 'step' that has had input_connections_by_name
@@ -100,6 +173,8 @@ class WorkflowProgress( object ):
def replacement_for_connection( self, connection ):
step_outputs = self.outputs[ connection.output_step.id ]
if step_outputs is STEP_OUTPUT_DELAYED:
raise modules.DelayedWorkflowEvaluation()
return step_outputs[ connection.output_name ]
def set_outputs_for_input( self, step, outputs={} ):
@@ -111,5 +186,13 @@ class WorkflowProgress( object ):
def set_step_outputs(self, step, outputs):
self.outputs[ step.id ] = outputs
def mark_step_outputs_delayed(self, step):
self.outputs[ step.id ] = STEP_OUTPUT_DELAYED
def _recover_mapping( self, step, step_invocations ):
try:
step.module.recover_mapping( step, step_invocations, self )
except modules.DelayedWorkflowEvaluation:
self.mark_step_outputs_delayed( step )
__all__ = [ invoke, WorkflowRunConfig ]
+96
View File
@@ -1,9 +1,15 @@
import uuid
from galaxy import exceptions
from galaxy import model
from galaxy.managers import histories
INPUT_STEP_TYPES = [ 'data_input', 'data_collection_input' ]
import logging
log = logging.getLogger( __name__ )
class WorkflowRunConfig( object ):
""" Wrapper around all the ways a workflow execution can be parameterized.
@@ -231,6 +237,96 @@ def build_workflow_run_config( trans, workflow, payload ):
return run_config
def workflow_run_config_to_request( trans, run_config, workflow ):
param_types = model.WorkflowRequestInputParameter.types
workflow_invocation = model.WorkflowInvocation()
workflow_invocation.uuid = uuid.uuid1()
workflow_invocation.history = run_config.target_history
def add_parameter( name, value, type ):
parameter = model.WorkflowRequestInputParameter(
name=name,
value=value,
type=type,
)
workflow_invocation.input_parameters.append( parameter )
replacement_dict = run_config.replacement_dict
for name, value in replacement_dict.iteritems():
add_parameter(
name=name,
value=value,
type=param_types.REPLACEMENT_PARAMETERS,
)
for step_id, content in run_config.inputs.iteritems():
if content.history_content_type == "dataset":
request_to_content = model.WorkflowRequestToInputDatasetAssociation()
request_to_content.dataset = content
request_to_content.workflow_step_id = step_id
workflow_invocation.input_datasets.append( request_to_content )
else:
request_to_content = model.WorkflowRequestToInputDatasetCollectionAssociation()
request_to_content.dataset_collection = content
request_to_content.workflow_step_id = step_id
workflow_invocation.input_dataset_collections.append( request_to_content )
for step in workflow.steps:
state = step.state
serializable_runtime_state = step.module.normalize_runtime_state( state )
step_state = model.WorkflowRequestStepState()
step_state.workflow_step_id = step.id
step_state.value = serializable_runtime_state
workflow_invocation.step_states.append( step_state )
add_parameter( "copy_inputs_to_history", "true" if run_config.copy_inputs_to_history else "false", param_types.META_PARAMETERS )
return workflow_invocation
def workflow_request_to_run_config( work_request_context, workflow_invocation ):
param_types = model.WorkflowRequestInputParameter.types
history = workflow_invocation.history
replacement_dict = {}
inputs = {}
param_map = {}
copy_inputs_to_history = None
for parameter in workflow_invocation.input_parameters:
parameter_type = parameter.type
if parameter_type == param_types.REPLACEMENT_PARAMETERS:
replacement_dict[ parameter.name ] = parameter.value
elif parameter_type == param_types.META_PARAMETERS:
if parameter.name == "copy_inputs_to_history":
copy_inputs_to_history = (parameter.value == "true")
#for parameter in workflow_invocation.step_parameters:
# step_id = parameter.workflow_step_id
# if step_id not in param_map:
# param_map[ step_id ] = {}
# param_map[ step_id ][ parameter.name ] = parameter.value
for input_association in workflow_invocation.input_datasets:
inputs[ input_association.workflow_step_id ] = input_association.dataset
for input_association in workflow_invocation.input_dataset_collections:
inputs[ input_association.workflow_step_id ] = input_association.dataset_collection
if copy_inputs_to_history is None:
raise exceptions.InconsistentDatabase("Failed to find copy_inputs_to_history parameter loading workflow_invocation from database.")
workflow_run_config = WorkflowRunConfig(
target_history=history,
replacement_dict=replacement_dict,
inputs=inputs,
param_map=param_map,
copy_inputs_to_history=copy_inputs_to_history,
)
return workflow_run_config
def __decode_id( trans, workflow_id, model_type="workflow" ):
try:
return trans.security.decode_id( workflow_id )
@@ -0,0 +1,41 @@
""" Module containing Galaxy workflow scheduling plugins. Galaxy's interface
for workflow scheduling is highly experimental and the interface required for
scheduling plugins will almost certainly change.
"""
from abc import ABCMeta
from abc import abstractmethod
class WorkflowSchedulingPlugin( object ):
""" A plugin defining how Galaxy should schedule plugins. By default
plugins are passive and should monitor Galaxy's work queue for
WorkflowRequests. Inherit from ActiveWorkflowSchedulingPlugin instead if
the scheduling plugin should be forced (i.e. if scheduling happen all at
once or the request will be stored and monitored outside of Galaxy.)
"""
__metaclass__ = ABCMeta
@property
@abstractmethod
def plugin_type( self ):
""" Short string providing labelling this plugin """
def startup( self, app ):
""" Called when Galaxy starts up if the plugin is enabled.
"""
def shutdown( self ):
""" Called when Galaxy is shutting down, workflow scheduling should
end.
"""
class ActiveWorkflowSchedulingPlugin( WorkflowSchedulingPlugin ):
__metaclass__ = ABCMeta
@abstractmethod
def schedule( self, workflow_invocation ):
""" Optionally return one or more commands to instrument job. These
commands will be executed on the compute server prior to the job
running.
"""
+46
View File
@@ -0,0 +1,46 @@
""" The class defines the stock Galaxy workflow scheduling plugin - currently
it simply schedules the whole workflow up front when offered.
"""
from ..schedulers import ActiveWorkflowSchedulingPlugin
from galaxy.work import context
from galaxy.workflow import run
from galaxy.workflow import run_request
import logging
log = logging.getLogger( __name__ )
class CoreWorkflowSchedulingPlugin( ActiveWorkflowSchedulingPlugin ):
plugin_type = "core"
def __init__( self, **kwds ):
pass
def startup( self, app ):
self.app = app
def shutdown( self ):
pass
def schedule( self, workflow_invocation ):
workflow = workflow_invocation.workflow
history = workflow_invocation.history
request_context = context.WorkRequestContext(
app=self.app,
history=history,
user=history.user
) # trans-like object not tied to a web-thread.
workflow_run_config = run_request.workflow_request_to_run_config(
request_context,
workflow_invocation
)
run.invoke(
trans=request_context,
workflow=workflow,
workflow_run_config=workflow_run_config,
workflow_invocation=workflow_invocation,
)
__all__ = [ CoreWorkflowSchedulingPlugin ]
+197
View File
@@ -0,0 +1,197 @@
import os
import time
import logging
import threading
from xml.etree import ElementTree
from galaxy import model
from galaxy.util import plugin_config
import galaxy.workflow.schedulers
log = logging.getLogger( __name__ )
DEFAULT_SCHEDULER_ID = "default" # well actually this should be called DEFAULT_DEFAULT_SCHEDULER_ID...
DEFAULT_SCHEDULER_PLUGIN_TYPE = "core"
EXCEPTION_MESSAGE_SHUTDOWN = "Exception raised while attempting to shutdown workflow scheduler."
EXCEPTION_MESSAGE_NO_SCHEDULERS = "Failed to defined workflow schedulers - no workflow schedulers defined."
EXCEPTION_MESSAGE_NO_DEFAULT_SCHEDULER = "Failed to defined workflow schedulers - no workflow scheduler found for default id '%s'."
EXCEPTION_MESSAGE_DUPLICATE_SCHEDULERS = "Failed to defined workflow schedulers - workflow scheduling plugin id '%s' duplicated."
class WorkflowSchedulingManager( object ):
""" A workflow scheduling manager based loosely on pattern established by
``galaxy.manager.JobManager``. Only schedules workflows on handler
processes.
"""
def __init__( self, app ):
self.app = app
self.__job_config = app.job_config
self.workflow_schedulers = {}
self.active_workflow_schedulers = {} # Passive workflow schedulers
# won't need to be monitored I
# guess.
self.request_monitor = None
self.__plugin_classes = self.__plugins_dict()
self.__init_schedulers()
if self._is_workflow_handler():
log.debug("Starting workflow schedulers")
self.__start_schedulers()
if self.active_workflow_schedulers:
self.__start_request_monitor()
else:
# Process should not schedule workflows - do nothing.
pass
# Provide a handler config-like interface by delegating to job handler
# config. Perhaps it makes sense to let there be explicit workflow
# handlers?
def _is_workflow_handler( self ):
return self.app.is_job_handler()
def _get_handler( self ):
return self.__job_config.get_handler( None )
def shutdown( self ):
for workflow_scheduler in self.workflow_schedulers.itervalues():
try:
workflow_scheduler.shutdown()
except Exception:
log.exception( EXCEPTION_MESSAGE_SHUTDOWN )
if self.request_monitor:
try:
self.request_monitor.shutdown()
except Exception:
log.exception( "Failed to shutdown worklfow request monitor." )
def queue( self, workflow_invocation, request_params ):
workflow_invocation.state = model.WorkflowInvocation.states.NEW
scheduler = request_params.get( "scheduler", None ) or self.default_scheduler_id
handler = self._get_handler()
workflow_invocation.scheduler = scheduler
workflow_invocation.handler = handler
sa_session = self.app.model.context
sa_session.add( workflow_invocation )
sa_session.flush()
return workflow_invocation
def __start_schedulers( self ):
for workflow_scheduler in self.workflow_schedulers.itervalues():
workflow_scheduler.startup( self.app )
def __plugins_dict( self ):
return plugin_config.plugins_dict( galaxy.workflow.schedulers, 'plugin_type' )
def __init_schedulers( self ):
config_file = self.app.config.workflow_schedulers_config_file
use_default_scheduler = False
if not config_file:
log.info( "Not workflow schedulers plugin config file defined, using default scheduler." )
use_default_scheduler = True
elif not os.path.exists( config_file ):
log.info( "Cannot find workflow schedulers plugin config file '%s', using default scheduler." % config_file )
use_default_scheduler = True
if use_default_scheduler:
self.__init_default_scheduler()
else:
plugins_element = ElementTree.parse( config_file ).getroot()
self.__init_schedulers_for_element( plugins_element )
def __init_default_scheduler( self ):
self.default_scheduler_id = DEFAULT_SCHEDULER_ID
self.__init_plugin( DEFAULT_SCHEDULER_PLUGIN_TYPE )
def __init_schedulers_for_element( self, plugins_element ):
plugins_kwds = dict( plugins_element.items() )
self.default_scheduler_id = plugins_kwds.get( 'default', DEFAULT_SCHEDULER_ID )
for plugin_element in plugins_element.getchildren():
plugin_type = plugin_element.tag
plugin_kwds = dict( plugin_element.items() )
plugin_kwds.update( self.extra_kwargs )
workflow_scheduler_id = plugin_kwds.get( 'id', None )
self.__init_plugin( plugin_type, workflow_scheduler_id, **plugin_kwds )
if not self.workflow_schedulers:
raise Exception( EXCEPTION_MESSAGE_NO_SCHEDULERS )
if self.default_scheduler_id not in self.workflow_schedulers:
raise Exception( EXCEPTION_MESSAGE_NO_DEFAULT_SCHEDULER % self.default_scheduler_id )
def __init_plugin( self, plugin_type, workflow_scheduler_id=None, **kwds ):
workflow_scheduler_id = workflow_scheduler_id or self.default_scheduler_id
if workflow_scheduler_id in self.workflow_schedulers:
raise Exception( EXCEPTION_MESSAGE_DUPLICATE_SCHEDULERS % workflow_scheduler_id )
workflow_scheduler = self.__plugin_classes[ plugin_type ]( **kwds )
self.workflow_schedulers[ workflow_scheduler_id ] = workflow_scheduler
if isinstance( workflow_scheduler, galaxy.workflow.schedulers.ActiveWorkflowSchedulingPlugin ):
self.active_workflow_schedulers[ workflow_scheduler_id ] = workflow_scheduler
def __start_request_monitor( self ):
self.request_monitor = WorkflowRequestMonitor( self.app, self )
class WorkflowRequestMonitor( object ):
def __init__( self, app, workflow_scheduling_manager ):
self.app = app
self.active = True
self.workflow_scheduling_manager = workflow_scheduling_manager
self.monitor_thread = threading.Thread( name="WorkflowRequestMonitor.monitor_thread", target=self.__monitor )
self.monitor_thread.setDaemon( True )
self.monitor_thread.start()
def __monitor( self ):
to_monitor = self.workflow_scheduling_manager.active_workflow_schedulers
while self.active:
for workflow_scheduler_id, workflow_scheduler in to_monitor.iteritems():
if not self.active:
return
self.__schedule( workflow_scheduler_id, workflow_scheduler )
time.sleep(1) # TODO: wake if stopped
def __schedule( self, workflow_scheduler_id, workflow_scheduler ):
invocation_ids = self.__active_invocation_ids( workflow_scheduler_id )
for invocation_id in invocation_ids:
self.__attempt_schedule( invocation_id, workflow_scheduler )
if not self.active:
return
def __attempt_schedule( self, invocation_id, workflow_scheduler ):
sa_session = self.app.model.context
workflow_invocation = sa_session.query( model.WorkflowInvocation ).get( invocation_id )
if not workflow_invocation or not workflow_invocation.active:
return False
try:
workflow_scheduler.schedule( workflow_invocation )
except Exception:
# TODO: eventually fail this - or fail it right away?
log.exception( "Exception raised while attempting to schedule workflow request." )
return False
# A workflow was obtained and scheduled...
return True
def __active_invocation_ids( self, scheduler_id ):
sa_session = self.app.model.context
handler = self.app.config.server_name
return model.WorkflowInvocation.poll_active_workflow_ids(
sa_session,
scheduler=scheduler_id,
handler=handler,
)
def shutdown( self ):
self.active = False
+28 -2
View File
@@ -135,9 +135,28 @@ class WorkflowsApiTestCase( api.ApiTestCase ):
# TODO: This should really be a post to workflows/<workflow_id>/run or
# something like that.
run_workflow_response = self._post( "workflows", data=workflow_request )
invocation_id = run_workflow_response.json()[ "id" ]
invocation = self._invocation_details( workflow_request[ "workflow_id" ], invocation_id )
assert invocation[ "state" ] == "scheduled", invocation
self._assert_status_code_is( run_workflow_response, 200 )
self.dataset_populator.wait_for_history( history_id, assert_ok=True )
def test_workflow_request( self ):
workflow = self.workflow_populator.load_workflow( name="test_for_queue" )
workflow_request, history_id = self._setup_workflow_run( workflow )
# TODO: This should really be a post to workflows/<workflow_id>/run or
# something like that.
url = "workflows/%s/request" % ( workflow_request[ "workflow_id" ] )
del workflow_request[ "workflow_id" ]
run_workflow_response = self._post( url, data=workflow_request )
self._assert_status_code_is( run_workflow_response, 200 )
# Give some time for workflow to get scheduled before scanning the history.
time.sleep( 5 )
self.dataset_populator.wait_for_history( history_id, assert_ok=True )
def test_cannot_run_inaccessible_workflow( self ):
workflow = self.workflow_populator.load_workflow( name="test_for_run_cannot_access" )
workflow_request, history_id = self._setup_workflow_run( workflow )
@@ -563,11 +582,18 @@ class WorkflowsApiTestCase( api.ApiTestCase ):
@skip_without_tool( "cat1" )
def test_invocation_usage( self ):
workflow_id, usage = self._run_workflow_once_get_invocation( "test_usage")
usage_details = self._invocation_details( workflow_id, usage[ "id" ] )
invocation_id = usage[ "id" ]
usage_details = self._invocation_details( workflow_id, invocation_id )
# Assert some high-level things about the structure of data returned.
self._assert_has_keys( usage_details, "inputs", "steps" )
for step in usage_details[ "steps" ]:
invocation_steps = usage_details[ "steps" ]
for step in invocation_steps:
self._assert_has_keys( step, "workflow_step_id", "order_index", "id" )
an_invocation_step = invocation_steps[ 0 ]
step_id = an_invocation_step[ "id" ]
step_response = self._get( "workflows/%s/usage/%s/steps/%s" % ( workflow_id, invocation_id, step_id ) )
self._assert_status_code_is( step_response, 200 )
self._assert_has_keys( step_response.json(), "id", "order_index" )
def _invocation_details( self, workflow_id, invocation_id ):
invocation_details_response = self._get( "workflows/%s/usage/%s" % ( workflow_id, invocation_id ) )
+49
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import unittest
import galaxy.model.mapping as mapping
import uuid
class MappingTests( unittest.TestCase ):
@@ -358,6 +359,54 @@ class MappingTests( unittest.TestCase ):
assert contents_iter_names( ids=[ d1.id, d3.id ] ) == [ "1", "3" ]
def test_workflows( self ):
model = self.model
user = model.User(
email="testworkflows@bx.psu.edu",
password="password"
)
stored_workflow = model.StoredWorkflow()
stored_workflow.user = user
workflow = model.Workflow()
workflow_step = model.WorkflowStep()
workflow.steps = [ workflow_step ]
workflow.stored_workflow = stored_workflow
self.persist( workflow )
assert workflow_step.id is not None
invocation_uuid = uuid.uuid1()
workflow_invocation = model.WorkflowInvocation()
workflow_invocation.uuid = invocation_uuid
workflow_invocation_step1 = model.WorkflowInvocationStep()
workflow_invocation_step1.workflow_invocation = workflow_invocation
workflow_invocation_step1.workflow_step = workflow_step
workflow_invocation_step2 = model.WorkflowInvocationStep()
workflow_invocation_step2.workflow_invocation = workflow_invocation
workflow_invocation_step2.workflow_step = workflow_step
workflow_invocation.workflow = workflow
h1 = model.History( name="WorkflowHistory1", user=user)
d1 = self.new_hda( h1, name="1" )
workflow_request_dataset = model.WorkflowRequestToInputDatasetAssociation()
workflow_request_dataset.workflow_invocation = workflow_invocation
workflow_request_dataset.workflow_step = workflow_step
workflow_request_dataset.dataset = d1
self.persist( workflow_invocation )
assert workflow_request_dataset is not None
assert workflow_invocation.id is not None
self.expunge()
loaded_invocation = self.query( model.WorkflowInvocation ).get( workflow_invocation.id )
assert loaded_invocation.uuid == invocation_uuid, "%s != %s" % (loaded_invocation.uuid, invocation_uuid)
assert loaded_invocation
assert len( loaded_invocation.steps ) == 2
def new_hda( self, history, **kwds ):
return history.add_dataset( self.model.HistoryDatasetAssociation( create_dataset=True, sa_session=self.model.session, **kwds ) )
View File