[源码解析] 并行分布式任务队列 Celery 之 EventDispatcher & Event 组件

0x00 摘要

Celery是一个简单、灵活且可靠的,处理大量事件的分布式系统,专注于实时处理的异步任务队列,同时也支持任务调度。

本文讲解 EventDispatcher 和 Event 组件 如何实现。

0x01 思路

EventDispatcher 和 Event 组件负责 Celery 内部事件(Event)的处理。

从字面上可以知道,EventDispatcher 组件的功能是事件(Event)分发,所以我们可以有如下已知信息:

  • 事件分发 势必有生产者,消费者,EventDispatcher 就是作为 事件生产者;
  • 涉及到生产消费,那么需要有一个 broker 存储中间事件;
  • 因为 Celery 底层依赖于 Kombu,而 Kombu 本身就有生产者,消费者概念,所以这里可以直接利用这两个概念;
  • Kombu 也提供了 Mailbox 的实现,它的作用就是通过 Mailbox 我们可以实现不同实例之间的事件发送和处理,具体可以是单播 和 广播;

所以我们可以大致推论:EventDispatcher 可以利用 kombu 的 producer, consumer 或者 Mailbox。

而 Events 是负责事件(Event)的接受,所以我们也可以推论:

  • Events 利用 Kombu 的消费者来处理 事件;
  • 具体如何处理事件,则会依据 Celery 的当前状态决定,这就涉及到了 State 功能;

我们下面就看看具体是怎么实现的。

为了让大家更好理解,我们先给出一个逻辑图如下:

0x02 定义

EventDispatcher 代码位于:celery\events\dispatcher.py

可以看到一个事件分发者需要拥有哪些成员变量以实现自己的功能:

  • connection (kombu.Connection) :就是用来和 Broker 交互的连接功能;
  • channel (kombu.Channel) : Channel 可以理解成共享一个Connection的多个轻量化连接。就是真正的连接。
    • Connection 是 AMQP 对 连接的封装;
    • Channel 是 AMQP 对 MQ 的操作的封装;
    • 具体以 "针对redis的轻量化连接" 来说,Channel 可以认为是 redis 操作和连接的封装。每个 Channel 都可以与 redis 建立一个连接,在此连接之上对 redis 进行操作,每个连接都有一个 socket,每个 socket 都有一个 file,从这个 file 可以进行 poll。
  • producer :事件生产者,使用 kombu producer 概念;
  • exchange :生产者发布事件时,先将事件发送到Exchange,通过Exchange与队列的绑定规则将事件发送到队列。
  • hostname : 用来标示自己,这样 EventDispatcher 的使用者可以知道并且使用;
  • groups :事件组功能;
  • _outbound_buffer :事件缓存;
  • clock :Lamport 逻辑时钟,在分布式系统中用于区分事件的发生顺序的时间机制;

具体类的定义是:

  1. class EventDispatcher:
  2. """Dispatches event messages.
  3. """
  4. DISABLED_TRANSPORTS = {'sql'}
  5. app = None
  6. def __init__(self, connection=None, hostname=None, enabled=True,
  7. channel=None, buffer_while_offline=True, app=None,
  8. serializer=None, groups=None, delivery_mode=1,
  9. buffer_group=None, buffer_limit=24, on_send_buffered=None):
  10. self.app = app_or_default(app or self.app)
  11. self.connection = connection
  12. self.channel = channel
  13. self.hostname = hostname or anon_nodename()
  14. self.buffer_while_offline = buffer_while_offline
  15. self.buffer_group = buffer_group or frozenset()
  16. self.buffer_limit = buffer_limit
  17. self.on_send_buffered = on_send_buffered
  18. self._group_buffer = defaultdict(list)
  19. self.mutex = threading.Lock()
  20. self.producer = None
  21. self._outbound_buffer = deque()
  22. self.serializer = serializer or self.app.conf.event_serializer
  23. self.on_enabled = set()
  24. self.on_disabled = set()
  25. self.groups = set(groups or [])
  26. self.tzoffset = [-time.timezone, -time.altzone]
  27. self.clock = self.app.clock
  28. self.delivery_mode = delivery_mode
  29. if not connection and channel:
  30. self.connection = channel.connection.client
  31. self.enabled = enabled
  32. conninfo = self.connection or self.app.connection_for_write()
  33. self.exchange = get_exchange(conninfo,
  34. name=self.app.conf.event_exchange)
  35. if conninfo.transport.driver_type in self.DISABLED_TRANSPORTS:
  36. self.enabled = False
  37. if self.enabled:
  38. self.enable()
  39. self.headers = {'hostname': self.hostname}
  40. self.pid = os.getpid()

我们先给出此时变量内容,大家可以先有所了解。

  1. self = {EventDispatcher} <celery.events.dispatcher.EventDispatcher object at 0x000001D37765B308>
  2. DISABLED_TRANSPORTS = {set: 1} {'sql'}
  3. app = {Celery} <Celery myTest at 0x1d375a69e88>
  4. buffer_group = {frozenset: 0} frozenset()
  5. buffer_limit = {int} 24
  6. buffer_while_offline = {bool} True
  7. channel = {NoneType} None
  8. clock = {LamportClock} 0
  9. connection = {Connection} <Connection: redis://localhost:6379// at 0x1d37765b388>
  10. delivery_mode = {int} 1
  11. enabled = {bool} True
  12. exchange = {Exchange} Exchange celeryev(fanout)
  13. groups = {set: 1} {'worker'}
  14. headers = {dict: 1} {'hostname': 'celery@DESKTOP-0GO3RPO'}
  15. hostname = {str} 'celery@DESKTOP-0GO3RPO'
  16. mutex = {lock} <unlocked _thread.lock object at 0x000001D377623A20>
  17. on_disabled = {set: 1} {<bound method Heart.stop of <celery.worker.heartbeat.Heart object at 0x000001D377636408>>}
  18. on_enabled = {set: 1} {<bound method Heart.start of <celery.worker.heartbeat.Heart object at 0x000001D377636408>>}
  19. on_send_buffered = {NoneType} None
  20. pid = {int} 26144
  21. producer = {Producer} <Producer: <promise: 0x1d37761cf78>>
  22. publisher = {Producer} <Producer: <promise: 0x1d37761cf78>>
  23. serializer = {str} 'json'
  24. tzoffset = {list: 2} [28800, 32400]
  25. _group_buffer = {defaultdict: 0} defaultdict(<class 'list'>, {})
  26. _outbound_buffer = {deque: 0} deque([])

0x03 Producer

我们发现,EventDispatcher 确实使用了 Kombu 的 Producer,当然 Celery 这里使用 ampq 对 Kombu 做了封装。所以我们重点就需要看如何配置 Producer。

具体需要配置的是:

  • Connection,需要以此来知道联系哪一个 Redis;

  • Exchange,需要知道读取哪一个 Queue;

下面我们就逐一分析。

3.1 Connection

由代码可以看到,Connection 是直接使用 Celery 的 connection_for_write

  1. conninfo = self.connection or self.app.connection_for_write()

此时变量为:

  1. connection = {Connection} <Connection: redis://localhost:6379// at 0x1be931de148>
  2. conninfo = {Connection} <Connection: redis://localhost:6379// at 0x1be931de148>

3.2 Exchange

Exchange 概念如下:

  • Exchange:交换机 或者 路由。事件发送者将事件发至Exchange,Exchange负责将事件分发至队列;
  • Queue:事件队列,存储着即将被应用消费掉的事件,Exchange负责将事件分发Queue,消费者从Queue接收事件;

具体来说,Exchange 用于路由事件(事件发给exchange,exchange发给对应的queue)。

交换机通过匹配事件的 routing_key 和 binding_key来转发事件,binding_key 是consumer 声明队列时与交换机的绑定关系。

路由就是比较routing-key(这个 message 提供)和 binding-key(这个queue 注册到 exchange 的时候提供)。

使用时,需要指定exchange的名称和类型(direct,topic和fanout)。可以发现,和RabbitMQ中的exchange概念是一样的。事件发送给exchages。交换机可以被命名,可以通过路由算法进行配置。

具体回到代码上。

  1. def get_exchange(conn, name=EVENT_EXCHANGE_NAME):
  2. """Get exchange used for sending events.
  3. Arguments:
  4. conn (kombu.Connection): Connection used for sending/receiving events.
  5. name (str): Name of the exchange. Default is ``celeryev``.
  6. Note:
  7. The event type changes if Redis is used as the transport
  8. (from topic -> fanout).
  9. """
  10. ex = copy(event_exchange)
  11. if conn.transport.driver_type == 'redis':
  12. # quick hack for Issue #436
  13. ex.type = 'fanout'
  14. if name != ex.name:
  15. ex.name = name
  16. return ex

此时变量为:

  1. EVENT_EXCHANGE_NAME = 'celeryev'
  2. self.exchange = {Exchange} Exchange celeryev(fanout)

所以我们知道,这里默认的 Exchange 就是一个 celeryev(fanout) 类型。

3.3 建立

于是,我们具体就看到了 Producer。

  1. def enable(self):
  2. self.producer = Producer(self.channel or self.connection,
  3. exchange=self.exchange,
  4. serializer=self.serializer,
  5. auto_declare=False)
  6. self.enabled = True
  7. for callback in self.on_enabled:
  8. callback()

0x04 分发事件

既然建立了 Producer,我们就可以进行发送。

4.1 Send 发送

发送事件就是直接是否需要成组发送。

  • 如果需要分组发送,就内部有一个缓存,然后成组发送;
  • 否则就直接调用 Producer publish API 发送。

关于如何区分分组是依靠如下代码:

  1. groups, group = self.groups, group_from(type)

相关变量为:

  1. group = {str} 'worker'
  2. groups = {set: 1} {'worker'}
  3. type = {str} 'worker-online'

发送具体代码如下:

  1. def send(self, type, blind=False, utcoffset=utcoffset, retry=False,
  2. retry_policy=None, Event=Event, **fields):
  3. """Send event.
  4. """
  5. if self.enabled:
  6. groups, group = self.groups, group_from(type)
  7. if groups and group not in groups:
  8. return
  9. if group in self.buffer_group:
  10. clock = self.clock.forward()
  11. event = Event(type, hostname=self.hostname,
  12. utcoffset=utcoffset(),
  13. pid=self.pid, clock=clock, **fields)
  14. buf = self._group_buffer[group]
  15. buf.append(event)
  16. if len(buf) >= self.buffer_limit:
  17. self.flush()
  18. elif self.on_send_buffered:
  19. self.on_send_buffered()
  20. else:
  21. return self.publish(type, fields, self.producer, blind=blind,
  22. Event=Event, retry=retry,
  23. retry_policy=retry_policy)

4.2 publish 与 broker 交互

send 会调用到这里。

这里构建了 routing_key :

  1. routing_key=type.replace('-', '.')

于是得倒了routing_key 为 'worker.online'。

也构建了 Event;

  1. event = {dict: 13}
  2. 'hostname' = {str} 'celery@DESKTOP-0GO3RPO'
  3. 'utcoffset' = {int} -8
  4. 'pid' = {int} 24320
  5. 'clock' = {int} 1
  6. 'freq' = {float} 2.0
  7. 'active' = {int} 0
  8. 'processed' = {int} 0
  9. 'loadavg' = {tuple: 3} (0.0, 0.0, 0.0)
  10. 'sw_ident' = {str} 'py-celery'
  11. 'sw_ver' = {str} '5.0.5'
  12. 'sw_sys' = {str} 'Windows'
  13. 'timestamp' = {float} 1611464767.3456059
  14. 'type' = {str} 'worker-online'
  15. __len__ = {int} 13

publish 代码如下:

  1. def publish(self, type, fields, producer,
  2. blind=False, Event=Event, **kwargs):
  3. """Publish event using custom :class:`~kombu.Producer`.
  4. Arguments:
  5. type (str): Event type name, with group separated by dash (`-`).
  6. fields: Dictionary of event fields, must be json serializable.
  7. producer (kombu.Producer): Producer instance to use:
  8. only the ``publish`` method will be called.
  9. retry (bool): Retry in the event of connection failure.
  10. retry_policy (Mapping): Map of custom retry policy options.
  11. See :meth:`~kombu.Connection.ensure`.
  12. blind (bool): Don't set logical clock value (also don't forward
  13. the internal logical clock).
  14. Event (Callable): Event type used to create event.
  15. Defaults to :func:`Event`.
  16. utcoffset (Callable): Function returning the current
  17. utc offset in hours.
  18. """
  19. clock = None if blind else self.clock.forward()
  20. event = Event(type, hostname=self.hostname, utcoffset=utcoffset(),
  21. pid=self.pid, clock=clock, **fields)
  22. with self.mutex:
  23. return self._publish(event, producer,
  24. routing_key=type.replace('-', '.'), **kwargs)
  25. def _publish(self, event, producer, routing_key, retry=False,
  26. retry_policy=None, utcoffset=utcoffset):
  27. exchange = self.exchange
  28. try:
  29. producer.publish(
  30. event,
  31. routing_key=routing_key,
  32. exchange=exchange.name,
  33. retry=retry,
  34. retry_policy=retry_policy,
  35. declare=[exchange],
  36. serializer=self.serializer,
  37. headers=self.headers,
  38. delivery_mode=self.delivery_mode,
  39. )
  40. except Exception as exc: # pylint: disable=broad-except
  41. if not self.buffer_while_offline:
  42. raise
  43. self._outbound_buffer.append((event, routing_key, exc))

因为是 pubsub,所以此时在 redis 之中看不到事件内容。

此时redis内容如下(看不到事件):

  1. redis-cli.exe -p 6379
  2. 127.0.0.1:6379> keys *
  3. 1) "_kombu.binding.celery.pidbox"
  4. 2) "_kombu.binding.celery"
  5. 3) "_kombu.binding.celeryev"
  6. 127.0.0.1:6379> smembers _kombu.binding.celeryev
  7. 1) "worker.#\x06\x16\x06\x16celeryev.64089900-d397-4564-b343-742664c1b214"
  8. 127.0.0.1:6379> smembers _kombu.binding.celery
  9. 1) "celery\x06\x16\x06\x16celery"
  10. 127.0.0.1:6379> smembers _kombu.binding.celery.pidbox
  11. 1) "\x06\x16\x06\x16celery@DESKTOP-0GO3RPO.celery.pidbox"
  12. 127.0.0.1:6379>

现在,EventDispatcher 组件已经把事件发送出去。

这个事件将如何处理?我们需要看看 Events 组件

0x05 Events 组件

5.1 Event 有什么用

前面说了,Celery 在 Task/Worker 的状态发生变化的时候就会发出 Event,所以,一个很明显的应用就是监控 Event 的状态,例如 Celery 大家所熟知的基于 WebUI 的管理工具 flower 就用到了 Event,但是,这也是一个比较明显的应用,除此之外,我们还可以利用 Event 来给 Task 做快照,甚至实时对 Task 的状态转变做出响应,例如任务失败之后触发报警,任务成功之后执行被依赖的任务等等,总结一下,其实就是:

  • 对 Task 的状态做快照;
  • 对 Task 的状态做实时处理;
  • 监控 Celery(Worker/Task) 的执行状态;

5.2 调试

Celery Events 可以用来开启快照相机,或者将事件dump到标准输出。

比如:

  1. celery -A proj events -c myapp.DumpCam --frequency=2.0
  2. celery -A proj events --camera=<camera-class> --frequency=1.0
  3. celery -A proj events --dump

为了调试,我们需要采用如下方式:

  1. app.start(argv=['events'])

具体命令实现是:

  1. def events(ctx, dump, camera, detach, frequency, maxrate, loglevel, **kwargs):
  2. """Event-stream utilities."""
  3. app = ctx.obj.app
  4. if dump:
  5. return _run_evdump(app)
  6. if camera:
  7. return _run_evcam(camera, app=app, freq=frequency, maxrate=maxrate,
  8. loglevel=loglevel,
  9. detach=detach,
  10. **kwargs)
  11. return _run_evtop(app)

5.3 入口

Events入口为:

  1. def _run_evtop(app):
  2. try:
  3. from celery.events.cursesmon import evtop
  4. _set_process_status('top')
  5. return evtop(app=app)

接着跟踪看看。

  1. def evtop(app=None): # pragma: no cover
  2. """Start curses monitor."""
  3. app = app_or_default(app)
  4. state = app.events.State()
  5. display = CursesMonitor(state, app)
  6. display.init_screen()
  7. refresher = DisplayThread(display)
  8. refresher.start()
  9. capture_events(app, state, display)

5.4 事件循环

我们来到了事件循环。

这里建立了一个 app.events.Receiver。

注意,这里给 Receiver 传入的 handlers={'*': state.event},是后续处理事件时候的处理函数。

  1. def capture_events(app, state, display): # pragma: no cover
  2. while 1:
  3. with app.connection_for_read() as conn:
  4. try:
  5. conn.ensure_connection(on_connection_error,
  6. app.conf.broker_connection_max_retries)
  7. recv = app.events.Receiver(conn, handlers={'*': state.event})
  8. display.resetscreen()
  9. display.init_screen()
  10. recv.capture()
  11. except conn.connection_errors + conn.channel_errors as exc:
  12. print(f'Connection lost: {exc!r}', file=sys.stderr)

结果发现是循环调用 recv.capture()。

具体如下:

  1. Events
  2. +--------------------+
  3. | loop |
  4. | |
  5. | |
  6. | |
  7. | |
  8. | v
  9. |
  10. | EventReceiver.capture()
  11. |
  12. | +
  13. | |
  14. | |
  15. | |
  16. | |
  17. | |
  18. | |
  19. +--------------------+

5.5 EventReceiver

EventReceiver 就是用来接收Event,并且处理的。而且需要留意,EventReceiver 是继承 ConsumerMixin。

  1. class EventReceiver(ConsumerMixin):
  2. """Capture events.
  3. Arguments:
  4. connection (kombu.Connection): Connection to the broker.
  5. handlers (Mapping[Callable]): Event handlers.
  6. This is a map of event type names and their handlers.
  7. The special handler `"*"` captures all events that don't have a
  8. handler.
  9. """

其代码如下:

  1. def capture(self, limit=None, timeout=None, wakeup=True):
  2. """Open up a consumer capturing events.
  3. This has to run in the main process, and it will never stop
  4. unless :attr:`EventDispatcher.should_stop` is set to True, or
  5. forced via :exc:`KeyboardInterrupt` or :exc:`SystemExit`.
  6. """
  7. for _ in self.consume(limit=limit, timeout=timeout, wakeup=wakeup):
  8. pass

对应变量如下:

  1. self.consume = {method} <bound method ConsumerMixin.consume of <celery.events.receiver.EventReceiver object at 0x000001CA8C22AB08>>
  2. self = {EventReceiver} <celery.events.receiver.EventReceiver object at 0x000001CA8C22AB08>

可以看到利用了 ConsumerMixin 来处理事件。其实从文章开始时候我们就知道,既然有 kombu . producer ,就必然有 kombu . consumer。

这里其实是有多个 EventReceiver 绑定了这个 Connection,然后 ConsumerMixin 帮助协调这些 Receiver,每个 Receiver 都可以收到这些 Event,但是能不能处理就看他们的 routing_key 设置得好不好了

所以如下:

  1. Events
  2. +--------------------+
  3. | loop |
  4. | |
  5. | |
  6. | |
  7. | |
  8. | v
  9. |
  10. | EventReceiver(ConsumerMixin).capture()
  11. |
  12. | +
  13. | |
  14. | |
  15. | |
  16. | |
  17. | |
  18. | |
  19. +--------------------+

5.6 ConsumerMixin

ConsumerMixin 是 Kombu 提供的 组合模式类,可以用来方便的实现 Consumer Programs。

  1. class ConsumerMixin:
  2. """Convenience mixin for implementing consumer programs.
  3. It can be used outside of threads, with threads, or greenthreads
  4. (eventlet/gevent) too.
  5. The basic class would need a :attr:`connection` attribute
  6. which must be a :class:`~kombu.Connection` instance,
  7. and define a :meth:`get_consumers` method that returns a list
  8. of :class:`kombu.Consumer` instances to use.
  9. Supporting multiple consumers is important so that multiple
  10. channels can be used for different QoS requirements.
  11. """

文件在 :kombu\mixins.py

  1. def consume(self, limit=None, timeout=None, safety_interval=1, **kwargs):
  2. elapsed = 0
  3. with self.consumer_context(**kwargs) as (conn, channel, consumers):
  4. for i in limit and range(limit) or count():
  5. if self.should_stop:
  6. break
  7. self.on_iteration()
  8. try:
  9. conn.drain_events(timeout=safety_interval)
  10. except socket.timeout:
  11. conn.heartbeat_check()
  12. elapsed += safety_interval
  13. if timeout and elapsed >= timeout:
  14. raise
  15. except OSError:
  16. if not self.should_stop:
  17. raise
  18. else:
  19. yield
  20. elapsed = 0

5.6.1 Consumer

ConsumerMixin 内部建立 Consumer如下:

  1. @contextmanager
  2. def Consumer(self):
  3. with self.establish_connection() as conn:
  4. self.on_connection_revived()
  5. channel = conn.default_channel
  6. cls = partial(Consumer, channel,
  7. on_decode_error=self.on_decode_error)
  8. with self._consume_from(*self.get_consumers(cls, channel)) as c:
  9. yield conn, channel, c
  10. self.on_consume_end(conn, channel)

在 具体建立时候,把self._receive设置为 Consumer callback。

  1. def get_consumers(self, Consumer, channel):
  2. return [Consumer(queues=[self.queue],
  3. callbacks=[self._receive], no_ack=True,
  4. accept=self.accept)]

堆栈为:

  1. get_consumers, receiver.py:72
  2. Consumer, mixins.py:230
  3. __enter__, contextlib.py:112
  4. consumer_context, mixins.py:181
  5. __enter__, contextlib.py:112
  6. consume, mixins.py:188
  7. capture, receiver.py:91
  8. evdump, dumper.py:95
  9. _run_evdump, events.py:21
  10. events, events.py:87
  11. caller, base.py:132
  12. new_func, decorators.py:21
  13. invoke, core.py:610
  14. invoke, core.py:1066
  15. invoke, core.py:1259
  16. main, core.py:782
  17. start, base.py:358
  18. <module>, myEvent.py:18

此时变量为:

  1. self.consume = {method} <bound method ConsumerMixin.consume of <celery.events.receiver.EventReceiver object at 0x000001FE106E06C8>>
  2. self.queue = {Queue} <unbound Queue celeryev.6e24485e-9f27-46e1-90c9-6b52f44b9902 -> <unbound Exchange celeryev(fanout)> -> #>
  3. self._receive = {method} <bound method EventReceiver._receive of <celery.events.receiver.EventReceiver object at 0x000001FE106E06C8>>
  4. Consumer = {partial} functools.partial(<class 'kombu.messaging.Consumer'>, <kombu.transport.redis.Channel object at 0x000001FE1080CC08>, on_decode_error=<bound method ConsumerMixin.on_decode_error of <celery.events.receiver.EventReceiver object at 0x000001FE106E06C8>>)
  5. channel = {Channel} <kombu.transport.redis.Channel object at 0x000001FE1080CC08>
  6. self = {EventReceiver} <celery.events.receiver.EventReceiver object at 0x000001FE106E06C8>

此时为:

  1. Events
  2. +-----------------------------------------+
  3. | EventReceiver(ConsumerMixin) |
  4. | |
  5. | |
  6. | | consume
  7. | | +------------------+
  8. | capture +-----------------> | Consumer |
  9. | | | |
  10. | | | |
  11. | | | |
  12. | _receive <----------------------+ callbacks |
  13. | | | |
  14. | | | |
  15. | | +------------------+
  16. +-----------------------------------------+

5.7 接收

当有事件时候,就调用 _receive 进行接收。

  1. def _receive(self, body, message, list=list, isinstance=isinstance):
  2. if isinstance(body, list): # celery 4.0+: List of events
  3. process, from_message = self.process, self.event_from_message
  4. [process(*from_message(event)) for event in body]
  5. else:
  6. self.process(*self.event_from_message(body))

5.8 处理

接受之后,就可以进行处理。

  1. def process(self, type, event):
  2. """Process event by dispatching to configured handler."""
  3. handler = self.handlers.get(type) or self.handlers.get('*')
  4. handler and handler(event)

此时如下:

这里的 Receiver . handlers 是建立 Receiver时候 传入的 handlers={'*': state.event},是后续处理事件时候的处理函数。

  1. Events
  2. +-----------------------------------------+
  3. | EventReceiver(ConsumerMixin) |
  4. | |
  5. | |
  6. | | consume
  7. | | +------------------+
  8. | capture +-----------------> | Consumer |
  9. | | | |
  10. | | | |
  11. | | | |
  12. | _receive <----------------------+ callbacks |
  13. | | | |
  14. | | | |
  15. | | +------------------+
  16. | |
  17. | handlers +------------+
  18. | | | +------------------+
  19. +-----------------------------------------+ | |state |
  20. | | |
  21. | | |
  22. +-------->event |
  23. | |
  24. | |
  25. +------------------+

5.9 state处理函数

具体如下:

  1. @cached_property
  2. def _event(self):
  3. return self._create_dispatcher()

概括起来是这样的:

  1. 先找 group 的 handler,有的话就用这个了,否则看下面;这个默认是没东西的,所以可以先pass
  2. 如果是 worker 的 Event,就执行 worker 对应的处理
  3. 如果是 task 的 Event,就执行 task 的对应处理
  1. def _create_dispatcher(self):
  2. # noqa: C901
  3. # pylint: disable=too-many-statements
  4. # This code is highly optimized, but not for reusability.
  5. get_handler = self.handlers.__getitem__
  6. event_callback = self.event_callback
  7. wfields = itemgetter('hostname', 'timestamp', 'local_received')
  8. tfields = itemgetter('uuid', 'hostname', 'timestamp',
  9. 'local_received', 'clock')
  10. taskheap = self._taskheap
  11. th_append = taskheap.append
  12. th_pop = taskheap.pop
  13. # Removing events from task heap is an O(n) operation,
  14. # so easier to just account for the common number of events
  15. # for each task (PENDING->RECEIVED->STARTED->final)
  16. #: an O(n) operation
  17. max_events_in_heap = self.max_tasks_in_memory * self.heap_multiplier
  18. add_type = self._seen_types.add
  19. on_node_join, on_node_leave = self.on_node_join, self.on_node_leave
  20. tasks, Task = self.tasks, self.Task
  21. workers, Worker = self.workers, self.Worker
  22. # avoid updating LRU entry at getitem
  23. get_worker, get_task = workers.data.__getitem__, tasks.data.__getitem__
  24. get_task_by_type_set = self.tasks_by_type.__getitem__
  25. get_task_by_worker_set = self.tasks_by_worker.__getitem__
  26. def _event(event,
  27. timetuple=timetuple, KeyError=KeyError,
  28. insort=bisect.insort, created=True):
  29. self.event_count += 1
  30. if event_callback:
  31. event_callback(self, event)
  32. group, _, subject = event['type'].partition('-')
  33. try:
  34. handler = get_handler(group)
  35. except KeyError:
  36. pass
  37. else:
  38. return handler(subject, event), subject
  39. if group == 'worker':
  40. try:
  41. hostname, timestamp, local_received = wfields(event)
  42. except KeyError:
  43. pass
  44. else:
  45. is_offline = subject == 'offline'
  46. try:
  47. worker, created = get_worker(hostname), False
  48. except KeyError:
  49. if is_offline:
  50. worker, created = Worker(hostname), False
  51. else:
  52. worker = workers[hostname] = Worker(hostname)
  53. worker.event(subject, timestamp, local_received, event)
  54. if on_node_join and (created or subject == 'online'):
  55. on_node_join(worker)
  56. if on_node_leave and is_offline:
  57. on_node_leave(worker)
  58. workers.pop(hostname, None)
  59. return (worker, created), subject
  60. elif group == 'task':
  61. (uuid, hostname, timestamp,
  62. local_received, clock) = tfields(event)
  63. # task-sent event is sent by client, not worker
  64. is_client_event = subject == 'sent'
  65. try:
  66. task, task_created = get_task(uuid), False
  67. except KeyError:
  68. task = tasks[uuid] = Task(uuid, cluster_state=self)
  69. task_created = True
  70. if is_client_event:
  71. task.client = hostname
  72. else:
  73. try:
  74. worker = get_worker(hostname)
  75. except KeyError:
  76. worker = workers[hostname] = Worker(hostname)
  77. task.worker = worker
  78. if worker is not None and local_received:
  79. worker.event(None, local_received, timestamp)
  80. origin = hostname if is_client_event else worker.id
  81. # remove oldest event if exceeding the limit.
  82. heaps = len(taskheap)
  83. if heaps + 1 > max_events_in_heap:
  84. th_pop(0)
  85. # most events will be dated later than the previous.
  86. timetup = timetuple(clock, timestamp, origin, ref(task))
  87. if heaps and timetup > taskheap[-1]:
  88. th_append(timetup)
  89. else:
  90. insort(taskheap, timetup)
  91. if subject == 'received':
  92. self.task_count += 1
  93. task.event(subject, timestamp, local_received, event)
  94. task_name = task.name
  95. if task_name is not None:
  96. add_type(task_name)
  97. if task_created: # add to tasks_by_type index
  98. get_task_by_type_set(task_name).add(task)
  99. get_task_by_worker_set(hostname).add(task)
  100. if task.parent_id:
  101. try:
  102. parent_task = self.tasks[task.parent_id]
  103. except KeyError:
  104. self._add_pending_task_child(task)
  105. else:
  106. parent_task.children.add(task)
  107. try:
  108. _children = self._tasks_to_resolve.pop(uuid)
  109. except KeyError:
  110. pass
  111. else:
  112. task.children.update(_children)
  113. return (task, task_created), subject
  114. return _event

具体如下:

  1. Events
  2. +-----------------------------+
  3. | EventReceiver(ConsumerMixin |
  4. | |
  5. | | +------------------+
  6. | | consume | Consumer |
  7. | | | |
  8. | capture +-----------------> | |
  9. | | | |
  10. | | | |
  11. | | | |
  12. | _receive <----------------------+ callbacks |
  13. | | | |
  14. | | | |
  15. | | +------------------+
  16. | |
  17. | handlers +------------+
  18. | | | +------------------------+
  19. +-----------------------------+ | |state |
  20. | | |
  21. | | |
  22. +---------> event +---+ |
  23. | | |
  24. | | |
  25. | v |
  26. | _create_dispatcher |
  27. | + |
  28. | | |
  29. | | |
  30. | | |
  31. +------------------------+
  32. |
  33. |
  34. +--------+------+
  35. group == 'worker' | | group == 'task'
  36. | |
  37. v v
  38. worker.event task.event

最终,逻辑如下:

  1. Producer Scope + Broker + Consumer Scope
  2. | |
  3. +-----------------------------+ | Redis pubsub | Events
  4. | EventDispatcher | | |
  5. | | | | +-----------------------------+
  6. | | | | | EventReceiver(ConsumerMixin |
  7. | | | | | |
  8. | connection | | | | | +------------------+
  9. | | | | | | consume | Consumer |
  10. | channel | | | | | | |
  11. | | | | | capture +-----------------> | |
  12. | producer +-----------------------> Event +-----------> | | | |
  13. | | | | | | | |
  14. | exchange | | | | | | |
  15. | | | | | _receive <----------------------+ callbacks |
  16. | hostname | | | | | | |
  17. | | | | | | | |
  18. | groups | | | | | +------------------+
  19. | | | | | |
  20. | _outbound_buffer | | | | handlers +------------+
  21. | | | | | | | +------------------------+
  22. | clock | | | +-----------------------------+ | |state |
  23. | | | | | | |
  24. +-----------------------------+ | | | | |
  25. | | +---------> event +---+ |
  26. | | | | |
  27. | | | | |
  28. | | | v |
  29. | | | _create_dispatcher |
  30. | | | + |
  31. | | | | |
  32. | | | | |
  33. | | | | |
  34. | | +------------------------+
  35. | | |
  36. | | |
  37. | | +--------+------+
  38. | | group == 'worker' | | group == 'task'
  39. | | | |
  40. | | v v
  41. + + worker.event task.event

手机如下:

至此,Celery 内部的事件发送,接受处理 的两个组件就讲解完毕。

0xEE 个人信息

★★★★★★关于生活和技术的思考★★★★★★

微信公众账号:罗西的思考

如果您想及时得到个人撰写文章的消息推送,或者想看看个人推荐的技术资料,可以扫描下面二维码(或者长按识别二维码)关注个人公众号)。

0xFF 参考

6: Events 的实现

Celery用户指引------监控与管理

[源码解析] 并行分布式任务队列 Celery 之 EventDispatcher & Event 组件的更多相关文章

  1. [源码解析] 并行分布式任务队列 Celery 之 Task是什么

    [源码解析] 并行分布式任务队列 Celery 之 Task是什么 目录 [源码解析] 并行分布式任务队列 Celery 之 Task是什么 0x00 摘要 0x01 思考出发点 0x02 示例代码 ...

  2. [源码解析] 并行分布式任务队列 Celery 之 消费动态流程

    [源码解析] 并行分布式任务队列 Celery 之 消费动态流程 目录 [源码解析] 并行分布式任务队列 Celery 之 消费动态流程 0x00 摘要 0x01 来由 0x02 逻辑 in komb ...

  3. [源码解析] 并行分布式任务队列 Celery 之 多进程模型

    [源码解析] 并行分布式任务队列 Celery 之 多进程模型 目录 [源码解析] 并行分布式任务队列 Celery 之 多进程模型 0x00 摘要 0x01 Consumer 组件 Pool boo ...

  4. [源码解析] 并行分布式任务队列 Celery 之 负载均衡

    [源码解析] 并行分布式任务队列 Celery 之 负载均衡 目录 [源码解析] 并行分布式任务队列 Celery 之 负载均衡 0x00 摘要 0x01 负载均衡 1.1 哪几个 queue 1.1 ...

  5. [源码解析] 并行分布式框架 Celery 之 Lamport 逻辑时钟 & Mingle

    [源码解析] 并行分布式框架 Celery 之 Lamport 逻辑时钟 & Mingle 目录 [源码解析] 并行分布式框架 Celery 之 Lamport 逻辑时钟 & Ming ...

  6. [源码分析] 并行分布式任务队列 Celery 之 Timer & Heartbeat

    [源码分析] 并行分布式任务队列 Celery 之 Timer & Heartbeat 目录 [源码分析] 并行分布式任务队列 Celery 之 Timer & Heartbeat 0 ...

  7. [源码解析] 并行分布式框架 Celery 之架构 (2)

    [源码解析] 并行分布式框架 Celery 之架构 (2) 目录 [源码解析] 并行分布式框架 Celery 之架构 (2) 0x00 摘要 0x01 上文回顾 0x02 worker的思考 2.1 ...

  8. [源码解析] 并行分布式框架 Celery 之架构 (1)

    [源码解析] 并行分布式框架 Celery 之架构 (1) 目录 [源码解析] 并行分布式框架 Celery 之架构 (1) 0x00 摘要 0x01 Celery 简介 1.1 什么是 Celery ...

  9. [源码解析] 并行分布式框架 Celery 之 worker 启动 (1)

    [源码解析] 并行分布式框架 Celery 之 worker 启动 (1) 目录 [源码解析] 并行分布式框架 Celery 之 worker 启动 (1) 0x00 摘要 0x01 Celery的架 ...

随机推荐

  1. POJ_1227 Jack Straws 【二维平面判两线段相交】

    一 题面 POJ1127 二 分析 在平面几何中,判断两线段相交的方法一般是使用跨立实验.但是这题考虑了非严格相交,即如何两个线段刚好端点相交则也是相交的,所以还需要使用快速排斥实验. 这里参考并引用 ...

  2. 《Selenium自动化测试实战:基于Python》Selenium自动化测试框架入门

    第1章  Selenium自动化测试框架入门 1.1  Selenium自动化测试框架概述 说到目前流行的自动化测试工具,相信只要做过软件测试相关工作,就一定听说过Selenium. 图1-1是某企业 ...

  3. java常用算法笔记

    1.将一个10进制的c转换为n进制 String s=new BigInteger(c+"",10).toString(n); 2. 求一个解退出 System.exit(0): ...

  4. Manjaro Linux平台用pyinstaller打包python可执行文件

    技术背景 当我们创建一个python项目,最终的成果如果希望用户能够不依赖于python源代码也能够正常的执行,就会比较的人性化.因为源代码数量众多,很难让每个用户都自行管理所有的源代码,因此我们需要 ...

  5. Centos7使用yum安装RabbitMq以及配置

    RabbitMQ是基于AMQP的一款消息管理系统,是基于erlang语言开发的! 消息队列,即MQ,Message Queue:消息队列是典型的:生产者.消费者模型.生产者不断向消息队列中生产消息,消 ...

  6. 体验用yarp当网关

    Yarp是微软开源的一个用.net实现的反向代理工具包,github库就叫reverse-proxy(反向代理)(吐槽一下微软起名字233333) nuget包preview9之前都叫Microsof ...

  7. 亲测有效JS中9种数组去重方法

    码文不易,转载请带上本文链接,感谢~ https://www.cnblogs.com/echoyya/p/14555831.html 目录 码文不易,转载请带上本文链接,感谢~ https://www ...

  8. vmstat-观察进程上线文切换

    vmstat 是一款指定采样周期和次数的功能性监测工具,我们可以看到,它不仅可以统计内存的使用情况,还可以观测到 CPU 的使用率.swap 的使用情况.但 vmstat 一般很少用来查看内存的使用情 ...

  9. Java高并发测试框架JCStress

    前言 如果要研究高并发,一般会借助高并发工具来进行测试.JCStress(Java Concurrency Stress)它是OpenJDK中的一个高并发测试工具,它可以帮助我们研究在高并发场景下JV ...

  10. Jenkins 实现Gitlab事件自动触发Jenkins构建及钉钉消息推送

    实现Gitlab事件自动触发Jenkins构建及钉钉消息推送 实践环境 GitLab Community Edition 12.6.4 Jenkins 2.284 Post build task 1. ...