/lib/galaxy/jobs/deferred/__init__.py

https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 189 lines · 177 code · 4 blank · 8 comment · 30 complexity · 234f8256c3b4b5e7308c0f0cb40208ba MD5 · raw file

  1. """
  2. Queue for running deferred code via plugins.
  3. """
  4. import os, sys, logging, threading
  5. from Queue import Queue, Empty
  6. from galaxy import model
  7. from galaxy.util.bunch import Bunch
  8. log = logging.getLogger( __name__ )
  9. class DeferredJobQueue( object ):
  10. job_states = Bunch( READY = 'ready',
  11. WAIT = 'wait',
  12. INVALID = 'invalid' )
  13. def __init__( self, app ):
  14. self.app = app
  15. self.sa_session = app.model.context.current
  16. self.queue = Queue()
  17. self.plugins = {}
  18. self._load_plugins()
  19. self.sleeper = Sleeper()
  20. self.running = True
  21. self.waiting_jobs = []
  22. self.__check_jobs_at_startup()
  23. self.monitor_thread = threading.Thread( target=self.__monitor )
  24. self.monitor_thread.start()
  25. log.info( 'Deferred job queue started' )
  26. def _load_plugins( self ):
  27. for fname in os.listdir( os.path.dirname( __file__ ) ):
  28. if not fname.startswith( '_' ) and fname.endswith( '.py' ):
  29. name = fname[:-3]
  30. module_name = 'galaxy.jobs.deferred.' + name
  31. try:
  32. module = __import__( module_name )
  33. except:
  34. log.exception( 'Deferred job plugin appears to exist but is not loadable: %s' % module_name )
  35. continue
  36. for comp in module_name.split( "." )[1:]:
  37. module = getattr( module, comp )
  38. if '__all__' not in dir( module ):
  39. log.error( 'Plugin "%s" does not contain a list of exported classes in __all__' % module_name )
  40. continue
  41. for obj in module.__all__:
  42. display_name = ':'.join( ( module_name, obj ) )
  43. plugin = getattr( module, obj )
  44. for name in ( 'check_job', 'run_job' ):
  45. if name not in dir( plugin ):
  46. log.error( 'Plugin "%s" does not contain required method "%s()"' % ( display_name, name ) )
  47. break
  48. else:
  49. self.plugins[obj] = plugin( self.app )
  50. self.plugins[obj].job_states = self.job_states
  51. log.debug( 'Loaded deferred job plugin: %s' % display_name )
  52. def __check_jobs_at_startup( self ):
  53. waiting_jobs = self.sa_session.query( model.DeferredJob ) \
  54. .filter( model.DeferredJob.state == model.DeferredJob.states.WAITING ).all()
  55. for job in waiting_jobs:
  56. if not self.__check_job_plugin( job ):
  57. continue
  58. if 'check_interval' in dir( self.plugins[job.plugin] ):
  59. job.check_interval = self.plugins[job.plugin].check_interval
  60. log.info( 'Recovered deferred job (id: %s) at startup' % job.id )
  61. # Pass the job ID as opposed to the job, since the monitor thread
  62. # needs to load it in its own threadlocal scoped session.
  63. self.waiting_jobs.append( job.id )
  64. def __monitor( self ):
  65. while self.running:
  66. try:
  67. self.__monitor_step()
  68. except:
  69. log.exception( 'Exception in monitor_step' )
  70. self.sleeper.sleep( 1 )
  71. log.info( 'job queue stopped' )
  72. def __monitor_step( self ):
  73. # TODO: Querying the database with this frequency is bad, we need message passing
  74. new_jobs = self.sa_session.query( model.DeferredJob ) \
  75. .filter( model.DeferredJob.state == model.DeferredJob.states.NEW ).all()
  76. for job in new_jobs:
  77. if not self.__check_job_plugin( job ):
  78. continue
  79. job.state = model.DeferredJob.states.WAITING
  80. self.sa_session.add( job )
  81. self.sa_session.flush()
  82. if 'check_interval' in dir( self.plugins[job.plugin] ):
  83. job.check_interval = self.plugins[job.plugin].check_interval
  84. self.waiting_jobs.append( job )
  85. new_waiting = []
  86. for job in self.waiting_jobs:
  87. try:
  88. # Recovered jobs are passed in by ID
  89. assert type( job ) is int
  90. job = self.sa_session.query( model.DeferredJob ).get( job )
  91. except:
  92. pass
  93. if job.is_check_time:
  94. try:
  95. job_state = self.plugins[job.plugin].check_job( job )
  96. except Exception, e:
  97. self.__fail_job( job )
  98. log.exception( 'Set deferred job %s to error because of an exception in check_job(): %s' % ( job.id, str( e ) ) )
  99. continue
  100. if job_state == self.job_states.READY:
  101. try:
  102. self.plugins[job.plugin].run_job( job )
  103. except Exception, e:
  104. self.__fail_job( job )
  105. log.exception( 'Set deferred job %s to error because of an exception in run_job(): %s' % ( job.id, str( e ) ) )
  106. continue
  107. elif job_state == self.job_states.INVALID:
  108. self.__fail_job( job )
  109. log.error( 'Unable to run deferred job (id: %s): Plugin "%s" marked it as invalid' % ( job.id, job.plugin ) )
  110. continue
  111. else:
  112. new_waiting.append( job )
  113. job.last_check = 'now'
  114. else:
  115. new_waiting.append( job )
  116. self.waiting_jobs = new_waiting
  117. def __check_job_plugin( self, job ):
  118. if job.plugin not in self.plugins:
  119. log.error( 'Invalid deferred job plugin: %s' ) % job.plugin
  120. job.state = model.DeferredJob.states.ERROR
  121. self.sa_session.add( job )
  122. self.sa_session.flush()
  123. return False
  124. return True
  125. def __check_if_ready_to_run( self, job ):
  126. return self.plugins[job.plugin].check_job( job )
  127. def __fail_job( self, job ):
  128. job.state = model.DeferredJob.states.ERROR
  129. self.sa_session.add( job )
  130. self.sa_session.flush()
  131. def shutdown( self ):
  132. self.running = False
  133. self.sleeper.wake()
  134. class Sleeper( object ):
  135. """
  136. Provides a 'sleep' method that sleeps for a number of seconds *unless*
  137. the notify method is called (from a different thread).
  138. """
  139. def __init__( self ):
  140. self.condition = threading.Condition()
  141. def sleep( self, seconds ):
  142. self.condition.acquire()
  143. self.condition.wait( seconds )
  144. self.condition.release()
  145. def wake( self ):
  146. self.condition.acquire()
  147. self.condition.notify()
  148. self.condition.release()
  149. class FakeTrans( object ):
  150. """A fake trans for calling the external set metadata tool"""
  151. def __init__( self, app, history=None, user=None):
  152. class Dummy( object ):
  153. def __init__( self ):
  154. self.id = None
  155. self.app = app
  156. self.sa_session = app.model.context.current
  157. self.dummy = Dummy()
  158. if not history:
  159. self.history = Dummy()
  160. else:
  161. self.history = history
  162. if not user:
  163. self.user = Dummy()
  164. else:
  165. self.user = user
  166. self.model = app.model
  167. def get_galaxy_session( self ):
  168. return self.dummy
  169. def log_event( self, message, tool_id=None ):
  170. pass
  171. def get_current_user_roles( self ):
  172. if self.user:
  173. return self.user.all_roles()
  174. else:
  175. return []
  176. def db_dataset_for( self, dbkey ):
  177. if self.history is None:
  178. return None
  179. datasets = self.sa_session.query( self.app.model.HistoryDatasetAssociation ) \
  180. .filter_by( deleted=False, history_id=self.history.id, extension="len" )
  181. for ds in datasets:
  182. if dbkey == ds.dbkey:
  183. return ds
  184. return None