You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

181 rivejä
4.8 KiB

  1. from __future__ import unicode_literals
  2. import redis
  3. from rq import Connection, Queue, Worker
  4. from frappe.utils import cstr
  5. from collections import defaultdict
  6. import frappe
  7. import MySQLdb
  8. import os, socket, time
  9. default_timeout = 300
  10. queue_timeout = {
  11. 'long': 1500,
  12. 'default': 300,
  13. 'short': 300
  14. }
  15. def enqueue(method, queue='default', timeout=300, event=None,
  16. async=True, job_name=None, **kwargs):
  17. '''
  18. Enqueue method to be executed using a background worker
  19. :param method: method string or method object
  20. :param queue: should be either long, default or short
  21. :param timeout: should be set according to the functions
  22. :param event: this is passed to enable clearing of jobs from queues
  23. :param async: if async=False, the method is executed immediately, else via a worker
  24. :param job_name: can be used to name an enqueue call, which can be used to prevent duplicate calls
  25. :param kwargs: keyword arguments to be passed to the method
  26. '''
  27. q = get_queue(queue, async=async)
  28. if not timeout:
  29. timeout = queue_timeout.get(queue) or 300
  30. return q.enqueue_call(execute_job, timeout=timeout,
  31. kwargs={
  32. "site": frappe.local.site,
  33. "user": frappe.session.user,
  34. "method": method,
  35. "event": event,
  36. "job_name": job_name or cstr(method),
  37. "async": async,
  38. "kwargs": kwargs
  39. })
  40. def execute_job(site, method, event, job_name, kwargs, user=None, async=True, retry=0):
  41. '''Executes job in a worker, performs commit/rollback and logs if there is any error'''
  42. from frappe.utils.scheduler import log
  43. if async:
  44. frappe.connect(site)
  45. if user:
  46. frappe.set_user(user)
  47. if isinstance(method, basestring):
  48. method_name = method
  49. method = frappe.get_attr(method)
  50. else:
  51. method_name = cstr(method.__name__)
  52. try:
  53. method(**kwargs)
  54. except (MySQLdb.OperationalError, frappe.RetryBackgroundJobError), e:
  55. frappe.db.rollback()
  56. if (retry < 5 and
  57. (isinstance(e, frappe.RetryBackgroundJobError) or e.args[0] in (1213, 1205))):
  58. # retry the job if
  59. # 1213 = deadlock
  60. # 1205 = lock wait timeout
  61. # or RetryBackgroundJobError is explicitly raised
  62. frappe.destroy()
  63. time.sleep(retry+1)
  64. return execute_job(site, method, event, job_name, kwargs,
  65. async=async, retry=retry+1)
  66. else:
  67. log(method_name, message=repr(locals()))
  68. raise
  69. except:
  70. frappe.db.rollback()
  71. log(method_name, message=repr(locals()))
  72. raise
  73. else:
  74. frappe.db.commit()
  75. finally:
  76. if async:
  77. frappe.destroy()
  78. def start_worker(queue=None):
  79. '''Wrapper to start rq worker. Connects to redis and monitors these queues.'''
  80. with frappe.init_site():
  81. # empty init is required to get redis_queue from common_site_config.json
  82. redis_connection = get_redis_conn()
  83. with Connection(redis_connection):
  84. queues = get_queue_list(queue)
  85. Worker(queues, name=get_worker_name(queue)).work()
  86. def get_worker_name(queue):
  87. '''When limiting worker to a specific queue, also append queue name to default worker name'''
  88. name = None
  89. if queue:
  90. # hostname.pid is the default worker name
  91. name = '{hostname}.{pid}.{queue}'.format(
  92. hostname=socket.gethostname(),
  93. pid=os.getpid(),
  94. queue=queue)
  95. return name
  96. def get_jobs(site=None, queue=None, key='method'):
  97. '''Gets jobs per queue or per site or both'''
  98. jobs_per_site = defaultdict(list)
  99. for queue in get_queue_list(queue):
  100. q = get_queue(queue)
  101. for job in q.jobs:
  102. if job.kwargs.get('site'):
  103. if site is None:
  104. # get jobs for all sites
  105. jobs_per_site[job.kwargs['site']].append(job.kwargs[key])
  106. elif job.kwargs['site'] == site:
  107. # get jobs only for given site
  108. jobs_per_site[site].append(job.kwargs[key])
  109. else:
  110. print 'No site found in job', job.__dict__
  111. return jobs_per_site
  112. def get_queue_list(queue_list=None):
  113. '''Defines possible queues. Also wraps a given queue in a list after validating.'''
  114. default_queue_list = queue_timeout.keys()
  115. if queue_list:
  116. if isinstance(queue_list, basestring):
  117. queue_list = [queue_list]
  118. for queue in queue_list:
  119. validate_queue(queue, default_queue_list)
  120. return queue_list
  121. else:
  122. return default_queue_list
  123. def get_queue(queue, async=True):
  124. '''Returns a Queue object tied to a redis connection'''
  125. validate_queue(queue)
  126. return Queue(queue, connection=get_redis_conn(), async=async)
  127. def validate_queue(queue, default_queue_list=None):
  128. if not default_queue_list:
  129. default_queue_list = queue_timeout.keys()
  130. if queue not in default_queue_list:
  131. frappe.throw("Queue should be one of {0}".format(', '.join(default_queue_list)))
  132. def get_redis_conn():
  133. if not hasattr(frappe.local, 'conf'):
  134. raise Exception('You need to call frappe.init')
  135. elif not frappe.local.conf.redis_queue:
  136. raise Exception('redis_queue missing in common_site_config.json')
  137. return redis.from_url(frappe.local.conf.redis_queue)
  138. def enqueue_test_job():
  139. enqueue('frappe.utils.background_jobs.test_job', s=100)
  140. def test_job(s):
  141. import time
  142. print 'sleeping...'
  143. time.sleep(s)