【发布时间】:2018-11-12 20:41:13
【问题描述】:
我正在构建一个使用 asyncio 连接到外部事件流的工作类。它是一个单一的流,但有几个消费者可以启用它。目标是仅在一个或多个消费者需要时保持连接。
我的要求如下:
- worker 实例是在消费者第一次需要时动态创建的。
- 当其他消费者需要它时,他们会重复使用同一个工作实例。
- 当最后一个消费者关闭流时,它会清理其资源。
这听起来很简单。但是,启动顺序引起了我的问题,因为它本身是异步的。因此,假设这个接口:
class Stream:
async def start(self, *, timeout=DEFAULT_TIMEOUT):
pass
async def stop(self):
pass
我有以下几种情况:
场景 1 - 启动时出现异常
- Consumer 1 请求 worker 启动。
- 工作器启动序列开始
- Consumer 2 请求 worker 启动。
- Worker 启动序列引发异常。
- 两个消费者都应该将异常视为调用 start() 的结果。
场景 2 - 部分异步取消
- Consumer 1 请求 worker 启动。
- 工作器启动序列开始
- Consumer 2 请求 worker 启动。
- 消费者 1 被取消。
- 工作器启动序列完成。
- 消费者 2 应该看到一个成功的开始。
场景 3 - 完成异步取消
- Consumer 1 请求 worker 启动。
- 工作器启动序列开始
- Consumer 2 请求 worker 启动。
- 消费者 1 被取消。
- 消费者 2 被取消。
- 因此必须取消工作器启动序列。
我很难涵盖所有场景,而不会遇到任何竞争条件以及裸露的 Future 或 Event 对象的意大利面条式混乱。
这是写start()的尝试。它依赖于_worker() 在完成启动序列时设置一个名为self._worker_ready 的asyncio.Event:
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'
似乎可以工作,但它似乎太复杂了,而且真的很难测试:工人有很多 await 点,如果我要尝试所有可能的取消点,会导致组合爆炸和执行命令。
我需要一个更好的方法。我因此想知道:
- 我的要求合理吗?
- 是否有常见的模式来执行此操作?
- 我的问题是否会引起一些代码异味?
【问题讨论】: