Merge ~laney/germinate:urlopen-timeout into germinate:master

Proposed by Iain Lane
Status: Merged
Merged at revision: eebce4eb69bf7b76f9bfd61b2578ce622ac55fce
Proposed branch: ~laney/germinate:urlopen-timeout
Merge into: germinate:master
Diff against target: 134 lines (+79/-2)
2 files modified
germinate/seeds.py (+14/-2)
germinate/tests/test_seeds.py (+65/-0)
Reviewer Review Type Date Requested Status
Colin Watson Approve
Review via email: mp+397151@code.launchpad.net

Description of the change

We're seeing this happening increasingly frequently in livefs builds now. I'm not sure what changed. Here's a first cut at this. Couple of thoughts

  - Are the constants OK?
  - Should I back off?

If the bug comes back then the new test will hang forever. Not sure if I should add a timeout there, or how to do that really.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

We should probably switch the HTTP library here to requests at some point, but something like this will do for now.

review: Needs Fixing
Revision history for this message
Iain Lane (laney) wrote :

Cheers, I've fixed it as requested.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/germinate/seeds.py b/germinate/seeds.py
2index 98ac34f..e3cfdaa 100644
3--- a/germinate/seeds.py
4+++ b/germinate/seeds.py
5@@ -28,6 +28,7 @@ import logging
6 import os
7 import re
8 import shutil
9+import socket
10 import subprocess
11 import sys
12 import tempfile
13@@ -107,6 +108,8 @@ class SeedVcs(object):
14
15 class Seed(six.Iterator):
16 """A single seed from a collection."""
17+ _n_retries = 5 # how many times to try downloading a URL
18+ _retry_timeout = 10 # timeout between each of the above retries
19
20 def _open_seed_bzr(self, base, branch, name):
21 global _vcs_cache_dir
22@@ -177,11 +180,20 @@ class Seed(six.Iterator):
23 fullpath = os.path.join(path, name)
24 _logger.info("Using %s", fullpath)
25 return open(fullpath)
26- _logger.info("Downloading %s", url)
27 req = Request(url)
28 req.add_header('Cache-Control', 'no-cache')
29 req.add_header('Pragma', 'no-cache')
30- return urlopen(req)
31+ for n in reversed(range(Seed._n_retries)): # try 5 times
32+ try:
33+ _logger.info("Downloading %s", url)
34+ return urlopen(req, timeout=Seed._retry_timeout)
35+ except socket.timeout as e:
36+ err = e
37+ if n > 0:
38+ _logger.warning("Timed out opening %s, retrying...", url)
39+
40+ _logger.error("Timed out opening %s, failing.", url)
41+ raise err
42
43 def _open_seed(self, base, branch, name, vcs=None):
44 if vcs is not None:
45diff --git a/germinate/tests/test_seeds.py b/germinate/tests/test_seeds.py
46index f747b0a..b92ec90 100644
47--- a/germinate/tests/test_seeds.py
48+++ b/germinate/tests/test_seeds.py
49@@ -17,13 +17,20 @@
50 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
51 # 02110-1301, USA.
52
53+import errno
54 import io
55 import os
56+import random
57+import socket
58 import textwrap
59+import threading
60+
61+from fixtures import MockPatch
62
63 from germinate.seeds import (
64 AtomicFile,
65 Seed,
66+ SeedError,
67 SingleSeedStructure,
68 )
69 from germinate.tests.helpers import TestCase
70@@ -109,6 +116,64 @@ class TestSeed(TestCase):
71 self.assertTrue(" * foo\n", lines[0])
72
73
74+class TestSeedHTTP(TestCase):
75+ def setUp(self):
76+ """ Make a server which reads anything you send it but never responds
77+ to you, to test timeouts are triggered properly. """
78+ super(TestSeedHTTP, self).setUp()
79+ self.n_hits = 0
80+ self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
81+ while True:
82+ try:
83+ self.port = random.randint(1025, 65535)
84+ self.server.bind(("localhost", self.port))
85+ break
86+ except OSError as e:
87+ if e.errno != errno.EADDRINUSE:
88+ raise
89+ self.server.listen(1)
90+
91+ def accept():
92+ while self.server is not None:
93+ try:
94+ (client, _) = self.server.accept()
95+ self.n_hits += 1
96+ except (OSError, socket.error) as e:
97+ if e.errno in (errno.EBADF, errno.EINVAL): # closed
98+ return
99+ raise
100+ # We'll read the HTTP request, not reply and the caller should
101+ # eventually timeout
102+ while True:
103+ if not client.recv(1024):
104+ break
105+ client.close()
106+
107+ self.thread = threading.Thread(target=accept)
108+ self.thread.start()
109+
110+ def tearDown(self):
111+ self.server.shutdown(socket.SHUT_RDWR)
112+ self.server.close()
113+ self.server = None
114+ self.thread.join()
115+ super(TestSeedHTTP, self).tearDown()
116+
117+ def test_init_http_timeout(self):
118+ """ When we connect to a server which doesn't respond, we timeout. """
119+ fixture = MockPatch("germinate.seeds.Seed._retry_timeout", 0.1)
120+ with fixture:
121+ try:
122+ Seed(
123+ ["http://localhost:%s" % self.port],
124+ ["collection.dist"],
125+ "test")
126+ except SeedError:
127+ self.assertEqual(self.n_hits, 5)
128+ return
129+ self.assertFalse(True, "SeedError was not raised")
130+
131+
132 class TestSingleSeedStructure(TestCase):
133 def test_basic(self):
134 """A SingleSeedStructure object has the correct basic properties."""

Subscribers

People subscribed via source and target branches

to all changes: