在多个消费者之间共享一个动态启动的工作者

时间:2018-11-08 15:17:06

标签: python-3.x python-asyncio

我正在构建一个工人类,该类使用asyncio连接到外部事件流。它是单个流,但是有多个使用者可以启用它。目标是仅在一个或多个消费者需要时才保持连接。

我的要求如下:

  • 工作者实例是在使用者第一次需要它时动态创建的。
  • 当其他使用者随后需要它时,他们会重复使用相同的工作程序实例。
  • 最后一个使用者关闭流时,会清理其资源。

听起来很简单。但是,启动顺序引起了我的问题,因为它本身是异步的。因此,假设此接口:

class Stream:
    async def start(self, *, timeout=DEFAULT_TIMEOUT):
        pass
    async def stop(self):
        pass

我有以下几种情况:

方案1 -启动时发生异常

  • 消费者1请求工作人员开始。
  • 工人启动顺序开始
  • 消费者2请求工作人员开始。
  • 工人启动顺序引发异常。
  • 两个使用者都应在调用start()时看到异常。

方案2 -部分异步取消

  • 消费者1请求工作人员开始。
  • 工人启动顺序开始
  • 消费者2请求工作人员开始。
  • 消费者1被取消。
  • 工人启动顺序完成。
  • 消费者2应该看到一个成功的开始。

方案3 -完全异步取消

  • 消费者1请求工作人员开始。
  • 工人启动顺序开始
  • 消费者2请求工作人员开始。
  • 消费者1被取消。
  • 消费2被取消。
  • 因此,
  • 工人启动顺序必须取消。

我很难涵盖所有情况,而又没有任何竞争条件,也没有一团乱麻的Future或Event对象的意粉。


这是尝试写start()的尝试。完成启动序列时,它依靠_worker()设置名为asyncio.Event的{​​{1}}:

self._worker_ready

似乎可以工作,但它似乎太复杂了,很难测试:工人有许多async def start(self, timeout=None): assert not self.closing if not self._task: self._task = asyncio.ensure_future(self._worker()) # Wait until worker is ready, has failed, or timeout triggers try: self._waiting_start += 1 wait_ready = asyncio.ensure_future(self._worker_ready.wait()) done, pending = await asyncio.wait( [self._task, wait_ready], return_when=asyncio.FIRST_COMPLETED, timeout=timeout ) except asyncio.CancelledError: wait_ready.cancel() if self._waiting_start == 1: self.closing = True self._task.cancel() with suppress(asyncio.CancelledError): await self._task # let worker shutdown raise finally: self._waiting_start -= 1 # worker failed to start - either throwing or timeout triggering if not self._worker_ready.is_set(): self.closing = True self._task.cancel() wait_ready.cancel() try: await self._task # let worker shutdown except asyncio.CancelledError: raise FeedTimeoutError('stream failed to start within %ss' % timeout) else: assert False, 'worker must propagate the exception' 分,如果我想尽一切可能的话会导致爆炸性爆炸取消点和执行命令。

我需要更好的方法。因此,我想知道:

  • 我的要求合理吗?
  • 是否有共同的模式来做到这一点?
  • 我的问题会引起一些代码异味吗?

2 个答案:

答案 0 :(得分:2)

您的要求听起来很合理。我会尝试通过将start替换为将来的字符(在这种情况下为任务)来简化Event,使用它既可以等待启动完成,也可以传播过程中发生的异常(如果有) 。像这样:

class Stream:
    async def start(self, *, timeout=DEFAULT_TIMEOUT):
        loop = asyncio.get_event_loop()
        if self._worker_startup_task is None:
            self._worker_startup_task = \
                loop.create_task(self._worker_startup())

        self._add_user()
        try:
            await asyncio.shield(asyncio.wait_for(
                self._worker_startup_task, timeout))
        except:
            self._rm_user()
            raise

    async def _worker_startup(self):
        loop = asyncio.get_event_loop()
        await asyncio.sleep(1)      # ...
        self._worker_task = loop.create_task(self._worker())

在此代码中,工作程序启动与工作程序协程分离,并且也移至单独的任务。可以等待这个单独的任务,并且不需要专用的Event,但是更重要的是,它允许方案1和2由相同的代码处理。即使有人取消了第一个消费者,工作人员启动任务也不会被取消-取消只是意味着等待它的消费者减少了。

因此,在取消消费者的情况下,await self._worker_startup_task对于其他消费者来说也很好,而在工作程序启动时发生实际异常的情况下,所有其他服务员将看到相同的异常,因为任务已完成。 / p>

方案3应该自动运行,因为无论出于何种原因,我们总是取消用户无法再看到的启动。如果消费者因为启动本身失败而走了,那么self._worker_startup_task将已经完成(有例外),并且取消操作将是无效的。如果是因为所有使用者都在等待启动时​​被取消,那么self._worker_startup_task.cancel()将按照方案3的要求取消启动顺序。

其余代码看起来像这样(未经测试):

    def __init__(self):
        self._users = 0
        self._worker_startup = None

    def _add_user(self):
        self._users += 1

    def _rm_user(self):
        self._users -= 1
        if self._users:
            return
        self._worker_startup_task.cancel()
        self._worker_startup_task = None
        if self._worker_task is not None:
            self._worker_task.cancel()
            self._worker_task = None

    async def stop(self):
        self._rm_user()

    async def _worker(self):
        # actual worker...
        while True:
            await asyncio.sleep(1)

答案 1 :(得分:1)

通过先前的测试并整合了@ user4815162342的建议,我想出了一个可重用的解决方案:

st = SharedTask(test())
task1 = asyncio.ensure_future(st.wait())
task2 = asyncio.ensure_future(st.wait(timeout=15))
task3 = asyncio.ensure_future(st.wait())

这做对了:task2在15秒后取消自身。除非所有任务都被取消,否则取消任务对test()无效。在这种情况下,要取消的最后一个任务将手动取消test()并等待取消处理完成。

如果通过了协程,则仅在第一个任务开始等待时安排它。

最后,等待共享任务完成后,只是立即产生其结果(似乎很明显,但初始版本却没有)。

import asyncio
from contextlib import suppress


class SharedTask:
    __slots__ = ('_clients', '_task')

    def __init__(self, task):
        if not (asyncio.isfuture(task) or asyncio.iscoroutine(task)):
            raise TypeError('task must be either a Future or a coroutine object')
        self._clients = 0
        self._task = task

    @property
    def started(self):
        return asyncio.isfuture(self._task)

    async def wait(self, *, timeout=None):
        self._task = asyncio.ensure_future(self._task)

        self._clients += 1
        try:
            return await asyncio.wait_for(asyncio.shield(self._task), timeout=timeout)
        except:
            self._clients -= 1
            if self._clients == 0 and not self._task.done():
                self._task.cancel()
                with suppress(asyncio.CancelledError):
                    await self._task
            raise

    def cancel(self):
        if asyncio.iscoroutine(self._task):
            self._task.close()
        elif asyncio.isfuture(self._task):
            self._task.cancel()

重新引发任务异常取消(在注释中提到)是有意的。它允许这种模式:

async def my_task():
    try:
        await do_stuff()
    except asyncio.CancelledError as exc:
        await flush_some_stuff()     # might raise an exception
        raise exc

客户端可以取消共享任务并处理可能导致的异常,无论my_task是否包裹在SharedTask中,其工作原理都相同。