2015-11-04 5 views
9

Stavo scrivendo alcuni moduli di etcd per SaltStack e mi sono imbattuto in questo strano problema in cui mi impedisce in qualche modo di ottenere un'eccezione e sono interessato a come lo sta facendo. Sembra specificamente incentrato su urllib3.Perché non riesco a catturare questa eccezione Python?

Un piccolo script (non sale):

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

E quando lo facciamo funzionare:

[[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.",) 

Ok, cattura che sodomita:

#!/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' 

eseguirlo:

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

Sembra buono: funziona tutto in python. Quindi qual è il problema? Ho questo nel modulo di sale 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 

E quando si corre che:

[[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, questo è strano. La lettura di etcd dovrebbe aver restituito etcd.EtcdConnectionFailed. Quindi, guardiamo oltre. Il nostro modulo è ora questo:

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)) 

e otteniamo:

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

Ok, sappiamo che siamo in grado di cogliere questa cosa. E ora sappiamo che ha generato un errore ReadTimeoutError, quindi prendiamocelo. La nuova versione del nostro modulo:

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)) 

E la nostra prova ..

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

Er, aspetta, cosa? Perché non l'abbiamo capito? Eccezioni funzionano, giusto ..?

ne dite se cercare di catturare la classe base da 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!' 

sperare e pregare ..

[[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! Ok, proviamo con un metodo diverso che restituisce una diversa eccezione etcd. Il nostro modulo ora assomiglia a questo:

import etcd 

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

E la nostra corsa:

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

Come prova finale, ho fatto questo modulo, che può funzionare sia in pitone dritto, o come modulo di sale. .

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() 

Attraverso pitone:

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

Attraverso sale:

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

Quindi, siamo in grado di intercettare le eccezioni da etcd che getta. Ma, mentre normalmente siamo in grado di catturare l'urllib3 ReadTimeoutError quando eseguiamo python-etcd dal suo solitario, quando lo faccio passare attraverso, niente sembra essere in grado di catturare quell'eccezione urllib3, tranne una clausola 'Exception' coperta.

Posso farlo, ma sono davvero curioso di sapere cosa diavolo sta facendo, in modo che un'eccezione sia inafferrabile. Non l'ho mai visto prima quando lavoro con Python, quindi sarei curioso di sapere come sta accadendo e come posso aggirarlo.

Edit:

Così mi è stato finalmente in grado di prenderlo.

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() 

E quando run:

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

Ancora non ha senso però. Da quello che so delle eccezioni, il ritorno dovrebbe essere 'preso 1'. Perché dovrei importare direttamente il nome dell'eccezione, piuttosto che usare solo il nome completo della classe?

ALTRE MODIFICHE!

Quindi, aggiungendo il confronto tra le due classi si produce 'False' - che è ovvio, perché la clausola except non funzionava, quindi quelle non potevano essere le stesse.

Ho aggiunto quanto segue allo script, proprio prima chiamo il c.read().

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

E ora ho questo nel registro:

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

Quindi, che sembra essere la ragione che sta ottenendo catturato così com'è. Questo è anche riproducibili, semplicemente scaricando l'etcd e richiede biblioteca e fare qualcosa di simile:

#!/usr/bin/python 

#import requests 
import etcd 

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

si finirà per ottenere l'eccezione 'destra' in rilievo - etcd.EtcdConnectionFailed. Tuttavia, le "richieste" di uncomment e ti ritroverai con urllib3.exceptions.ReadTimeoutError, perché ora ecc non attira più l'eccezione.

Pertanto, quando le richieste vengono importate, riscrive le eccezioni urllib3 e qualsiasi altro modulo che tenta di intercettarle fallisce. Inoltre, sembra che le versioni più recenti delle richieste non abbiano questo problema.

+0

'" Hrm, è strano. La lettura di etcd dovrebbe essere restituita etcd.EtcdConnectionFailed "'. Non proprio strano'ReadTimeoutError' descrive il fatto che il tempo per ricevere i dati è scaduto a livello di protocollo di underlaying. 'ConnectionFailed' descrive il fatto che il client non è stato in grado di connettersi al servizio remoto (per una serie di motivi). Si tratta di due condizioni distinte che si possono verificare, abbastanza comuni in rete, soprattutto se il server remoto ha risorse basse e lente. – Pynchia

+0

Pynchia, ho detto che avrebbe dovuto restituirlo perché in generale, con python-etcd 0.4.2, quando il timeout scade su una chiamata in attesa, si solleva etcd.EtcdConnectionFailed. È un'istanza locale di etcd e ho verificato che etcd era attivo. Inoltre, come evidenziato dall'ultimo script, sta accadendo qualcosa di diverso dal networking. – sjmh

+0

Oh sì. Questa è un'altra ragione per cui una connessione può fallire. Ma nel tuo caso la lettura sta fallendo a causa di un timeout. Detto questo, la biblioteca potrebbe comportarsi in modo strano. – Pynchia

risposta

3

La mia risposta è un po 'di speculazione, perché non riesco a dimostrarlo in pratica con queste librerie esatte (per cominciare non riesco a riprodurre il tuo errore in quanto dipende anche dalle versioni delle librerie e da come sono installate), ma mostra comunque uno dei possibili modi in cui questo accade:

L'ultimo esempio fornisce un buon indizio: il punto è infatti che in momenti diversi nel momento dell'esecuzione del programma, il nome urllib3.exceptions.ReadTimeoutError può fare riferimento a classi diverse. ReadTimeoutError è, come per ogni altro modulo in Python, è semplicemente un nome nel namespace urllib3.exceptions, e può essere riassegnato (ma non significa che sia una buona idea farlo).

Quando ci si riferisce a questo nome per il suo "percorso" completo - ci si assicura che faccia riferimento allo stato attuale di esso al momento in cui ci si riferisce ad esso. Tuttavia, quando lo importiamo per la prima volta come from urllib3.exceptions import ReadTimeoutError - porta il nome ReadTimeoutError nello spazio dei nomi che esegue l'importazione e questo nome è associato al valore di urllib3.exceptions.ReadTimeoutError al momento dell'importazione. Ora, se qualche altro codice riassegna in seguito il valore per urllib3.exceptions.ReadTimeoutError - i due (il suo valore "corrente"/"ultimo" e quello precedentemente importato) potrebbero essere effettivamente diversi - quindi tecnicamente potresti finire con due classi diverse. Ora, quale classe di eccezioni verrà effettivamente sollevata - questo dipende da come il codice che solleva l'errore lo usa: se hanno precedentemente importato lo ReadTimeoutError nel loro spazio dei nomi - allora questo (il "originale") verrà sollevato.

Per verificare se questo è il caso si potrebbe aggiungere il seguente alla except ReadTimeoutError blocco:

print(urllib3.exceptions.ReadTimeoutError == ReadTimeoutError) 

Se viene stampato False - dimostra che per il momento l'eccezione viene sollevata, i due "Riferimenti" si riferiscono a classi diverse davvero.


Un esempio semplificato di una scarsa applicazione che può produrre risultati simili:

File api.py (progettato correttamente ed esiste felicemente da sé):

class MyApiException(Exception): 
    pass 

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

File apibreaker.py (una colpa):

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 

File 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") 

Quando viene eseguito:

$ python apiuser.py 
Caught exception of a reassigned class 

Se si rimuove la linea import apibreaker - chiaramente poi tutto va di nuovo ai loro posti come dovrebbe essere.

Questo è un esempio molto semplificato, ma abbastanza illustrativo per dimostrare che quando una classe è definita in un certo modulo - di recente tipo (oggetto che rappresenta nuova classe stessa) viene "aggiunto" sotto il suo nome classe dichiarata al namespace del modulo creato. Come con qualsiasi altra variabile, il suo valore può essere modificato tecnicamente. La stessa cosa succede alle funzioni.

+0

In realtà ho proposto un test molto simile alcuni giorni fa, ma è stato down-votato senza spiegazione, quindi ho rimosso la mia risposta. Il mio suggerimento era di confrontare l'eccezione cercando di essere catturati con l'eccezione effettivamente ricevuta, quindi vedi se sono * veramente * gli stessi, nonostante la stampa dello stesso. Non so chi l'abbia votato. –

+0

Grazie per questo - mi guida lungo il percorso che penso che fornisce la risposta e che è quando si esegue questo attraverso SaltStack, stack di sale sta importando nelle richieste, che è riassegnazione delle eccezioni urllib3. Urllib3.exceptions.ReadTimeoutError ora appartiene a requests.packages.urllib3.exceptions, dove ReadTimeoutError non viene riassegnato e si trova ancora dal modulo urllib3.exceptions. Etcd sta cercando di catturare la versione urllib3, non quella delle richieste e quindi lascia perdere. Inoltre, @ TomKarzes, non sono sicuro di chi ha downvoted, ma non ero io! – sjmh