From: G S Niteesh Babu <niteesh.gs@gmail.com>
To: qemu-devel@nongnu.org
Cc: ehabkost@redhat.com, kchamart@redhat.com, jsnow@redhat.com,
armbru@redhat.com, wainersm@redhat.com,
G S Niteesh Babu <niteesh.gs@gmail.com>,
stefanha@redhat.com, crosa@redhat.com, eblake@redhat.com
Subject: [PATCH v4 7/7] python/aqmp-tui: Add QMP connection manager
Date: Thu, 19 Aug 2021 23:08:31 +0530 [thread overview]
Message-ID: <20210819173831.23515-8-niteesh.gs@gmail.com> (raw)
In-Reply-To: <20210819173831.23515-1-niteesh.gs@gmail.com>
The connection manager will take care of connecting/disconnecting
to the server. This will also try to reconnect to the server in
certain situations where the client has been disconnected due to
some error condition.
Signed-off-by: G S Niteesh Babu <niteesh.gs@gmail.com>
---
python/qemu/aqmp/aqmp_tui.py | 127 +++++++++++++++++++++++++++++------
1 file changed, 105 insertions(+), 22 deletions(-)
diff --git a/python/qemu/aqmp/aqmp_tui.py b/python/qemu/aqmp/aqmp_tui.py
index 03d4808acd..c47abe0a25 100644
--- a/python/qemu/aqmp/aqmp_tui.py
+++ b/python/qemu/aqmp/aqmp_tui.py
@@ -35,8 +35,9 @@
import urwid_readline
from ..qmp import QEMUMonitorProtocol, QMPBadPortError
+from .error import ProtocolError
from .message import DeserializationError, Message, UnexpectedTypeError
-from .protocol import ConnectError
+from .protocol import ConnectError, Runstate
from .qmp_client import ExecInterruptedError, QMPClient
from .util import create_task, pretty_traceback
@@ -128,17 +129,26 @@ class App(QMPClient):
Initializes the widgets and starts the urwid event loop.
"""
- def __init__(self, address: Union[str, Tuple[str, int]]) -> None:
+ def __init__(self, address: Union[str, Tuple[str, int]], num_retries: int,
+ retry_delay: Optional[int]) -> None:
"""
Initializes the TUI.
:param address:
Address of the server to connect to.
+ :param num_retries:
+ The number of times to retry before stopping to reconnect.
+ :param retry_delay:
+ The delay(sec) before each retry
"""
urwid.register_signal(type(self), UPDATE_MSG)
self.window = Window(self)
self.address = address
self.aloop: Optional[asyncio.AbstractEventLoop] = None
+ self.num_retries = num_retries
+ self.retry_delay = retry_delay if retry_delay else 2
+ self.retry: bool = False
+ self.disconnecting: bool = False
super().__init__()
def add_to_history(self, msg: str, level: Optional[str] = None) -> None:
@@ -212,10 +222,10 @@ def handle_event(self, event: Message) -> None:
"""
try:
await self._raw(msg, assign_id='id' not in msg)
- except ExecInterruptedError:
- logging.info('Error server disconnected before reply')
+ except ExecInterruptedError as err:
+ logging.info('Error server disconnected before reply %s', str(err))
self.add_to_history('Server disconnected before reply', 'ERROR')
- self._set_status("[Server Disconnected]")
+ await self.disconnect()
except Exception as err:
logging.error('Exception from _send_to_server: %s', str(err))
raise err
@@ -237,10 +247,10 @@ def cb_send_to_server(self, raw_msg: str) -> None:
create_task(self._send_to_server(msg))
except (ValueError, TypeError) as err:
logging.info('Invalid message: %s', str(err))
- self.add_to_history(f'{raw_msg}: {err}')
+ self.add_to_history(f'{raw_msg}: {err}', 'ERROR')
except (DeserializationError, UnexpectedTypeError) as err:
logging.info('Invalid message: %s', err.error_message)
- self.add_to_history(f'{raw_msg}: {err.error_message}')
+ self.add_to_history(f'{raw_msg}: {err.error_message}', 'ERROR')
def unhandled_input(self, key: str) -> None:
"""
@@ -266,18 +276,32 @@ def kill_app(self) -> None:
:raise Exception: When an unhandled exception is caught.
"""
- # It is ok to call disconnect even in disconnect state
+ await self.disconnect()
+ logging.debug('Disconnect finished. Exiting app')
+ raise urwid.ExitMainLoop()
+
+ async def disconnect(self) -> None:
+ """
+ Overrides the disconnect method to handle the errors locally.
+ """
+ if self.disconnecting:
+ return
try:
- await self.disconnect()
- logging.debug('Disconnect finished. Exiting app')
- except EOFError:
- # We receive an EOF during disconnect, ignore that
- pass
+ self.disconnecting = True
+ await super().disconnect()
+ self.retry = False
+ except EOFError as err:
+ logging.info('disconnect: %s', str(err))
+ self.retry = True
+ except ProtocolError as err:
+ logging.info('disconnect: %s', str(err))
+ self.retry = False
except Exception as err:
- logging.info('_kill_app: %s', str(err))
- # Let the app crash after providing a proper stack trace
+ logging.error('disconnect: Unhandled exception %s', str(err))
+ self.retry = False
raise err
- raise urwid.ExitMainLoop()
+ finally:
+ self.disconnecting = False
def _set_status(self, msg: str) -> None:
"""
@@ -301,18 +325,72 @@ def _get_formatted_address(self) -> str:
addr = f'{self.address}'
return addr
- async def connect_server(self) -> None:
+ async def _initiate_connection(self) -> Optional[ConnectError]:
+ """
+ Tries connecting to a server a number of times with a delay between
+ each try. If all retries failed then return the error faced during
+ the last retry.
+
+ :return: Error faced during last retry.
+ """
+ current_retries = 0
+ err = None
+
+ # initial try
+ await self.connect_server()
+ while self.retry and current_retries < self.num_retries:
+ logging.info('Connection Failed, retrying in %d', self.retry_delay)
+ status = f'[Retry #{current_retries} ({self.retry_delay}s)]'
+ self._set_status(status)
+
+ await asyncio.sleep(self.retry_delay)
+
+ err = await self.connect_server()
+ current_retries += 1
+ # If all retries failed report the last error
+ if err:
+ logging.info('All retries failed: %s', err)
+ return err
+ return None
+
+ async def manage_connection(self) -> None:
+ """
+ Manage the connection based on the current run state.
+
+ A reconnect is issued when the current state is IDLE and the number
+ of retries is not exhausted.
+ A disconnect is issued when the current state is DISCONNECTING.
+ """
+ while True:
+ if self.runstate == Runstate.IDLE:
+ err = await self._initiate_connection()
+ # If retry is still true then, we have exhausted all our tries.
+ if err:
+ self._set_status(f'[Error: {err.error_message}]')
+ else:
+ addr = self._get_formatted_address()
+ self._set_status(f'[Connected {addr}]')
+ elif self.runstate == Runstate.DISCONNECTING:
+ self._set_status('[Disconnected]')
+ await self.disconnect()
+ # check if a retry is needed
+ if self.runstate == Runstate.IDLE:
+ continue
+ await self.runstate_changed()
+
+ async def connect_server(self) -> Optional[ConnectError]:
"""
Initiates a connection to the server at address `self.address`
and in case of a failure, sets the status to the respective error.
"""
try:
await self.connect(self.address)
- addr = self._get_formatted_address()
- self._set_status(f'Connected to {addr}')
+ self.retry = False
except ConnectError as err:
logging.info('connect_server: ConnectError %s', str(err))
- self._set_status(f'[ConnectError: {err.error_message}]')
+ self.retry = True
+ return err
+ return None
def run(self, debug: bool = False) -> None:
"""
@@ -341,7 +419,7 @@ def run(self, debug: bool = False) -> None:
event_loop=event_loop)
create_task(self.wait_for_events(), self.aloop)
- create_task(self.connect_server(), self.aloop)
+ create_task(self.manage_connection(), self.aloop)
try:
main_loop.run()
except Exception as err:
@@ -566,6 +644,11 @@ def main() -> None:
parser = argparse.ArgumentParser(description='AQMP TUI')
parser.add_argument('qmp_server', help='Address of the QMP server. '
'Format <UNIX socket path | TCP addr:port>')
+ parser.add_argument('--num-retries', type=int, default=10,
+ help='Number of times to reconnect before giving up.')
+ parser.add_argument('--retry-delay', type=int,
+ help='Time(s) to wait before next retry. '
+ 'Default action is to wait 2s between each retry.')
parser.add_argument('--log-file', help='The Log file name')
parser.add_argument('--log-level', default='WARNING',
help='Log level <CRITICAL|ERROR|WARNING|INFO|DEBUG|>')
@@ -581,7 +664,7 @@ def main() -> None:
except QMPBadPortError as err:
parser.error(str(err))
- app = App(address)
+ app = App(address, args.num_retries, args.retry_delay)
root_logger = logging.getLogger()
root_logger.setLevel(logging.getLevelName(args.log_level))
--
2.17.1
next prev parent reply other threads:[~2021-08-19 17:45 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-08-19 17:38 [PATCH v4 0/7] AQMP TUI Draft G S Niteesh Babu
2021-08-19 17:38 ` [PATCH v4 1/7] python: disable pylint errors for aqmp-tui G S Niteesh Babu
2021-08-19 17:38 ` [PATCH v4 2/7] python: Add dependencies for AQMP TUI G S Niteesh Babu
2021-08-21 19:54 ` John Snow
2021-08-19 17:38 ` [PATCH v4 3/7] python/aqmp-tui: Add AQMP TUI draft G S Niteesh Babu
2021-08-21 20:05 ` John Snow
2021-08-21 22:21 ` Niteesh G. S.
2021-08-22 7:33 ` John Snow
2021-08-23 10:37 ` Niteesh G. S.
2021-08-19 17:38 ` [PATCH v4 4/7] python: Add entry point for aqmp-tui G S Niteesh Babu
2021-08-19 17:38 ` [PATCH v4 5/7] python: add optional pygments dependency G S Niteesh Babu
2021-08-19 17:38 ` [PATCH v4 6/7] python/aqmp-tui: Add syntax highlighting G S Niteesh Babu
2021-08-19 17:38 ` G S Niteesh Babu [this message]
2021-08-21 4:09 ` [PATCH v4 0/7] AQMP TUI Draft John Snow
2021-08-21 21:20 ` Niteesh G. S.
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20210819173831.23515-8-niteesh.gs@gmail.com \
--to=niteesh.gs@gmail.com \
--cc=armbru@redhat.com \
--cc=crosa@redhat.com \
--cc=eblake@redhat.com \
--cc=ehabkost@redhat.com \
--cc=jsnow@redhat.com \
--cc=kchamart@redhat.com \
--cc=qemu-devel@nongnu.org \
--cc=stefanha@redhat.com \
--cc=wainersm@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).