您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

317 行
9.5 KiB

  1. import os
  2. import socket
  3. import time
  4. from functools import lru_cache
  5. from uuid import uuid4
  6. from collections import defaultdict
  7. from typing import List
  8. import redis
  9. from redis.exceptions import BusyLoadingError, ConnectionError
  10. from rq import Connection, Queue, Worker
  11. from rq.logutils import setup_loghandlers
  12. from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
  13. import frappe
  14. from frappe import _
  15. import frappe.monitor
  16. from frappe.utils import cstr, get_bench_id
  17. from frappe.utils.redis_queue import RedisQueue
  18. from frappe.utils.commands import log
  19. @lru_cache()
  20. def get_queues_timeout():
  21. common_site_config = frappe.get_conf()
  22. custom_workers_config = common_site_config.get("workers", {})
  23. default_timeout = 300
  24. return {
  25. "default": default_timeout,
  26. "short": default_timeout,
  27. "long": 1500,
  28. **{
  29. worker: config.get("timeout", default_timeout)
  30. for worker, config in custom_workers_config.items()
  31. }
  32. }
  33. redis_connection = None
  34. def enqueue(method, queue='default', timeout=None, event=None,
  35. is_async=True, job_name=None, now=False, enqueue_after_commit=False, **kwargs):
  36. '''
  37. Enqueue method to be executed using a background worker
  38. :param method: method string or method object
  39. :param queue: should be either long, default or short
  40. :param timeout: should be set according to the functions
  41. :param event: this is passed to enable clearing of jobs from queues
  42. :param is_async: if is_async=False, the method is executed immediately, else via a worker
  43. :param job_name: can be used to name an enqueue call, which can be used to prevent duplicate calls
  44. :param now: if now=True, the method is executed via frappe.call
  45. :param kwargs: keyword arguments to be passed to the method
  46. '''
  47. # To handle older implementations
  48. is_async = kwargs.pop('async', is_async)
  49. if now or frappe.flags.in_migrate:
  50. return frappe.call(method, **kwargs)
  51. q = get_queue(queue, is_async=is_async)
  52. if not timeout:
  53. timeout = get_queues_timeout().get(queue) or 300
  54. queue_args = {
  55. "site": frappe.local.site,
  56. "user": frappe.session.user,
  57. "method": method,
  58. "event": event,
  59. "job_name": job_name or cstr(method),
  60. "is_async": is_async,
  61. "kwargs": kwargs
  62. }
  63. if enqueue_after_commit:
  64. if not frappe.flags.enqueue_after_commit:
  65. frappe.flags.enqueue_after_commit = []
  66. frappe.flags.enqueue_after_commit.append({
  67. "queue": queue,
  68. "is_async": is_async,
  69. "timeout": timeout,
  70. "queue_args":queue_args
  71. })
  72. return frappe.flags.enqueue_after_commit
  73. else:
  74. return q.enqueue_call(execute_job, timeout=timeout,
  75. kwargs=queue_args)
  76. def enqueue_doc(doctype, name=None, method=None, queue='default', timeout=300,
  77. now=False, **kwargs):
  78. '''Enqueue a method to be run on a document'''
  79. return enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name,
  80. doc_method=method, queue=queue, timeout=timeout, now=now, **kwargs)
  81. def run_doc_method(doctype, name, doc_method, **kwargs):
  82. getattr(frappe.get_doc(doctype, name), doc_method)(**kwargs)
  83. def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, retry=0):
  84. '''Executes job in a worker, performs commit/rollback and logs if there is any error'''
  85. if is_async:
  86. frappe.connect(site)
  87. if os.environ.get('CI'):
  88. frappe.flags.in_test = True
  89. if user:
  90. frappe.set_user(user)
  91. if isinstance(method, str):
  92. method_name = method
  93. method = frappe.get_attr(method)
  94. else:
  95. method_name = cstr(method.__name__)
  96. frappe.monitor.start("job", method_name, kwargs)
  97. try:
  98. method(**kwargs)
  99. except (frappe.db.InternalError, frappe.RetryBackgroundJobError) as e:
  100. frappe.db.rollback()
  101. if (retry < 5 and
  102. (isinstance(e, frappe.RetryBackgroundJobError) or
  103. (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))):
  104. # retry the job if
  105. # 1213 = deadlock
  106. # 1205 = lock wait timeout
  107. # or RetryBackgroundJobError is explicitly raised
  108. frappe.destroy()
  109. time.sleep(retry+1)
  110. return execute_job(site, method, event, job_name, kwargs,
  111. is_async=is_async, retry=retry+1)
  112. else:
  113. frappe.log_error(title=method_name)
  114. raise
  115. except:
  116. frappe.db.rollback()
  117. frappe.log_error(title=method_name)
  118. frappe.db.commit()
  119. print(frappe.get_traceback())
  120. raise
  121. else:
  122. frappe.db.commit()
  123. finally:
  124. frappe.monitor.stop()
  125. if is_async:
  126. frappe.destroy()
  127. def start_worker(queue=None, quiet = False, rq_username=None, rq_password=None):
  128. '''Wrapper to start rq worker. Connects to redis and monitors these queues.'''
  129. with frappe.init_site():
  130. # empty init is required to get redis_queue from common_site_config.json
  131. redis_connection = get_redis_conn(username=rq_username, password=rq_password)
  132. queues = get_queue_list(queue, build_queue_name=True)
  133. queue_name = queue and generate_qname(queue)
  134. if os.environ.get('CI'):
  135. setup_loghandlers('ERROR')
  136. with Connection(redis_connection):
  137. logging_level = "INFO"
  138. if quiet:
  139. logging_level = "WARNING"
  140. Worker(queues, name=get_worker_name(queue_name)).work(logging_level = logging_level)
  141. def get_worker_name(queue):
  142. '''When limiting worker to a specific queue, also append queue name to default worker name'''
  143. name = None
  144. if queue:
  145. # hostname.pid is the default worker name
  146. name = '{uuid}.{hostname}.{pid}.{queue}'.format(
  147. uuid=uuid4().hex,
  148. hostname=socket.gethostname(),
  149. pid=os.getpid(),
  150. queue=queue)
  151. return name
  152. def get_jobs(site=None, queue=None, key='method'):
  153. '''Gets jobs per queue or per site or both'''
  154. jobs_per_site = defaultdict(list)
  155. def add_to_dict(job):
  156. if key in job.kwargs:
  157. jobs_per_site[job.kwargs['site']].append(job.kwargs[key])
  158. elif key in job.kwargs.get('kwargs', {}):
  159. # optional keyword arguments are stored in 'kwargs' of 'kwargs'
  160. jobs_per_site[job.kwargs['site']].append(job.kwargs['kwargs'][key])
  161. for queue in get_queue_list(queue):
  162. q = get_queue(queue)
  163. jobs = q.jobs + get_running_jobs_in_queue(q)
  164. for job in jobs:
  165. if job.kwargs.get('site'):
  166. # if job belongs to current site, or if all jobs are requested
  167. if (job.kwargs['site'] == site) or site is None:
  168. add_to_dict(job)
  169. else:
  170. print('No site found in job', job.__dict__)
  171. return jobs_per_site
  172. def get_queue_list(queue_list=None, build_queue_name=False):
  173. '''Defines possible queues. Also wraps a given queue in a list after validating.'''
  174. default_queue_list = list(get_queues_timeout())
  175. if queue_list:
  176. if isinstance(queue_list, str):
  177. queue_list = [queue_list]
  178. for queue in queue_list:
  179. validate_queue(queue, default_queue_list)
  180. else:
  181. queue_list = default_queue_list
  182. return [generate_qname(qtype) for qtype in queue_list] if build_queue_name else queue_list
  183. def get_workers(queue=None):
  184. '''Returns a list of Worker objects tied to a queue object if queue is passed, else returns a list of all workers'''
  185. if queue:
  186. return Worker.all(queue=queue)
  187. else:
  188. return Worker.all(get_redis_conn())
  189. def get_running_jobs_in_queue(queue):
  190. '''Returns a list of Jobs objects that are tied to a queue object and are currently running'''
  191. jobs = []
  192. workers = get_workers(queue)
  193. for worker in workers:
  194. current_job = worker.get_current_job()
  195. if current_job:
  196. jobs.append(current_job)
  197. return jobs
  198. def get_queue(qtype, is_async=True):
  199. '''Returns a Queue object tied to a redis connection'''
  200. validate_queue(qtype)
  201. return Queue(generate_qname(qtype), connection=get_redis_conn(), is_async=is_async)
  202. def validate_queue(queue, default_queue_list=None):
  203. if not default_queue_list:
  204. default_queue_list = list(get_queues_timeout())
  205. if queue not in default_queue_list:
  206. frappe.throw(_("Queue should be one of {0}").format(', '.join(default_queue_list)))
  207. @retry(
  208. retry=retry_if_exception_type(BusyLoadingError) | retry_if_exception_type(ConnectionError),
  209. stop=stop_after_attempt(10),
  210. wait=wait_fixed(1)
  211. )
  212. def get_redis_conn(username=None, password=None):
  213. if not hasattr(frappe.local, 'conf'):
  214. raise Exception('You need to call frappe.init')
  215. elif not frappe.local.conf.redis_queue:
  216. raise Exception('redis_queue missing in common_site_config.json')
  217. global redis_connection
  218. cred = frappe._dict()
  219. if frappe.conf.get('use_rq_auth'):
  220. if username:
  221. cred['username'] = username
  222. cred['password'] = password
  223. else:
  224. cred['username'] = frappe.get_site_config().rq_username or get_bench_id()
  225. cred['password'] = frappe.get_site_config().rq_password
  226. elif os.environ.get('RQ_ADMIN_PASWORD'):
  227. cred['username'] = 'default'
  228. cred['password'] = os.environ.get('RQ_ADMIN_PASWORD')
  229. try:
  230. redis_connection = RedisQueue.get_connection(**cred)
  231. except (redis.exceptions.AuthenticationError, redis.exceptions.ResponseError):
  232. log(f'Wrong credentials used for {cred.username or "default user"}. '
  233. 'You can reset credentials using `bench create-rq-users` CLI and restart the server',
  234. colour='red')
  235. raise
  236. except Exception:
  237. log(f'Please make sure that Redis Queue runs @ {frappe.get_conf().redis_queue}', colour='red')
  238. raise
  239. return redis_connection
  240. def get_queues() -> List[Queue]:
  241. """Get all the queues linked to the current bench.
  242. """
  243. queues = Queue.all(connection=get_redis_conn())
  244. return [q for q in queues if is_queue_accessible(q)]
  245. def generate_qname(qtype: str) -> str:
  246. """Generate qname by combining bench ID and queue type.
  247. qnames are useful to define namespaces of customers.
  248. """
  249. return f"{get_bench_id()}:{qtype}"
  250. def is_queue_accessible(qobj: Queue) -> bool:
  251. """Checks whether queue is relate to current bench or not.
  252. """
  253. accessible_queues = [generate_qname(q) for q in list(get_queues_timeout())]
  254. return qobj.name in accessible_queues
  255. def enqueue_test_job():
  256. enqueue('frappe.utils.background_jobs.test_job', s=100)
  257. def test_job(s):
  258. import time
  259. print('sleeping...')
  260. time.sleep(s)