Development of an internal social media platform with personalised dashboards for students
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.

migrate.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. # -*- coding: utf-8 -*-
  2. """
  3. celery.contrib.migrate
  4. ~~~~~~~~~~~~~~~~~~~~~~
  5. Migration tools.
  6. """
  7. from __future__ import absolute_import, print_function, unicode_literals
  8. import socket
  9. from functools import partial
  10. from itertools import cycle, islice
  11. from kombu import eventloop, Queue
  12. from kombu.common import maybe_declare
  13. from kombu.utils.encoding import ensure_bytes
  14. from celery.app import app_or_default
  15. from celery.five import string, string_t
  16. from celery.utils import worker_direct
  17. __all__ = ['StopFiltering', 'State', 'republish', 'migrate_task',
  18. 'migrate_tasks', 'move', 'task_id_eq', 'task_id_in',
  19. 'start_filter', 'move_task_by_id', 'move_by_idmap',
  20. 'move_by_taskmap', 'move_direct', 'move_direct_by_id']
  21. MOVING_PROGRESS_FMT = """\
  22. Moving task {state.filtered}/{state.strtotal}: \
  23. {body[task]}[{body[id]}]\
  24. """
  25. class StopFiltering(Exception):
  26. pass
  27. class State(object):
  28. count = 0
  29. filtered = 0
  30. total_apx = 0
  31. @property
  32. def strtotal(self):
  33. if not self.total_apx:
  34. return '?'
  35. return string(self.total_apx)
  36. def __repr__(self):
  37. if self.filtered:
  38. return '^{0.filtered}'.format(self)
  39. return '{0.count}/{0.strtotal}'.format(self)
  40. def republish(producer, message, exchange=None, routing_key=None,
  41. remove_props=['application_headers',
  42. 'content_type',
  43. 'content_encoding',
  44. 'headers']):
  45. body = ensure_bytes(message.body) # use raw message body.
  46. info, headers, props = (message.delivery_info,
  47. message.headers, message.properties)
  48. exchange = info['exchange'] if exchange is None else exchange
  49. routing_key = info['routing_key'] if routing_key is None else routing_key
  50. ctype, enc = message.content_type, message.content_encoding
  51. # remove compression header, as this will be inserted again
  52. # when the message is recompressed.
  53. compression = headers.pop('compression', None)
  54. for key in remove_props:
  55. props.pop(key, None)
  56. producer.publish(ensure_bytes(body), exchange=exchange,
  57. routing_key=routing_key, compression=compression,
  58. headers=headers, content_type=ctype,
  59. content_encoding=enc, **props)
  60. def migrate_task(producer, body_, message, queues=None):
  61. info = message.delivery_info
  62. queues = {} if queues is None else queues
  63. republish(producer, message,
  64. exchange=queues.get(info['exchange']),
  65. routing_key=queues.get(info['routing_key']))
  66. def filter_callback(callback, tasks):
  67. def filtered(body, message):
  68. if tasks and body['task'] not in tasks:
  69. return
  70. return callback(body, message)
  71. return filtered
  72. def migrate_tasks(source, dest, migrate=migrate_task, app=None,
  73. queues=None, **kwargs):
  74. app = app_or_default(app)
  75. queues = prepare_queues(queues)
  76. producer = app.amqp.TaskProducer(dest)
  77. migrate = partial(migrate, producer, queues=queues)
  78. def on_declare_queue(queue):
  79. new_queue = queue(producer.channel)
  80. new_queue.name = queues.get(queue.name, queue.name)
  81. if new_queue.routing_key == queue.name:
  82. new_queue.routing_key = queues.get(queue.name,
  83. new_queue.routing_key)
  84. if new_queue.exchange.name == queue.name:
  85. new_queue.exchange.name = queues.get(queue.name, queue.name)
  86. new_queue.declare()
  87. return start_filter(app, source, migrate, queues=queues,
  88. on_declare_queue=on_declare_queue, **kwargs)
  89. def _maybe_queue(app, q):
  90. if isinstance(q, string_t):
  91. return app.amqp.queues[q]
  92. return q
  93. def move(predicate, connection=None, exchange=None, routing_key=None,
  94. source=None, app=None, callback=None, limit=None, transform=None,
  95. **kwargs):
  96. """Find tasks by filtering them and move the tasks to a new queue.
  97. :param predicate: Filter function used to decide which messages
  98. to move. Must accept the standard signature of ``(body, message)``
  99. used by Kombu consumer callbacks. If the predicate wants the message
  100. to be moved it must return either:
  101. 1) a tuple of ``(exchange, routing_key)``, or
  102. 2) a :class:`~kombu.entity.Queue` instance, or
  103. 3) any other true value which means the specified
  104. ``exchange`` and ``routing_key`` arguments will be used.
  105. :keyword connection: Custom connection to use.
  106. :keyword source: Optional list of source queues to use instead of the
  107. default (which is the queues in :setting:`CELERY_QUEUES`).
  108. This list can also contain new :class:`~kombu.entity.Queue` instances.
  109. :keyword exchange: Default destination exchange.
  110. :keyword routing_key: Default destination routing key.
  111. :keyword limit: Limit number of messages to filter.
  112. :keyword callback: Callback called after message moved,
  113. with signature ``(state, body, message)``.
  114. :keyword transform: Optional function to transform the return
  115. value (destination) of the filter function.
  116. Also supports the same keyword arguments as :func:`start_filter`.
  117. To demonstrate, the :func:`move_task_by_id` operation can be implemented
  118. like this:
  119. .. code-block:: python
  120. def is_wanted_task(body, message):
  121. if body['id'] == wanted_id:
  122. return Queue('foo', exchange=Exchange('foo'),
  123. routing_key='foo')
  124. move(is_wanted_task)
  125. or with a transform:
  126. .. code-block:: python
  127. def transform(value):
  128. if isinstance(value, string_t):
  129. return Queue(value, Exchange(value), value)
  130. return value
  131. move(is_wanted_task, transform=transform)
  132. The predicate may also return a tuple of ``(exchange, routing_key)``
  133. to specify the destination to where the task should be moved,
  134. or a :class:`~kombu.entitiy.Queue` instance.
  135. Any other true value means that the task will be moved to the
  136. default exchange/routing_key.
  137. """
  138. app = app_or_default(app)
  139. queues = [_maybe_queue(app, queue) for queue in source or []] or None
  140. with app.connection_or_acquire(connection, pool=False) as conn:
  141. producer = app.amqp.TaskProducer(conn)
  142. state = State()
  143. def on_task(body, message):
  144. ret = predicate(body, message)
  145. if ret:
  146. if transform:
  147. ret = transform(ret)
  148. if isinstance(ret, Queue):
  149. maybe_declare(ret, conn.default_channel)
  150. ex, rk = ret.exchange.name, ret.routing_key
  151. else:
  152. ex, rk = expand_dest(ret, exchange, routing_key)
  153. republish(producer, message,
  154. exchange=ex, routing_key=rk)
  155. message.ack()
  156. state.filtered += 1
  157. if callback:
  158. callback(state, body, message)
  159. if limit and state.filtered >= limit:
  160. raise StopFiltering()
  161. return start_filter(app, conn, on_task, consume_from=queues, **kwargs)
  162. def expand_dest(ret, exchange, routing_key):
  163. try:
  164. ex, rk = ret
  165. except (TypeError, ValueError):
  166. ex, rk = exchange, routing_key
  167. return ex, rk
  168. def task_id_eq(task_id, body, message):
  169. return body['id'] == task_id
  170. def task_id_in(ids, body, message):
  171. return body['id'] in ids
  172. def prepare_queues(queues):
  173. if isinstance(queues, string_t):
  174. queues = queues.split(',')
  175. if isinstance(queues, list):
  176. queues = dict(tuple(islice(cycle(q.split(':')), None, 2))
  177. for q in queues)
  178. if queues is None:
  179. queues = {}
  180. return queues
  181. def start_filter(app, conn, filter, limit=None, timeout=1.0,
  182. ack_messages=False, tasks=None, queues=None,
  183. callback=None, forever=False, on_declare_queue=None,
  184. consume_from=None, state=None, accept=None, **kwargs):
  185. state = state or State()
  186. queues = prepare_queues(queues)
  187. consume_from = [_maybe_queue(app, q)
  188. for q in consume_from or list(queues)]
  189. if isinstance(tasks, string_t):
  190. tasks = set(tasks.split(','))
  191. if tasks is None:
  192. tasks = set([])
  193. def update_state(body, message):
  194. state.count += 1
  195. if limit and state.count >= limit:
  196. raise StopFiltering()
  197. def ack_message(body, message):
  198. message.ack()
  199. consumer = app.amqp.TaskConsumer(conn, queues=consume_from, accept=accept)
  200. if tasks:
  201. filter = filter_callback(filter, tasks)
  202. update_state = filter_callback(update_state, tasks)
  203. ack_message = filter_callback(ack_message, tasks)
  204. consumer.register_callback(filter)
  205. consumer.register_callback(update_state)
  206. if ack_messages:
  207. consumer.register_callback(ack_message)
  208. if callback is not None:
  209. callback = partial(callback, state)
  210. if tasks:
  211. callback = filter_callback(callback, tasks)
  212. consumer.register_callback(callback)
  213. # declare all queues on the new broker.
  214. for queue in consumer.queues:
  215. if queues and queue.name not in queues:
  216. continue
  217. if on_declare_queue is not None:
  218. on_declare_queue(queue)
  219. try:
  220. _, mcount, _ = queue(consumer.channel).queue_declare(passive=True)
  221. if mcount:
  222. state.total_apx += mcount
  223. except conn.channel_errors:
  224. pass
  225. # start migrating messages.
  226. with consumer:
  227. try:
  228. for _ in eventloop(conn, # pragma: no cover
  229. timeout=timeout, ignore_timeouts=forever):
  230. pass
  231. except socket.timeout:
  232. pass
  233. except StopFiltering:
  234. pass
  235. return state
  236. def move_task_by_id(task_id, dest, **kwargs):
  237. """Find a task by id and move it to another queue.
  238. :param task_id: Id of task to move.
  239. :param dest: Destination queue.
  240. Also supports the same keyword arguments as :func:`move`.
  241. """
  242. return move_by_idmap({task_id: dest}, **kwargs)
  243. def move_by_idmap(map, **kwargs):
  244. """Moves tasks by matching from a ``task_id: queue`` mapping,
  245. where ``queue`` is a queue to move the task to.
  246. Example::
  247. >>> move_by_idmap({
  248. ... '5bee6e82-f4ac-468e-bd3d-13e8600250bc': Queue('name'),
  249. ... 'ada8652d-aef3-466b-abd2-becdaf1b82b3': Queue('name'),
  250. ... '3a2b140d-7db1-41ba-ac90-c36a0ef4ab1f': Queue('name')},
  251. ... queues=['hipri'])
  252. """
  253. def task_id_in_map(body, message):
  254. return map.get(body['id'])
  255. # adding the limit means that we don't have to consume any more
  256. # when we've found everything.
  257. return move(task_id_in_map, limit=len(map), **kwargs)
  258. def move_by_taskmap(map, **kwargs):
  259. """Moves tasks by matching from a ``task_name: queue`` mapping,
  260. where ``queue`` is the queue to move the task to.
  261. Example::
  262. >>> move_by_taskmap({
  263. ... 'tasks.add': Queue('name'),
  264. ... 'tasks.mul': Queue('name'),
  265. ... })
  266. """
  267. def task_name_in_map(body, message):
  268. return map.get(body['task']) # <- name of task
  269. return move(task_name_in_map, **kwargs)
  270. def filter_status(state, body, message, **kwargs):
  271. print(MOVING_PROGRESS_FMT.format(state=state, body=body, **kwargs))
  272. move_direct = partial(move, transform=worker_direct)
  273. move_direct_by_id = partial(move_task_by_id, transform=worker_direct)
  274. move_direct_by_idmap = partial(move_by_idmap, transform=worker_direct)
  275. move_direct_by_taskmap = partial(move_by_taskmap, transform=worker_direct)