Python中的Singal和Exception

概述

python是如何處理中斷信號Singal的?

python中的異常(Exception)分為哪幾種,不同Exception的繼承關(guān)系?他們有什么不同?Error和Exception有區(qū)別嗎?

發(fā)現(xiàn)問題

最近處理了一個線上問題,使用signal中的ALARM機制對程序進行超時管理,超時會觸發(fā)自定義的AirflowTaskTimeoutException(原理是在程序啟動時注冊一個SIGALRM(14)的中斷回調(diào)方法,并定義在超時N秒后觸發(fā)回調(diào)方法,在回調(diào)方法中拋出AirflowTaskTimeout,然后捕獲該異常進行程序退出的善后工作。詳細情況請參看Apache-Airflow的TaskTimeout機制,TaskTimeout),但是由于程序中引用的一個模塊(elasticsearch-py)捕獲了所有Exception,然后拋出了一個新建的ConnectionError異常(詳細情況請看perform_request中的異常處理),導致上層程序無法捕捉到AirflowTaskTimeout。由于Http請求耗時較長,終端信號大概率在這部分被處理,而這個方法默認會重試3次(見transport.py#310),最終結(jié)果就是,本來該因超時停止的程序,在接收到一次超時信號后會重新再執(zhí)行一次,本來該結(jié)束的程序怎么也停不下來。

我的第一反應是:修改exception代碼塊中的程序,讓AirflowTaskTimeout能夠以本來面目拋出。于是提出僅將網(wǎng)絡請求相關(guān)的異常轉(zhuǎn)化為ConnectionError拋出,其它異常原樣拋出。提了一個issue。

后來,又考慮到這么大范圍的異常捕獲不太妥,查看了elasticsearch-pytry代碼塊中代碼可能拋出什么異常,發(fā)現(xiàn)其中可能的異常都繼承自HttpError,認為將異常捕捉范圍縮小到HttpError似乎更合理,線上的問題通過這種方式得到解決。于是提交了pr

但是elasticsearch-py的作者不認可這個修改,原因是:所修改的代碼中的Transport模塊作為本庫的基礎(chǔ),修改這里會造成嚴重后果(breaking change),Transport模塊應該只拋出TransportError相關(guān)的異常。推薦使用內(nèi)建的超時機制或者異步IO任務。

的確,如果上層有依賴TransportTransportError的異常處理邏輯(見transport.py#316),勢必會影響到原有的程序。但是,我仍然認為對未知異常的劫持是不道德可取的。

在后續(xù)的探索中發(fā)現(xiàn),同樣是向程序發(fā)送中斷信號,kill -2 $PID可以終止程序(這里的停止不能善后)。但是kill -14 $PID就會因為AirflowTaskTimeout被劫持而達不到停止程序的效果。

開始Signal和Exception的探索之旅…………

探索Python中的Exception

書接上文,kill -2 $PID可以結(jié)束程序,kill -14 $PID不能結(jié)束程序。原因肯定出現(xiàn)在對這兩個信號的處理邏輯上。

信號值14就是前述的超時機制使用的信號SIGALRM,發(fā)送信號14不能結(jié)束程序的原因是AirflowTaskTimeout被劫持導致后續(xù)的程序不能檢測到有效的AirflowTaskTimeout異常。

觀察被kill -2 $PID結(jié)束的程序,可以看到堆棧異常日志最后一行打印了KeyboardInterrupt,這是一個python內(nèi)建的異常類型,為什么它沒有被劫持?

Traceback (most recent call last):
  File "ep1.py", line 77, in <module>
    do_query()
  File "ep1.py", line 67, in do_query
    long_time_request()
  File "ep1.py", line 59, in long_time_request
    time.sleep(60) # DO A LONG TIME QUERY
KeyboardInterrupt

進一步查看python的源碼發(fā)現(xiàn)了其中的玄機,原來python中的異常都繼承自BaseException,而內(nèi)建的異常中僅有4個異常直接繼承自BaseException,他們分別是:

SystemExit # 系統(tǒng)退出異常,~這么翻譯不對吧?~
KeyboardInterrupt # 用戶終止程序引發(fā)的異常,就是kill -2引發(fā)的,平常ctrl + c也是觸發(fā)這個異常
GeneratorExit # 迭代類型結(jié)束時觸發(fā)的異常,用來結(jié)束對迭代類型的for循環(huán)遍歷
Exception # 其它所有內(nèi)建異常,也是我們程序中創(chuàng)建新的異常的父類

后來又翻了一下《Python核心編程》,BaseException是在python2.5之后才引入了,目的就是為了區(qū)分SystemExit、KeyboardInterrupt和其他異常的捕獲。

這么一看,一切都清晰了,程序中異常捕捉雖然廣泛,但是僅捕捉了Exception一族的異常。而KeyboardInterrupt集成自BaseException,他和Exception不是子孫關(guān)系。所以except Exception as e:語句不能捕獲KeyboardInterrupt(劫持失敗~)。

ErrorException有什么區(qū)別嗎?個人覺得就是名稱的區(qū)別,可能Error代表的異常更嚴重。

  • python內(nèi)建異常的繼承關(guān)系
BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

探索Python中Singal

kill -2 $PID是通過什么方式觸發(fā)的KeyboardInterrupt異常?這一部分我還沒弄清楚,以下敘述有個人猜測部分,歡迎指正、討論。

查看python源碼(C部分)發(fā)現(xiàn),_signal.py文件中有一個常量SIG_DFL(used to refer to the system default handler),這是一個默認的信號處理handler,是這個默認的handler拋出了KeyboardInterrupt`異常嗎,在哪里?

實驗過程用的代碼

后面發(fā)現(xiàn)還可以通過修改Airflow中的AirflowTaskTimeout,讓他繼承自BaseException,這樣就無懼try except Exception的劫持了。

import time
import signal
import logging

logging.basicConfig()
logger = logging.getLogger(__name__)


class MyTimeout(Exception):
    """
    我們自定義的timeout異常
    """
    pass


# class MyTimeout(BaseException):
#     """
#     我們自定義的timeout異常,基于BaseException
#     """
#     pass

class LibaryDeifinedExcepiton(Exception):
    """
    模塊自定義的異常
    """
    pass

class timeout(object):
    """
    To be used in a ``with`` block and timeout its content.
    """

    def __init__(self, seconds=1, error_message='Timeout'):
        self.seconds = seconds
        self.error_message = error_message
        self.log = logger

    def handle_timeout(self, signum, frame):
        self.log.error("Process timed out")
        raise MyTimeout(self.error_message)

    def __enter__(self):
        try:
            signal.signal(signal.SIGALRM, self.handle_timeout)
            signal.alarm(self.seconds)
        except ValueError as e:
            self.log.warning("timeout can't be used in the current context")
            self.log.exception(e)

    def __exit__(self, type, value, traceback):
        try:
            signal.alarm(0)
        except ValueError as e:
            self.log.warning("timeout can't be used in the current context")
            self.log.exception(e)

def long_time_request():
    try:
        time.sleep(60) # DO A LONG TIME QUERY
    except Exception as e:
        raise LibaryDeifinedExcepiton('N/A', str(e), e)

def do_query():
    try:
        with timeout(30):
            long_time_request()
            print("after request")
    except Exception as e:
        print("got excepiton %s"%e)
        if isinstance(e, MyTimeout):
            print("got MyTimeout exception")
            # Do sth you want, when MyTimeout happened.
        else:
            print("not a MyTimeout exception but a %s"%type(e))

def do_query_2():
    try:
        with timeout(30):
            long_time_request()
            print("after request")
    except MyTimeout as e:  # 捕捉超時異常,做善后工作
        print("got MyTimeout excepiton %s"%e)
        # Do sth you want, when MyTimeout happened.

if __name__ == "__main__":
    do_query()
    # do_query_2()

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容