Merge lp:~smoser/software-properties/trunk.lp-1779302-retry-recv-keys into lp:software-properties

Proposed by Scott Moser
Status: Merged
Approved by: Brian Murray
Approved revision: 1046
Merged at revision: 1044
Proposed branch: lp:~smoser/software-properties/trunk.lp-1779302-retry-recv-keys
Merge into: lp:software-properties
Diff against target: 171 lines (+92/-42)
1 file modified
softwareproperties/ppa.py (+92/-42)
To merge this branch: bzr merge lp:~smoser/software-properties/trunk.lp-1779302-retry-recv-keys
Reviewer Review Type Date Requested Status
Brian Murray Needs Information
Review via email: mp+349213@code.launchpad.net

Commit message

Retry on failed lookups of gpg keys.

Recent events have made keyserver.ubuntu.com (both hkp and https
interfaces) less reliable than they previously were.

The change here retries on failure to read from key server other than
a 404 error.

To post a comment you must log in.
1043. By Scott Moser

remove debug pdb imports and fix flake8 reports

1044. By Scott Moser

make sure to set/re-set err_msg and err

Revision history for this message
Brian Murray (brian-murray) wrote :

I've a couple of in-line questions.

review: Needs Information
Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Seth Arnold (seth-arnold) wrote :

retries seems a funny name for something that actually describes sleep duration; I didn't expect to see the retries=[] assignment. But that's just nit-picking..

Thanks

Revision history for this message
Scott Moser (smoser) wrote :

if retries is None:
   retries = []

that is just to make sure that
a.) retries is an iterable
b.) avoid modification of a default in parameter. Since None is a immutable but list() is not.

suggestions on the name?

retry_delays ?

Revision history for this message
Brian Murray (brian-murray) wrote :

I've an in-line comment about the HTTPError.

With regards to retries, I think retry_delays is a better name and maybe sleep_waits instead of sleeps but again this is just nitpicking.

1045. By Scott Moser

update HTTPError comment per bdmurray review

1046. By Scott Moser

adjust for bdmurray feedback. Add docstring to get_info_from_https.

sleeps -> sleep_delays
retries -> retry_delays

Revision history for this message
Scott Moser (smoser) wrote :

I adjusted per both of Brian's suggestions.

It is hard to use 'add-apt-repository' in a loop and see that it
used retries at all because there is no logging or '-v' output
or anything in that path. So, I have a script that I'm testing with at:
  http://paste.ubuntu.com/p/7QZftQNCPv/

If you wait long enough you'll see a 'Succeeded on try 1' message,
or a failure that will mention trying 3 times.

$ python ./my.py --help

This is test script for merge proposal for bug 1779302 at
https://code.launchpad.net/~smoser/software-properties/trunk.lp-1779302-retry-recv-keys/+merge/349213

Run it like:

  python testme.py https://..../
    or
  python testme.py fingerprint
    or
  python testme.py

  fingerprint:
     use keyserver.ubuntu.com url to read fprint as add-apt-repository does.
  https://
     read the url provided

  if no argument given, default
  fingerprint=03A13844F2209DCECC02378B251157FC154A23ED

python can be python2 or python3.

Revision history for this message
Scott Moser (smoser) wrote :

Just for later info, my posted script in python2 eventually caught both a success-on-retry and an error-after-retry:

