Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

disconnect gracefully in an unexpected exit #345

Closed
wants to merge 9 commits into from

Conversation

das7pad
Copy link
Contributor

@das7pad das7pad commented Jul 5, 2017

The UI currently does not cancel the long-polling request on an unexpected error, for example a SystemExit or KeyboardInterrupt seen in #336.
The cleanup is then done by the garbage collector of python, which does not play nice with asyncio.

Running hangups.client.Client.disconnect would raise the KeyboardInterrupt or SystemExit again, we need to discard a raised Exception manually.

if self._listen_future._exception is not None:
logger.info('disconnect: discard %s',
repr(self._listen_future._exception))
self._client._listen_future._exception = None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid the protected access, could we instead call self._listen_future.result() and catch the exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A broad except is needed then. dde895c solves this without a raised Exception.

@das7pad
Copy link
Contributor Author

das7pad commented Jul 7, 2017

Caught KeyboardInterrupt, exiting abnormally
Exception ignored in: <bound method Task.del of <Task pending coro=<<aiohttp.client._RequestContextManager object at 0x7f42a2bd90a8>() running at <...>/hangups/http_utils.py:76> wait_for= cb=[_release_waiter(<Future pendi...sk._wakeup()]>)() at /usr/local/lib/python3.5/asyncio/tasks.py:358]>>
Traceback (most recent call last):
File "/usr/local/lib/python3.5/asyncio/tasks.py", line 92, in del
File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1289, in call_exception_handler
File "/usr/local/lib/python3.5/logging/init.py", line 1309, in error
File "/usr/local/lib/python3.5/logging/init.py", line 1416, in _log
File "/usr/local/lib/python3.5/logging/init.py", line 1426, in handle
File "/usr/local/lib/python3.5/logging/init.py", line 1488, in callHandlers
File "/usr/local/lib/python3.5/logging/init.py", line 856, in handle
File "/usr/local/lib/python3.5/logging/init.py", line 1048, in emit
File "/usr/local/lib/python3.5/logging/init.py", line 1038, in _open
NameError: name 'open' is not defined
Exception ignored in: <bound method Task.del of <Task pending coro=<Conversation.send_message() running at <...>/hangups/conversation.py:446> wait_for= cb=[ConversationWidget._on_message_sent()]>>
Traceback (most recent call last):
File "/usr/local/lib/python3.5/asyncio/tasks.py", line 92, in del
File "/usr/local/lib/python3.5/asyncio/base_events.py", line 1289, in call_exception_handler
File "/usr/local/lib/python3.5/logging/init.py", line 1309, in error
File "/usr/local/lib/python3.5/logging/init.py", line 1416, in _log
File "/usr/local/lib/python3.5/logging/init.py", line 1426, in handle
File "/usr/local/lib/python3.5/logging/init.py", line 1488, in callHandlers
File "/usr/local/lib/python3.5/logging/init.py", line 856, in handle
File "/usr/local/lib/python3.5/logging/init.py", line 1048, in emit
File "/usr/local/lib/python3.5/logging/init.py", line 1038, in _open
NameError: name 'open' is not defined

A KeyboardInterrupt while sending a message.

@das7pad
Copy link
Contributor Author

das7pad commented Jul 7, 2017

The fix in 1348c20 has a hardcoded delay of 3 seconds for resources (especially connections) to be closed internally after sending.
Setting the delay to 0.1 was already sufficient on my hosts to destruct the ssl-connections gracefully, but a busy/very slow host might need more time to do so - see the second part of aio-libs/aiohttp#1925 (comment) and further comments.

@tdryer
Copy link
Owner

tdryer commented Jul 9, 2017

Could we cancel the task instead of waiting for it to complete?

The UI can start some other tasks as well. As a more general solution, could we keep track of them all and cancel each one on exit?

This patch also seems to work for turning KeyboardInterrupt into a graceful exit.

@das7pad
Copy link
Contributor Author

das7pad commented Jul 10, 2017

The loop will likely stop before the cleanup of resources used inside the tasks finished and finally branches completed. A common traceback is shown above - the garbage collector cleaned up in a wrong sequence and resources are missing; another one could be caused by a call on loop.call_exception_handler after the loop got closed.
In addition a task spawned by asyncio.shield(<coro>) might not complete. (which the hangups core does not use)

If the sequence of completion does not matter, asyncio.gather might be useful:

tasks = [<one>, <two>, ...]
# run 
for task in tasks:
    tasks.cancel()
await asyncio.gather(*tasks, return_exceptions=True)

The diff works great for the KeyboardInterrupt case, but still ignores a low-level error. A low-level error could be caused by a change in aiohttp which we support in a wide range of versions, however not all of them are completely backwardscompatible and could raise an Exception. The UI/Terminal should stay clean and any traceback better goes into the log.

@tdryer
Copy link
Owner

tdryer commented Jul 10, 2017

The loop will likely stop before the cleanup of resources used inside the tasks finished and finally branches completed.

Is that a problem if we re-start the loop and wait for the cancelled tasks to finish?

The diff works great for the KeyboardInterrupt case, but still ignores a low-level error. A low-level error could be caused by a change in aiohttp which we support in a wide range of versions, however not all of them are completely backwardscompatible and could raise an Exception. The UI/Terminal should stay clean and any traceback better goes into the log.

I'd actually prefer unexpected exceptions to exit hangups and print a traceback. That makes it obvious when there's a bug.

@das7pad
Copy link
Contributor Author

das7pad commented Jul 11, 2017

Is that a problem if we re-start the loop and wait for the cancelled tasks to finish?

re-start at which point? multiple loop.run_until_complete() are no problem.
After loop.close() there is no way back and pending or scheduled tasks are discard.

I'd actually prefer unexpected exceptions to exit hangups and print a traceback. That makes it obvious when there's a bug.

Ok. The traceback of the raised low-level Exception will reach the terminal and hangups will exit, but the Exceptions or resource warnings this forceful exit would result in, will not be raised as we wait for and close the resources graceful in the recent commits.

@das7pad das7pad force-pushed the disconnect-gracefully branch from 5f9067e to 34cff59 Compare August 9, 2017 11:05
@tdryer
Copy link
Owner

tdryer commented Jan 8, 2018

Started working on another approach in #378.

@tdryer
Copy link
Owner

tdryer commented Jan 10, 2018

Superseded by #378.

@tdryer tdryer closed this Jan 10, 2018
@das7pad das7pad deleted the disconnect-gracefully branch January 14, 2018 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants