简体   繁体   English

如何用电机彻底关闭Change Streams?

[英]How to cleanly shutdown Change Streams with Motor?

TL; TL; DR DR

This was indeed a bug in Motor 1.2.0 that was promptly fixed by A. Jesse Jiryu Davis and is available in version 1.2.1 or greater of the driver. 这确实是Motor 1.2.0中的一个错误,由A. Jesse Jiryu Davis迅速修复,并且可以在1.2.1或更高版本的驱动程序中使用。

Original Question 原始问题

I wrote a program to monitor changes to a MongoDB collection using its new Change Stream feature, on Python 3. Here's the MCVE: 我编写了一个程序,用于在Python 3上使用其新的Change Stream功能监视MongoDB集合的更改。这是MCVE:

from asyncio import get_event_loop, CancelledError
from contextlib import suppress
from motor.motor_asyncio import AsyncIOMotorClient

async def watch(collection):
    async with collection.watch([]) as stream:
        async for change in stream:
            print(change)

async def cleanup():
    task.cancel()

    with suppress(CancelledError):
        await task

if __name__ == '__main__':
    conn = AsyncIOMotorClient()
    loop = get_event_loop()
    task = loop.create_task(watch(conn.database.collection))  # Replace with a real collection.

    try:
        loop.run_forever()

    except KeyboardInterrupt:
        pass

    finally:
        loop.run_until_complete(cleanup())
        loop.shutdown_asyncgens()
        loop.close()

When I kill the program with CTRL+C, it raises three different exceptions. 当我用CTRL + C杀死程序时,它引发了三个不同的异常。

^Cexception calling callback for <Future at 0x102efea58 state=finished raised InvalidStateError>
Traceback (most recent call last):
  File "/Users/viotti/motor/lib/python3.6/site-packages/motor/core.py", line 1259, in _next
    change = self.delegate.next()
  File "/Users/viotti/motor/lib/python3.6/site-packages/pymongo/change_stream.py", line 79, in next
    change = self._cursor.next()
  File "/Users/viotti/motor/lib/python3.6/site-packages/pymongo/command_cursor.py", line 292, in next
    raise StopIteration
StopIteration

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/Users/viotti/motor/lib/python3.6/site-packages/motor/core.py", line 1264, in _next
    future.set_exception(StopAsyncIteration())
asyncio.base_futures.InvalidStateError: invalid state

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/concurrent/futures/_base.py", line 324, in _invoke_callbacks
    callback(self)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/futures.py", line 414, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 620, in call_soon_threadsafe
    self._check_closed()
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

Is there a way to make that program close silently? 有没有办法让这个程序默默关闭?

I'm testing with Python 3.6.4, Motor 1.2 and pymongo 3.6.0 on macOS Sierra. 我在macOS Sierra上测试Python 3.6.4,Motor 1.2和pymongo 3.6.0。

I think your code is correct, problem on motor 's side. 我认为你的代码是正确的, motor方面的问题。

While investigating I found two problems: 在调查时我发现了两个问题:

  1. If at the moment you close loop connection is not established, you'll get exception calling callback for <Future error since loop closed before async callbacks done. 如果此时您没有建立闭环连接,那么您将获得exception calling callback for <Future自从异步回调完成之前循环关闭后的exception calling callback for <Future错误。 It seems not to be related to async generators or streams, but to any motor usage. 它似乎与异步生成器或流无关,而与任何motor使用有关。
  2. AgnosticChangeStream async iteration mechanism ( _next function ) is written without thinking of case when it cancelled. AgnosticChangeStream异步迭代机制( _next函数 )是在不考虑取消时的情况下编写的。 Try of setting exception to cancelled future leads to InvalidStateError . 尝试将异常设置为取消将来会导致InvalidStateError

This code demonstrates two problems and possible workarounds: 此代码演示了两个问题和可能的解决方法:

import types
import asyncio
from contextlib import suppress
from motor.motor_asyncio import AsyncIOMotorClient


async def test():
    while True:
        await asyncio.sleep(0.1)


async def cleanup(task):
    task.cancel()
    with suppress(asyncio.CancelledError):
        await task


def _next(self, future):
    try:
        if not self.delegate:
            self.delegate = self._collection.delegate.watch(**self._kwargs)

        change = self.delegate.next()
        self._framework.call_soon(self.get_io_loop(),
                                  future.set_result,
                                  change)
    except StopIteration:
        future.set_exception(StopAsyncIteration())
    except Exception as exc:

        # CASE 2:
        # Cancellation of async iteration (and future with it) happens immediately
        # and trying to set exception to cancelled future leads to InvalidStateError,
        # we should prevent it:
        if future.cancelled():
            return

        future.set_exception(exc)


async def watch(collection):
    async with collection.watch([]) as stream:

        # Patch stream to achieve CASE 2:
        stream._next = types.MethodType(_next, stream)

        async for change in stream:
            print(change)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tmp = asyncio.ensure_future(test())  # Way to receive KeyboardInterrupt immediately.

    client = AsyncIOMotorClient()
    collection = client.test_database.test_collection
    task = asyncio.ensure_future(watch(collection))

    try:
        loop.run_forever()
    except KeyboardInterrupt:
        print('KeyboardInterrupt')
    finally:
        loop.run_until_complete(cleanup(tmp))
        loop.run_until_complete(cleanup(task))

        # CASE 1:
        # Looks like propagating KeyboardInterrupt doesn't affect motor's try
        # to establish connection to db and I didn't find a way to stop this manually.
        # We should keep event loop alive until we receive ServerSelectionTimeoutError
        # and motor would be able to execute it's asyncio callbacks:
        loop.run_until_complete(asyncio.sleep(client.server_selection_timeout))

        loop.shutdown_asyncgens()
        loop.close()

It finishes without warnings/exceptions (on my machine at least) due to added fixes. 由于添加了修复,它完成时没有警告/异常(至少在我的机器上)。

I don't recommend you to use hacks above! 我不建议你使用上面的黑客! It's only to demonstrate problem places and possible solutions. 它只是为了展示问题所在和可能的解决方案。 I'm not sure it does everything properly. 我不确定它能做好一切。

Instead I advice you to create issue at motor user group / Jira appending there your snippet and probably my answer and wait until bug would be fixed. 相反,我建议你在汽车用户组/ Jira创建问题,在那里附加你的片段,可能是我的答案,等到bug修复。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM