From patchwork Tue Apr 13 15:55:48 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Snow X-Patchwork-Id: 12200869 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-13.5 required=3.0 tests=BAYES_00,DKIM_INVALID, DKIM_SIGNED,HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 570F5C433ED for ; Tue, 13 Apr 2021 15:59:59 +0000 (UTC) Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.kernel.org (Postfix) with ESMTPS id C0125610C8 for ; Tue, 13 Apr 2021 15:59:58 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mail.kernel.org C0125610C8 Authentication-Results: mail.kernel.org; dmarc=fail (p=none dis=none) header.from=redhat.com Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Received: from localhost ([::1]:51488 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1lWLSH-0006Bw-N0 for qemu-devel@archiver.kernel.org; Tue, 13 Apr 2021 11:59:57 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:38428) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1lWLOc-0001Sg-0C for qemu-devel@nongnu.org; Tue, 13 Apr 2021 11:56:10 -0400 Received: from us-smtp-delivery-124.mimecast.com ([216.205.24.124]:46809) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1lWLOX-0000BB-F6 for qemu-devel@nongnu.org; Tue, 13 Apr 2021 11:56:09 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1618329364; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=SfeXp++ZNmfkKdeGgfGhy8Bkbe50OiQTtJtWuExrEss=; b=OuDMJE8fP0P/0eB+ZICAwP2KfiMuZefD7eW2JnOQbrZPt/v9bBhuumhhyluivHpyYzAFDg grhkqQSJCRy1DbOk4mY0Gi/Ka8nvRo0XWtIAXxCIxmMqFH5qPxlyZoPvDnF0m8BVnzVucc P+sGyuXxzEE2DNXnvh1Dux9byxWJgb0= Received: from mimecast-mx01.redhat.com (mimecast-mx01.redhat.com [209.132.183.4]) (Using TLS) by relay.mimecast.com with ESMTP id us-mta-197-ifCkLrEGMwmVxv8rBOIjhQ-1; Tue, 13 Apr 2021 11:56:01 -0400 X-MC-Unique: ifCkLrEGMwmVxv8rBOIjhQ-1 Received: from smtp.corp.redhat.com (int-mx03.intmail.prod.int.phx2.redhat.com [10.5.11.13]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mimecast-mx01.redhat.com (Postfix) with ESMTPS id 40CDC189C440 for ; Tue, 13 Apr 2021 15:56:00 +0000 (UTC) Received: from scv.redhat.com (ovpn-117-61.rdu2.redhat.com [10.10.117.61]) by smtp.corp.redhat.com (Postfix) with ESMTP id 7E2636A034; Tue, 13 Apr 2021 15:55:59 +0000 (UTC) From: John Snow To: qemu-devel@nongnu.org Subject: [PATCH RFC 2/7] error: Error classes and so on. Date: Tue, 13 Apr 2021 11:55:48 -0400 Message-Id: <20210413155553.2660523-3-jsnow@redhat.com> In-Reply-To: <20210413155553.2660523-1-jsnow@redhat.com> References: <20210413155553.2660523-1-jsnow@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 2.79 on 10.5.11.13 Authentication-Results: relay.mimecast.com; auth=pass smtp.auth=CUSA124A263 smtp.mailfrom=jsnow@redhat.com X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Received-SPF: pass client-ip=216.205.24.124; envelope-from=jsnow@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -27 X-Spam_score: -2.8 X-Spam_bar: -- X-Spam_report: (-2.8 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H4=0.001, RCVD_IN_MSPIKE_WL=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: crosa@redhat.com, John Snow , ehabkost@redhat.com, stefanha@redhat.com, armbru@redhat.com Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: "Qemu-devel" May be somewhat hard to make sense of until you see how these classes are used later on. Notably, although I have split QMP's functionality into a "protocol" class and a "QMP" class, representing a separation of the loop mechanisms and the QMP protocol itself, this file was written prior to that split and contains both "generic" and "QMP-specific" error classes. It will have to be split out later, but for the purposes of an RFC where I wanted a quick eyeball on design, I thought it wasn't necessary to clean that up just yet. The MultiException class might warrant a closer inspection, it's the "weirdest" thing here. It's intended to be used internally by the module, but as with all best laid plans, there is always the ability it will somehow leak out into the caller's space through some unforseen mechanism. Signed-off-by: John Snow --- error.py | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 error.py diff --git a/error.py b/error.py new file mode 100644 index 0000000..f19f8e0 --- /dev/null +++ b/error.py @@ -0,0 +1,163 @@ +"""Generic error classes. + +This module seeks to provide semantic error classes that are intended to +be used directly by clients when they would like to handle particular +semantic failures (e.g. "failed to connect") without needing to know the +enumeration of possible reasons for that failure. + +AQMPError serves as the ancestor for almost all exceptions raised by +this package, and is suitable for use in handling semantic errors from +this library. In most cases, individual methods will attempt to catch +and re-encapsulate various exceptions to provide a semantic +error-handling interface, though this is not necessarily true of +internal interfaces. + +Some errors are not defined here in this module, but exist alongside +more specific error domains in other modules. They are listed here for +convenience anyway. + +The error inheritance tree is as follows:: + + MultiException + AQMPError + ProtocolError + RawProtocolError + DeserializationError + UnexpectedTypeError + GreetingError + NegotiationError + MsgProtocolError (message.py) + ObjectTypeError (message.py) + OrphanedError (message.py) + ServerParseError (message.py) + ConnectError + DisconnectedError + StateError + +The only exception that is not an `AQMPError` is `MultiException`. It is +special, and used to encapsulate one-or-more exceptions of an arbitrary +kind; this exception MAY be raised on disconnect() when there are two or +more exceptions from the AQMP event loop to report back to the caller. + +(The bottom half is designed in such a way that exceptions are attempted +to be handled internally, but in cases of catastrophic failure, it may +still occur.) + +See `MultiException` and `AsyncProtocol.disconnect()` for more details. + +""" + +from typing import Iterable, Iterator + + +class AQMPError(Exception): + # Don't use this directly: create a subclass. + """Base failure for all errors raised by AQMP.""" + + +class ProtocolError(AQMPError): + """Abstract error class for protocol failures.""" + def __init__(self, error_message: str): + super().__init__() + self.error_message = error_message + + def __str__(self) -> str: + return f"QMP protocol error: {self.error_message}" + + +class RawProtocolError(ProtocolError): + """ + Abstract error class for low-level parsing failures. + """ + def __init__(self, error_message: str, raw: bytes): + super().__init__(error_message) + self.raw = raw + + def __str__(self) -> str: + return "\n".join([ + super().__str__(), + f" raw bytes were: {str(self.raw)}", + ]) + + +class DeserializationError(RawProtocolError): + """Incoming message was not understood as JSON.""" + + +class UnexpectedTypeError(RawProtocolError): + """Incoming message was JSON, but not a JSON object.""" + + +class ConnectError(AQMPError): + """ + Initial connection process failed. + Always wraps a "root cause" exception that can be interrogated for info. + """ + + +class GreetingError(ProtocolError): + """An exception occurred during the Greeting phase.""" + def __init__(self, error_message: str, exc: Exception): + super().__init__(error_message) + self.exc = exc + + def __str__(self) -> str: + return ( + f"QMP protocol error: {self.error_message}\n" + f" Cause: {self.exc!s}\n" + ) + + +class NegotiationError(ProtocolError): + """An exception occurred during the Negotiation phase.""" + def __init__(self, error_message: str, exc: Exception): + super().__init__(error_message) + self.exc = exc + + def __str__(self) -> str: + return ( + f"QMP protocol error: {self.error_message}\n" + f" Cause: {self.exc!s}\n" + ) + + +class DisconnectedError(AQMPError): + """ + Command was not able to be completed; we have been Disconnected. + + This error is raised in response to a pending execution when the + back-end is unable to process responses any more. + """ + + +class StateError(AQMPError): + """ + An API command (connect, execute, etc) was issued at an inappropriate time. + + (e.g. execute() while disconnected; connect() while connected; etc.) + """ + + +class MultiException(Exception): + """ + Used for multiplexing exceptions. + + This exception is used in the case that errors were encountered in both the + Reader and Writer tasks, and we must raise more than one. + """ + def __init__(self, exceptions: Iterable[BaseException]): + super().__init__(exceptions) + self.exceptions = list(exceptions) + + def __str__(self) -> str: + ret = "------------------------------\n" + ret += "Multiple Exceptions occurred:\n" + ret += "\n" + for i, exc in enumerate(self.exceptions): + ret += f"{i}) {str(exc)}\n" + ret += "\n" + ret += "-----------------------------\n" + return ret + + def __iter__(self) -> Iterator[BaseException]: + return iter(self.exceptions)