Я писал несколько модулей etcd для SaltStack и столкнулся с этой странной проблемой, где это каким-то образом мешало мне перехватывать исключение, и мне интересно, как это делается. Кажется, что он сосредоточен вокруг urllib3.
Небольшой script (не соль):
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.",)
Хорошо, поймаем этого 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'
Запустите его:
[[email protected] _modules]# /tmp/etcd_watch.py
connect failed
Выглядит хорошо - все работает на питоне. Так в чем проблема? У меня это в модуле соли и т.д.:
[[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.
Герм, это странно. 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()
Через python:
[[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, которые он выбрасывает. Но, хотя мы, как правило, можем поймать urllib3 ReadTimeoutError, когда мы запускаем python-etcd своим одиноким, когда я запускаю его через соль, ничто, похоже, не в состоянии поймать это исключение urllib3, кроме обложки "Исключение".
Я могу это сделать, но мне действительно интересно, что делает черная соль, делая это так, чтобы исключение было несовместимым. Я никогда не видел этого раньше, работая с python, поэтому мне было бы любопытно, как это происходит и как я могу обойти его.
Edit:
Итак, я наконец смог его поймать.
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 не работает, поэтому они не могут быть одинаковыми.
Я добавил следующее в script, прямо перед вызовом c.read().
log.debug(urllib3.exceptions.ReadTimeoutError.__module__)
log.debug(ReadTimeoutError.__module__)
И теперь я получаю это в журнале:
[DEBUG ] requests.packages.urllib3.exceptions
[DEBUG ] urllib3.exceptions
Итак, это, по-видимому, причина, по которой его поймали так, как она есть. Это также можно воспроизвести, просто загрузив библиотеку etcd и request и сделав что-то вроде этого:
#!/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, и любой другой модуль, который пытается их поймать, терпит неудачу. Кроме того, похоже, что более новые версии запросов не имеют этой проблемы.