2015-11-04 79 views
9

我正在爲SaltStack編寫一些etcd模塊,並遇到了這個奇怪的問題,它在某種程度上阻止了我捕獲一個異常,我對它是如何做到這一點感興趣。它似乎特別圍繞着urllib3。爲什麼我無法捕捉到這個python異常?

一個小腳本(不加鹽):

import etcd 
c = etcd.Client('127.0.0.1', 4001) 
print c.read('/test1', wait=True, timeout=2) 

當我們運行:

[[email protected] utils]# /tmp/etcd_watch.py 
Traceback (most recent call last): 
    File "/tmp/etcd_watch.py", line 5, in <module> 
    print c.read('/test1', wait=True, timeout=2) 
    File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read 
    timeout=timeout) 
    File "/usr/lib/python2.6/site-packages/etcd/client.py", line 788, in api_execute 
    cause=e 
etcd.EtcdConnectionFailed: Connection to etcd failed due to ReadTimeoutError("HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out.",) 

好吧,讓我們趕上開溜:

#!/usr/bin/python 

import etcd 
c = etcd.Client('127.0.0.1', 4001) 

try: 
    print c.read('/test1', wait=True, timeout=2) 
except etcd.EtcdConnectionFailed: 
    print 'connect failed' 

運行:

[[email protected] _modules]# /tmp/etcd_watch.py 
connect failed 

看起來不錯 - 這是所有工作的蟒蛇。那麼問題是什麼?我有這樣的鹽ETCD模塊:

[[email protected] _modules]# cat sjmh.py 
import etcd 

def test(): 
    c = etcd.Client('127.0.0.1', 4001) 
    try: 
    return c.read('/test1', wait=True, timeout=2) 
    except etcd.EtcdConnectionFailed: 
    return False 

當我們運行:

[[email protected] _modules]# salt 'alpha' sjmh.test 
alpha: 
    The minion function caused an exception: Traceback (most recent call last): 
     File "/usr/lib/python2.6/site-packages/salt/minion.py", line 1173, in _thread_return 
     return_data = func(*args, **kwargs) 
     File "/var/cache/salt/minion/extmods/modules/sjmh.py", line 5, in test 
     c.read('/test1', wait=True, timeout=2) 
     File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read 
     timeout=timeout) 
     File "/usr/lib/python2.6/site-packages/etcd/client.py", line 769, in api_execute 
     _ = response.data 
     File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 150, in data 
     return self.read(cache_content=True) 
     File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 218, in read 
     raise ReadTimeoutError(self._pool, None, 'Read timed out.') 
    ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out. 

HRM,這是不可思議。 etcd的讀取應該已經返回etcd.EtcdConnectionFailed。所以,我們再來看看。我們現在模塊是這樣的:

import etcd 

def test(): 
    c = etcd.Client('127.0.0.1', 4001) 
    try: 
    return c.read('/test1', wait=True, timeout=2) 
    except Exception as e: 
    return str(type(e)) 

而我們得到:

[[email protected] _modules]# salt 'alpha' sjmh.test 
alpha: 
    <class 'urllib3.exceptions.ReadTimeoutError'> 

好了,我們知道我們可以趕上這個事情。我們現在知道它拋出了一個ReadTimeoutError,所以讓我們來捕捉它。我們的模塊的最新版本:

import etcd 
import urllib3.exceptions 

def test(): 
    c = etcd.Client('127.0.0.1', 4001) 
    try: 
    c.read('/test1', wait=True, timeout=2) 
    except urllib3.exceptions.ReadTimeoutError as e: 
    return 'caught ya!' 
    except Exception as e: 
    return str(type(e)) 

而我們測試的..

[[email protected] _modules]# salt 'alpha' sjmh.test 
alpha: 
    <class 'urllib3.exceptions.ReadTimeoutError'> 

呃,等等,什麼?我們爲什麼不抓住這個?例外工作,對.. ..?

怎麼樣,如果我們試圖從urllib3趕上基類..

[[email protected] _modules]# cat sjmh.py 
import etcd 
import urllib3.exceptions 

def test(): 
    c = etcd.Client('127.0.0.1', 4001) 
    try: 
    c.read('/test1', wait=True, timeout=2) 
    except urllib3.exceptions.HTTPError: 
    return 'got you this time!' 

希望並祈禱..

[[email protected] _modules]# salt 'alpha' sjmh.test 
alpha: 
    The minion function caused an exception: Traceback (most recent call last): 
     File "/usr/lib/python2.6/site-packages/salt/minion.py", line 1173, in _thread_return 
     return_data = func(*args, **kwargs) 
     File "/var/cache/salt/minion/extmods/modules/sjmh.py", line 7, in test 
     c.read('/test1', wait=True, timeout=2) 
     File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read 
     timeout=timeout) 
     File "/usr/lib/python2.6/site-packages/etcd/client.py", line 769, in api_execute 
     _ = response.data 
     File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 150, in data 
     return self.read(cache_content=True) 
     File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 218, in read 
     raise ReadTimeoutError(self._pool, None, 'Read timed out.') 
    ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out. 

BLAST YE!好的,讓我們嘗試一個不同的方法,返回一個不同的etcd異常。我們的模塊現在看起來是這樣的:

import etcd 

def test(): 
    c = etcd.Client('127.0.0.1', 4001) 
    try: 
    c.delete('/') 
    except etcd.EtcdRootReadOnly: 
    return 'got you this time!' 

而且我們來看:

[[email protected] _modules]# salt 'alpha' sjmh.test 
alpha: 
    got you this time! 

作爲最後的測試,我做了這個模塊,我可以運行從直蟒蛇,或鹽模塊。 。

import etcd 
import urllib3 

def test(): 
    c = etcd.Client('127.0.0.1', 4001) 
    try: 
    c.read('/test1', wait=True, timeout=2) 
    except urllib3.exceptions.ReadTimeoutError: 
    return 'got you this time!' 
    except etcd.EtcdConnectionFailed: 
    return 'cant get away from me!' 
    except etcd.EtcdException: 
    return 'oh no you dont' 
    except urllib3.exceptions.HTTPError: 
    return 'get back here!' 
    except Exception as e: 
    return 'HOW DID YOU GET HERE? {0}'.format(type(e)) 

if __name__ == "__main__": 
    print test() 

通過蟒蛇:

[[email protected] _modules]# python ./sjmh.py 
cant get away from me! 

通過鹽:

[[email protected] _modules]# salt 'alpha' sjmh.test 
alpha: 
    HOW DID YOU GET HERE? <class 'urllib3.exceptions.ReadTimeoutError'> 

因此,我們可以從ETCD捕獲異常,它拋出。但是,當我們通過它的寂寞運行python-etcd時,通常我們能夠捕獲urllib3 ReadTimeoutError,當我通過salt運行它時,似乎沒有任何東西能夠捕獲urllib3異常,除了一個'Exception'子句。

我可以做到這一點,但我真的很好奇,鹽是怎麼做的,這使得一個異常是無法捕捉的。在使用python之前我從來沒有見過這樣的東西,所以我很好奇它是如何發生的以及我如何解決它。

編輯:

所以我終於能夠抓住它。

import etcd 
import urllib3.exceptions 
from urllib3.exceptions import ReadTimeoutError 

def test(): 
    c = etcd.Client('127.0.0.1', 4001) 
    try: 
    c.read('/test1', wait=True, timeout=2) 
    except urllib3.exceptions.ReadTimeoutError: 
    return 'caught 1' 
    except urllib3.exceptions.HTTPError: 
    return 'caught 2' 
    except ReadTimeoutError: 
    return 'caught 3' 
    except etcd.EtcdConnectionFailed as ex: 
    return 'cant get away from me!' 
    except Exception as ex: 
    return 'HOW DID YOU GET HERE? {0}'.format(type(ex)) 

if __name__ == "__main__": 
    print test() 

當運行:

[[email protected] _modules]# salt 'alpha' sjmh.test 
alpha: 
    caught 3 

它仍然沒有任何意義,但。從我所瞭解的例外情況來看,回報率應該是'抓住1'。爲什麼我必須直接導入異常的名稱,而不是僅僅使用完整的類名?

更多編輯!

因此,在兩個類之間加入比較會產生'False' - 這很明顯,因爲except子句不起作用,所以這些不可能是相同的。

在我調用c.read()之前,我在腳本中添加了以下內容。

log.debug(urllib3.exceptions.ReadTimeoutError.__module__) 
log.debug(ReadTimeoutError.__module__) 

現在我得到這個在日誌中:

[DEBUG ] requests.packages.urllib3.exceptions 
[DEBUG ] urllib3.exceptions 

所以,這似乎是陷入事情是這樣的原因。這也是重複性由剛剛下載ETCD並請求庫,做這樣的事情:

#!/usr/bin/python 

#import requests 
import etcd 

c = etcd.Client('127.0.0.1', 4001) 
c.read("/blah", wait=True, timeout=2) 

你將最終獲得提出的「正確」的例外 - etcd.EtcdConnectionFailed。但是,取消註釋'請求',並且您將以urllib3.exceptions.ReadTimeoutError結束,因爲etcd現在不再捕獲異常。

所以看起來,當請求被導入時,它會重寫urllib3的異常,而其他任何試圖捕獲這些異常的模塊都會失敗。此外,似乎較新版本的請求不存在此問題。

+0

