01f9005feb
- Backport aqmp patches from upstream which can fix iotest issues * Patches added: python-aqmp-add-__del__-method-to-legacy.patch python-aqmp-add-_session_guard.patch python-aqmp-add-SocketAddrT-to-package-r.patch python-aqmp-add-socket-bind-step-to-lega.patch python-aqmp-add-start_server-and-accept-.patch python-aqmp-copy-type-definitions-from-q.patch python-aqmp-drop-_bind_hack.patch python-aqmp-fix-docstring-typo.patch python-aqmp-Fix-negotiation-with-pre-oob.patch python-aqmp-fix-race-condition-in-legacy.patch Python-aqmp-fix-type-definitions-for-myp.patch python-aqmp-handle-asyncio.TimeoutError-.patch python-aqmp-refactor-_do_accept-into-two.patch python-aqmp-remove-_new_session-and-_est.patch python-aqmp-rename-accept-to-start_serve.patch python-aqmp-rename-AQMPError-to-QMPError.patch python-aqmp-split-_client_connected_cb-o.patch python-aqmp-squelch-pylint-warning-for-t.patch python-aqmp-stop-the-server-during-disco.patch python-introduce-qmp-shell-wrap-convenie.patch python-machine-raise-VMLaunchFailure-exc.patch python-move-qmp-shell-under-the-AQMP-pac.patch python-move-qmp-utilities-to-python-qemu.patch python-qmp-switch-qmp-shell-to-AQMP.patch python-support-recording-QMP-session-to-.patch python-upgrade-mypy-to-0.780.patch - Drop the patches which are workaround to fix iotest issues * Patches dropped: Revert-python-iotests-replace-qmp-with-a.patch Revert-python-machine-add-instance-disam.patch Revert-python-machine-add-sock_dir-prope.patch Revert-python-machine-handle-fast-QEMU-t.patch Revert-python-machine-move-more-variable.patch Revert-python-machine-remove-_remove_mon.patch OBS-URL: https://build.opensuse.org/request/show/966963 OBS-URL: https://build.opensuse.org/package/show/Virtualization/qemu?expand=0&rev=708
141 lines
5.7 KiB
Diff
141 lines
5.7 KiB
Diff
From: John Snow <jsnow@redhat.com>
|
|
Date: Fri, 25 Feb 2022 15:59:39 -0500
|
|
Subject: python/aqmp: add _session_guard()
|
|
MIME-Version: 1.0
|
|
Content-Type: text/plain; charset=UTF-8
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
Git-commit: 40196c23939758abc5300e85333e676196e3ba6d
|
|
|
|
In _new_session, there's a fairly complex except clause that's used to
|
|
give semantic errors to callers of accept() and connect(). We need to
|
|
create a new two-step replacement for accept(), so factoring out this
|
|
piece of logic will be useful.
|
|
|
|
Bolster the comments and docstring here to try and demystify what's
|
|
going on in this fairly delicate piece of Python magic.
|
|
|
|
(If we were using Python 3.7+, this would be an @asynccontextmanager. We
|
|
don't have that very nice piece of magic, however, so this must take an
|
|
Awaitable to manage the Exception contexts properly. We pay the price
|
|
for platform compatibility.)
|
|
|
|
Signed-off-by: John Snow <jsnow@redhat.com>
|
|
Acked-by: Kevin Wolf <kwolf@redhat.com>
|
|
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
|
|
Message-id: 20220225205948.3693480-2-jsnow@redhat.com
|
|
Signed-off-by: John Snow <jsnow@redhat.com>
|
|
Signed-off-by: Li Zhang <lizhang@suse.de>
|
|
---
|
|
python/qemu/aqmp/protocol.py | 89 +++++++++++++++++++++++++-----------
|
|
1 file changed, 62 insertions(+), 27 deletions(-)
|
|
|
|
diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py
|
|
index 33358f5cd72b61bd060b8dea6091..009883f64d011e44dd003e9dcde3 100644
|
|
--- a/python/qemu/aqmp/protocol.py
|
|
+++ b/python/qemu/aqmp/protocol.py
|
|
@@ -317,6 +317,62 @@ class AsyncProtocol(Generic[T]):
|
|
# Section: Session machinery
|
|
# --------------------------
|
|
|
|
+ async def _session_guard(self, coro: Awaitable[None], emsg: str) -> None:
|
|
+ """
|
|
+ Async guard function used to roll back to `IDLE` on any error.
|
|
+
|
|
+ On any Exception, the state machine will be reset back to
|
|
+ `IDLE`. Most Exceptions will be wrapped with `ConnectError`, but
|
|
+ `BaseException` events will be left alone (This includes
|
|
+ asyncio.CancelledError, even prior to Python 3.8).
|
|
+
|
|
+ :param error_message:
|
|
+ Human-readable string describing what connection phase failed.
|
|
+
|
|
+ :raise BaseException:
|
|
+ When `BaseException` occurs in the guarded block.
|
|
+ :raise ConnectError:
|
|
+ When any other error is encountered in the guarded block.
|
|
+ """
|
|
+ # Note: After Python 3.6 support is removed, this should be an
|
|
+ # @asynccontextmanager instead of accepting a callback.
|
|
+ try:
|
|
+ await coro
|
|
+ except BaseException as err:
|
|
+ self.logger.error("%s: %s", emsg, exception_summary(err))
|
|
+ self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
|
|
+ try:
|
|
+ # Reset the runstate back to IDLE.
|
|
+ await self.disconnect()
|
|
+ except:
|
|
+ # We don't expect any Exceptions from the disconnect function
|
|
+ # here, because we failed to connect in the first place.
|
|
+ # The disconnect() function is intended to perform
|
|
+ # only cannot-fail cleanup here, but you never know.
|
|
+ emsg = (
|
|
+ "Unexpected bottom half exception. "
|
|
+ "This is a bug in the QMP library. "
|
|
+ "Please report it to <qemu-devel@nongnu.org> and "
|
|
+ "CC: John Snow <jsnow@redhat.com>."
|
|
+ )
|
|
+ self.logger.critical("%s:\n%s\n", emsg, pretty_traceback())
|
|
+ raise
|
|
+
|
|
+ # CancelledError is an Exception with special semantic meaning;
|
|
+ # We do NOT want to wrap it up under ConnectError.
|
|
+ # NB: CancelledError is not a BaseException before Python 3.8
|
|
+ if isinstance(err, asyncio.CancelledError):
|
|
+ raise
|
|
+
|
|
+ # Any other kind of error can be treated as some kind of connection
|
|
+ # failure broadly. Inspect the 'exc' field to explore the root
|
|
+ # cause in greater detail.
|
|
+ if isinstance(err, Exception):
|
|
+ raise ConnectError(emsg, err) from err
|
|
+
|
|
+ # Raise BaseExceptions un-wrapped, they're more important.
|
|
+ raise
|
|
+
|
|
@property
|
|
def _runstate_event(self) -> asyncio.Event:
|
|
# asyncio.Event() objects should not be created prior to entrance into
|
|
@@ -371,34 +427,13 @@ class AsyncProtocol(Generic[T]):
|
|
"""
|
|
assert self.runstate == Runstate.IDLE
|
|
|
|
- try:
|
|
- phase = "connection"
|
|
- await self._establish_connection(address, ssl, accept)
|
|
-
|
|
- phase = "session"
|
|
- await self._establish_session()
|
|
+ await self._session_guard(
|
|
+ self._establish_connection(address, ssl, accept),
|
|
+ 'Failed to establish connection')
|
|
|
|
- except BaseException as err:
|
|
- emsg = f"Failed to establish {phase}"
|
|
- self.logger.error("%s: %s", emsg, exception_summary(err))
|
|
- self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
|
|
- try:
|
|
- # Reset from CONNECTING back to IDLE.
|
|
- await self.disconnect()
|
|
- except:
|
|
- emsg = "Unexpected bottom half exception"
|
|
- self.logger.critical("%s:\n%s\n", emsg, pretty_traceback())
|
|
- raise
|
|
-
|
|
- # NB: CancelledError is not a BaseException before Python 3.8
|
|
- if isinstance(err, asyncio.CancelledError):
|
|
- raise
|
|
-
|
|
- if isinstance(err, Exception):
|
|
- raise ConnectError(emsg, err) from err
|
|
-
|
|
- # Raise BaseExceptions un-wrapped, they're more important.
|
|
- raise
|
|
+ await self._session_guard(
|
|
+ self._establish_session(),
|
|
+ 'Failed to establish session')
|
|
|
|
assert self.runstate == Runstate.RUNNING
|
|
|