我正在为SaltStack编写一些etcd模块并遇到了这个奇怪的问题,它以某种方式阻止我捕获异常,我对它是如何做到这一点感兴趣。它似乎特别以urllib3为中心。
一个小脚本(不是盐):
import etcd
c = etcd.Client('127.0.0.1', 4001)
print c.read('/test1', wait=True, timeout=2)
当我们运行它时:
[root@alpha 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.",)
好的,让我们抓住那个bugger:
#!/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'
运行它:
[root@alpha _modules]# /tmp/etcd_watch.py
connect failed
看起来不错 - 这都是有效的python。那么问题是什么?我在salt etcd模块中有这个:
[root@alpha _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
当我们运行时:
[root@alpha _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.
嗯,这很奇怪。 etcd的read应该返回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))
我们得到:
[root@alpha _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))
我们的测试..
[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
<class 'urllib3.exceptions.ReadTimeoutError'>
呃,等等,什么?我们为什么不赶上那个?例外工作,对吧..?
如果我们尝试从urllib3捕获基类怎么样..
[root@alpha _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!'
希望并祈祷......
[root@alpha _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 Exception。我们的模块现在看起来像这样:
import etcd
def test():
c = etcd.Client('127.0.0.1', 4001)
try:
c.delete('/')
except etcd.EtcdRootReadOnly:
return 'got you this time!'
我们的跑步:
[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
got you this time!
作为最后的测试,我制作了这个模块,我可以从直接python或作为盐模块运行..
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()
通过python:
[root@alpha _modules]# python ./sjmh.py
cant get away from me!
通过盐:
[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
HOW DID YOU GET HERE? <class 'urllib3.exceptions.ReadTimeoutError'>
因此,我们可以从它抛出的etcd中捕获异常。但是,虽然当我们通过它的寂寞运行python-etcd时,我们通常能够捕获urllib3 ReadTimeoutError,但是当我通过salt运行它时,似乎没有任何东西能够捕获该urllib3异常,除了一个毯子'异常'子句。
我可以做到这一点,但我真的很好奇,正在做的是什么,这使得异常无法捕捉。我在使用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()
跑步时:
[root@alpha _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 :(得分: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
- 显然所有内容都会回到原来的位置。
这是一个非常简化的示例,但足以表明当在某个模块中定义类时 - 新创建的类型(表示新类本身的对象)在其声明的类名下“添加”到模块的名称空间。与任何其他变量一样 - 其值可以在技术上进行修改。功能也是如此。