' 「HRM,這是不可思議。ETCD的閱讀應該已經回到etcd.EtcdConnectionFailed」'。不是很奇怪。'ReadTimeoutError'描述了接收數據的時間已經在底層協議級別到期的事實。 'ConnectionFailed'描述客戶端無法連接到遠程服務(出於多種原因)的事實。它們是您可能遇到的兩種截然不同的情況,在網絡中非常常見,尤其是在遠程服務器資源不足且速度慢的情況下。 – Pynchia

+0

Pynchia,我說它應該返回它,因爲一般來說,在python-etcd 0.4.2中,當等待調用超時到期時,它會引發etcd.EtcdConnectionFailed。這是一個本地etcd實例,我證實etcd已經啓動。另外,正如最後一個腳本所證明的,網絡以外的事情正在發生。 – sjmh

+0

哦,是的。這是Connection可能失敗的另一個原因。但在你的情況下,由於超時,讀取失敗。也就是說,圖書館可能會表現得很奇怪。 – Pynchia

回答

3

我的回答有點臆測,因爲我不能用這些確切的庫在實踐中證明它(首先我不能重現你的錯誤,因爲它也取決於庫版本以及它們是如何安裝的),但是仍然顯示發生這種情況的可能方法之一:

最後一個例子給出了一個很好的線索:重點確實是在程序執行時的不同時刻,名稱urllib3.exceptions.ReadTimeoutError可能指的是不同的類。 ReadTimeoutError是,就像在Python中所有其他模塊,簡直是在urllib3.exceptions命名空間,一個名稱,可以重新分配(但並不意味着它是一個好主意,這樣做)。

當提到這個名稱,由其完全合格的「路徑」 - 我們保證通過我們提到它的時候,指的是它的實際狀態。但是,當我們第一次導入它時,如from urllib3.exceptions import ReadTimeoutError - 它將名稱ReadTimeoutError帶入進行導入的名稱空間,並且在導入時,此名稱爲,其值爲urllib3.exceptions.ReadTimeoutError。現在,如果一些其他的代碼後重新分配爲urllib3.exceptions.ReadTimeoutError的價值 - 這兩個(它的「當前」 /「最新」值與先前進口之一)可能會實際上是不同的 - 所以在技術上你可能最終有兩個不同的班級。現在,它的異常類,將實際募集 - 這取決於所引發錯誤代碼是如何使用它:如果他們之前導入的ReadTimeoutError到他們的命名空間 - 那麼這一個(「原始」)將提高。

要驗證這是否是你可以添加以下到except ReadTimeoutError塊的情況下:

print(urllib3.exceptions.ReadTimeoutError == ReadTimeoutError) 

如果打印False - 它證明了在引發異常的時候,兩個「引用」指到不同的班級。


不良實現其可產生相似的結果的簡化的例子:

文件api.py(適當的設計和通過本身愉快地存在):

class MyApiException(Exception): 
    pass 

def foo(): 
    raise MyApiException('BOOM!') 

文件apibreaker.py(一個怪):

import api 

class MyVeryOwnException(Exception): 
    # note, this doesn't extend MyApiException, 
    # but creates a new "branch" in the hierarhcy 
    pass 

# DON'T DO THIS AT HOME! 
api.MyApiException = MyVeryOwnException 

文件apiuser.py

import api 
from api import MyApiException, foo 
import apibreaker 

if __name__ == '__main__': 
    try: 
     foo() 
    except MyApiException: 
     print("Caught exception of an original class") 
    except api.MyApiException: 
     print("Caught exception of a reassigned class") 

執行時:

$ python apiuser.py 
Caught exception of a reassigned class 

如果刪除線import apibreaker - 顯然然後一切都回到自己的地方,因爲它應該是。

這是一個非常簡化的例子,但足以說明當某個模塊中定義了一個類時 - 新創建的類型(代表新類本身的對象)在其聲明的類名稱下添加到模塊的名稱空間中。與其他任何變量一樣 - 其價值可以在技術上進行修改。同樣的事情發生在功能上。

+0

我實際上在幾天前提出了一個非常類似的測試,但它沒有任何解釋就被低估了,所以我刪除了我的答案。我的建議是比較試圖捕獲的異常和實際接收到的異常,看看它們是否真的相同,儘管打印出的是相同的。我不知道是誰投了票。 –

+0

感謝你這樣做 - 它使我走上了我認爲提供答案的路徑,那就是通過SaltStack運行時,salt-stack在請求中導入,這會重新分配urllib3異常。 urllib3.exceptions.ReadTimeoutError現在屬於requests.packages.urllib3.exceptions,ReadTimeoutError沒有被重新分配,仍然來自urllib3.exceptions模塊。 Etcd正試圖抓住urllib3版本,而不是要求一個,所以它傳遞。此外,@TomKarzes,不知道誰downvoted,但不是我! – sjmh