urllib2でtimeoutを捕まえる¶
urllib2を使っていて、timeoutを入れたくなりました。で、timeoutが発生し たらリトライをする、といった処理をしたいのですが、このtimeoutがどうい う例外なのかが分かりませんでした。
世界のtk0miya先生は urlib2.URLErrorで、messageに "timed out"と書かれてい る例外が飛んでくる、とおっしゃっていました。
しかし、手元で試したところ、 socket.timeout が飛んできておりました。
はてな?
手元は2.7.2 on Linux(ubuntu)、世界の小宮先生は2.7.1とのこと。
書いていたのはこういうコードでした。
try:
f = urllib2.urlopen(req, timeout=timeout)
body = f.read()
except socket.timeout, e:
print("TIMEOUT!")
コードを追う¶
ということで、 urllib2のコード を追っ ていきます。
def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
global _opener
if _opener is None:
_opener = build_opener()
return _opener.open(url, data, timeout)
でtimeoutを設定したのち、openしてます。実際のopenは このあたり です。
try:
h.request(req.get_method(), req.get_selector(), req.data, headers)
except socket.error, err: # XXX what error?
h.close()
raise URLError(err)
ほー。socket.errorであればURLErrorで投げなおしているようです。であれば、 URLErrorがということ?
socketを追っていきます。
Lib/socket.py はcreate_connection()とwrite()、readinto()が呼ばれており、実態は Modules/socketmodule.c へ。
ここを見ると、
static PyObject *socket_timeout;
(略)
socket_timeout = PyErr_NewException("socket.timeout",
socket_error, NULL);
Py_INCREF(socket_timeout);
PyModule_AddObject(m, "timeout", socket_timeout);
とあり、socket.timeoutという例外を作っています。baseクラスは socket_errorですね。
実際に投げているところはこんな感じでいろいろなところで投げています。
if (timeout == 1) {
PyErr_SetString(socket_timeout, "timed out");
return NULL;
}
ふむふむ。
基本に戻れ¶
と、ここで世界の小宮先生から「traceは?」との指摘が。確かに。
見てみるとこんな感じでした。
Traceback (most recent call last):
File "hoge.py", line 62, in _callAPI
body = f.read()
File "/usr/lib/python2.7/socket.py", line 351, in read
data = self._sock.recv(rbufsize)
File "/usr/lib/python2.7/httplib.py", line 561, in read
s = self.fp.read(amt)
File "/usr/lib/python2.7/socket.py", line 380, in read
data = self._sock.recv(left)
timeout: timed out
…
あ!urllib2を通っていない?
確認してみると世界の小宮先生の場合、 urlopen でtimeoutしているとのこと。 一方、今回はreadのところでtimeoutしています。
つまり、
urlopenでtimeoutの場合
urllib2.URLError が飛んでくる
read()でtimeoutの場合
socket.timeout が飛んでくる
のではないかと考えられます。
検証¶
socket.pyの readinto を見てみると、
if self._timeout_occurred:
raise IOError("cannot read from timed out object")
while True:
try:
return self._sock.recv_into(b)
except timeout:
self._timeout_occurred = True
raise
except InterruptedError:
continue
except error as e:
if e.args[0] in _blocking_errnos:
return None
raise
となっています。timeoutを捕まえてそのままraiseしています。ん?while trueに入る前にtimeoutが起きるとIOErrorが変えるのか。これは今は関係ないけどちょっと気になる点。
一方、 create_connection は
for res in getaddrinfo(host, port, 0, SOCK_STREAM):
af, socktype, proto, canonname, sa = res
sock = None
try:
sock = socket(af, socktype, proto)
if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
return sock
except error as _:
err = _
if sock is not None:
sock.close()
if err is not None:
raise err
else:
raise error("getaddrinfo returns an empty list")
とあり、socket.errorを捕まえてそのまま投げています。
と、ここでtimeoutしていたら、世界の小宮先生が 続き を書いてしまわれました。
どこでこういう変更があったか¶
ということで、乗りかかった船で、どこでどういう変更があったかを追跡して みる。といっても、先生の後を追っているだけだけど。
で、調べてみると 71523: b66bbbdc7abd で urllib2 に 変更が あったみたいだ。
変更前
try:
h.request(req.get_method(), req.get_selector(), req.data, headers)
try:
r = h.getresponse(buffering=True)
except TypeError: #buffering kw not supported
r = h.getresponse()
except socket.error, err: # XXX what error?
raise URLError(err)
finally:
h.close()
変更後
try:
h.request(req.get_method(), req.get_selector(), req.data, headers)
except socket.error, err: # XXX what error?
h.close()
raise URLError(err)
else:
try:
r = h.getresponse(buffering=True)
except TypeError: # buffering kw not supported
r = h.getresponse()
b66bbbdc7abd以前であれば、request()でもgetresponse()でも、socket.errorは すべてURLErrorになって投げ直されています。
ところが、変更以後はh.request()で発生したsocket.errorはURLErrorとなり ますが、それ以降のh.getresponse()で発生したsocket.errorはそのまま上がっ ていくことになります。
h.request()はhttplib中でsocket.sendall()を呼んでおり、h.getresponse() はsocket.read()を呼んでいます。
h.request()でtimeout、つまり、sendでtimeoutすると、URLErrorとなります が、一旦送りきってしまえばURLErrorとはなりません。…ほんと?
なお、f.read()部分ではurllib2を通らず、httplib経由でsocket.pyを叩くこと になりますので、このURLErrorでの投げ直しは行われず、そのまま socket.errorになります。
小宮先生にも検証してもらって、確証が持てました。
というわけで、結論です。
urlopen()中でHTTP Request送信中 |
urlopen()中でHTTPヘッダ受信中 |
f.read()でHTTP Body部受信中 |
|
---|---|---|---|
Python2.7.1 |
urllib2.URLError |
urllib2.URLError |
socket.timeout |
Python2.7.2 |
urllib2.URLError |
urllib2.URLError |
socket.timeout |
Python2.7.1 |
urllib2.URLError |
socket.timeout |
socket.timeout |
とはいえ、結局はurllib2.URLErrorとsocket.timeoutの両方を捕まえておけば いい、と言うコードになるわけですが。
Comments
comments powered by Disqus