$ python ./my.py
Looking at url=https://keyserver.ubuntu.com/pks/lookup?op=get&options=mr&exact=on&search=0x03A13844F2209DCECC02378B251157FC154A23ED
. [0.597575]: 3137 retries-used=0
<snip>
. [0.545431]: 3137 retries-used=0
. [0.538139]: 3137 retries-used=0
. [0.549522]: 3137 retries-used=0
. [0.541063]: 3137 retries-used=0
. [0.548106]: 3137 retries-used=0
. [0.541548]: 3137 retries-used=0
. [6.445298]: 3137 retries-used=0
Returning delay 1
Returning delay 2
. [64.300604]: 3137 retries-used=2
Suceeded on try 3
Returning delay 1
Returning delay 2
Errored out, Used 2 retries
Traceback (most recent call last):
  File "./my.py", line 69, in <module>
    retry_delays=myiter)
  File "/home/smoser-public/src/software-properties/trunk.lp-1779302-retry-recv-keys/softwareproperties/ppa.py", line 96, in get_info_from_https
    data = func(lp_url=url, accept_json=accept_json, retry_delays=retry_delays)
  File "/home/smoser-public/src/software-properties/trunk.lp-1779302-retry-recv-keys/softwareproperties/ppa.py", line 199, in _get_https_content_pycurl
    original_error=err)
softwareproperties.ppa.PPAException: 'Error reading https://keyserver.ubuntu.com/pks/lookup?op=get&options=mr&exact=on&search=0x03A13844F2209DCECC02378B251157FC154A23ED (3 tries): response code 503'

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'softwareproperties/ppa.py'
2--- softwareproperties/ppa.py 2018-03-30 13:59:59 +0000
3+++ softwareproperties/ppa.py 2018-07-16 17:34:48 +0000
4@@ -28,6 +28,7 @@
5 import shutil
6 import subprocess
7 import tempfile
8+import time
9
10 from gettext import gettext as _
11 from threading import Thread
12@@ -79,18 +80,26 @@
13 def encode(s):
14 return re.sub("[^a-zA-Z0-9_-]", "_", s)
15
16-def get_info_from_https(url, accept_json):
17- if NEED_PYCURL:
18- # python2 has no cert verification so we need pycurl
19- data = _get_https_content_pycurl(url, accept_json)
20- else:
21- # python3 has cert verification so we can use the buildin urllib
22- data = _get_https_content_py3(url, accept_json)
23+
24+def get_info_from_https(url, accept_json, retry_delays=None):
25+ """Return the content from url.
26+ accept_json indicates that:
27+ a.) Send header Accept: 'application/json'
28+ b.) Instead of raw content, return json.loads(content)
29+ retry_delays is None or an iterator (including list or tuple)
30+ If it is None, no retries will be done.
31+ If it is an iterator, each value is number of seconds to delay before
32+ retrying. For example, retry_delays=(3,5) means to try up to 3
33+ times, with a 3s delay after first failure and 5s delay after second.
34+ Retries will not be done on 404."""
35+ func = _get_https_content_pycurl if NEED_PYCURL else _get_https_content_py3
36+ data = func(lp_url=url, accept_json=accept_json, retry_delays=retry_delays)
37 if accept_json:
38 return json.loads(data)
39 else:
40 return data
41
42+
43 def get_info_from_lp(lp_url):
44 return get_info_from_https(lp_url, True)
45
46@@ -113,41 +122,81 @@
47 return os.path.basename(get_info_from_lp(lp_url)["current_series_link"])
48
49
50-def _get_https_content_py3(lp_url, accept_json):
51- try:
52- headers = {"Accept":" application/json"} if accept_json else {}
53- request = urllib.request.Request(str(lp_url), headers=headers)
54- lp_page = urllib.request.urlopen(request, cafile=LAUNCHPAD_PPA_CERT)
55- data = lp_page.read().decode("utf-8", "strict")
56- except (URLError, HTTPException) as e:
57- # HTTPException doesn't have a reason but might have a string
58- # representation
59- reason = hasattr(e, "reason") and e.reason or e
60- raise PPAException("Error reading %s: %s" % (lp_url, reason), e)
61- return data
62-
63-def _get_https_content_pycurl(lp_url, accept_json):
64+def _get_https_content_py3(lp_url, accept_json, retry_delays=None):
65+ if retry_delays is None:
66+ retry_delays = []
67+
68+ trynum = 0
69+ err = None
70+ sleep_waits = iter(retry_delays)
71+ headers = {"Accept": "application/json"} if accept_json else {}
72+
73+ while True:
74+ trynum += 1
75+ try:
76+ request = urllib.request.Request(str(lp_url), headers=headers)
77+ lp_page = urllib.request.urlopen(request,
78+ cafile=LAUNCHPAD_PPA_CERT)
79+ return lp_page.read().decode("utf-8", "strict")
80+ except (HTTPException, URLError) as e:
81+ err = PPAException(
82+ "Error reading %s (%d tries): %s" % (lp_url, trynum, e.reason),
83+ e)
84+ # do not retry on 404. HTTPError is a subclass of URLError.
85+ if isinstance(e, HTTPError) and e.code == 404:
86+ break
87+ try:
88+ time.sleep(next(sleep_waits))
89+ except StopIteration:
90+ break
91+
92+ raise err
93+
94+
95+def _get_https_content_pycurl(lp_url, accept_json, retry_delays=None):
96 # this is the fallback code for python2
97- try:
98- callback = CurlCallback()
99- curl = pycurl.Curl()
100- curl.setopt(pycurl.SSL_VERIFYPEER, 1)
101- curl.setopt(pycurl.SSL_VERIFYHOST, 2)
102- curl.setopt(pycurl.WRITEFUNCTION, callback.body_callback)
103- if LAUNCHPAD_PPA_CERT:
104- curl.setopt(pycurl.CAINFO, LAUNCHPAD_PPA_CERT)
105- curl.setopt(pycurl.URL, str(lp_url))
106- if accept_json:
107- curl.setopt(pycurl.HTTPHEADER, ["Accept: application/json"])
108- curl.perform()
109- response = curl.getinfo(curl.RESPONSE_CODE)
110- curl.close()
111- data = callback.contents
112- except pycurl.error as e:
113- raise PPAException("Error reading %s: %s" % (lp_url, e), e)
114- if response != 200:
115- raise PPAException("Error reading %s: response code %i" % (lp_url, response))
116- return data
117+ if retry_delays is None:
118+ retry_delays = []
119+
120+ trynum = 0
121+ sleep_waits = iter(retry_delays)
122+
123+ while True:
124+ err_msg = None
125+ err = None
126+ trynum += 1
127+ try:
128+ callback = CurlCallback()
129+ curl = pycurl.Curl()
130+ curl.setopt(pycurl.SSL_VERIFYPEER, 1)
131+ curl.setopt(pycurl.SSL_VERIFYHOST, 2)
132+ curl.setopt(pycurl.WRITEFUNCTION, callback.body_callback)
133+ if LAUNCHPAD_PPA_CERT:
134+ curl.setopt(pycurl.CAINFO, LAUNCHPAD_PPA_CERT)
135+ curl.setopt(pycurl.URL, str(lp_url))
136+ if accept_json:
137+ curl.setopt(pycurl.HTTPHEADER, ["Accept: application/json"])
138+ curl.perform()
139+ response = curl.getinfo(curl.RESPONSE_CODE)
140+ curl.close()
141+
142+ if response != 200:
143+ err_msg = "response code %i" % response
144+ except pycurl.error as e:
145+ err_msg = str(e)
146+ err = e
147+
148+ if err_msg is None:
149+ return callback.contents
150+
151+ try:
152+ time.sleep(next(sleep_waits))
153+ except StopIteration:
154+ break
155+
156+ raise PPAException(
157+ "Error reading %s (%d tries): %s" % (lp_url, trynum, err_msg),
158+ original_error=err)
159
160
161 def mangle_ppa_shortcut(shortcut):
162@@ -199,7 +248,8 @@
163 print("Error: signing key fingerprint does not exist")
164 return False
165
166- return get_info_from_https(SKS_KEYSERVER % signing_key_fingerprint, accept_json=False)
167+ return get_info_from_https(SKS_KEYSERVER % signing_key_fingerprint,
168+ accept_json=False, retry_delays=(1, 2))
169
170 def _minimize_key(self, key):
171 p = self.gpg_cmd("import-minimal,import-export")

Subscribers

People subscribed via source and target branches

to status/vote changes: