Skip to content

Commit 8f08321

Browse files
committed
archive: allow configurable retries
some backends might be quite unreliable, so a single retry is not sufficient making the retries configurable can mitigate this issue
1 parent d00b4da commit 8f08321

File tree

5 files changed

+128
-21
lines changed

5 files changed

+128
-21
lines changed

doc/manual/configuration.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2290,10 +2290,12 @@ Type: Dictionary or list of dictionaries
22902290

22912291
The ``archive`` key configures the default binary artifact server(s) that
22922292
should be used. It is either directly an archive backend entry or a list of
2293-
archive backends. For each entry at least the ``backend`` key must be
2294-
specified. Optionally there can be a ``name`` key that is used in log output
2295-
and a ``flags`` key that receives a list of various flags, in particular for
2296-
what operations the backend might be used. See the following list for possible flags.
2293+
archive backends. For each entry at least the ``backend`` key must be specified.
2294+
Optionally there can be a ``name`` key that is used in log output, a ``retries`` key
2295+
that determines the amount of retries for failed operations (default is 1,
2296+
only used for `http` backend) and a ``flags`` key that receives a list of various
2297+
flags, in particular for what operations the backend might be used. See the
2298+
following list for possible flags.
22972299
The default is ``[download, upload]``.
22982300

22992301
``download``
@@ -2372,6 +2374,8 @@ Example::
23722374
archive:
23732375
backend: http
23742376
url: "http://localhost:8001/upload"
2377+
retries: 2
2378+
name: "http-backend"
23752379

23762380
HTTP basic authentication is supported. The user name and password must be put
23772381
in the URL. Be careful to escape special characters of the password with proper

pym/bob/archive.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -872,16 +872,17 @@ def __init__(self, spec):
872872
super().__init__(spec)
873873
self.__url = urllib.parse.urlparse(spec["url"])
874874
self._webdav = WebDav(self.__url, spec.get("sslVerify", True))
875+
self._retries = spec.get("retries", 1)
875876

876877
def __retry(self, request):
877-
retry = True
878+
retries = self._retries
878879
while True:
879880
try:
880881
return (True, request())
881882
except (HTTPException, OSError) as e:
882883
self._webdav._resetConnection()
883-
if not retry: return (False, e)
884-
retry = False
884+
if retries == 0: return (False, e)
885+
retries -= 1
885886

886887
def _canManage(self):
887888
return True

pym/bob/input.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2999,6 +2999,7 @@ def __init__(self):
29992999
httpArchive = baseArchive.copy()
30003000
httpArchive["url"] = HttpUrlValidator()
30013001
httpArchive[schema.Optional("sslVerify")] = bool
3002+
httpArchive[schema.Optional("retries")] = PositiveValidator()
30023003
shellArchive = baseArchive.copy()
30033004
shellArchive.update({
30043005
schema.Optional('download') : str,
@@ -3065,6 +3066,14 @@ def validate(self, data):
30653066
self.__schema.validate(data)
30663067
return (data["name"], data.get("if"))
30673068

3069+
class PositiveValidator:
3070+
def validate(self, data):
3071+
if not isinstance(data, int):
3072+
raise schema.SchemaError(None, "Value must be an integer")
3073+
if data < 0:
3074+
raise schema.SchemaError(None, "Int value must not be negative")
3075+
return data
3076+
30683077
class RecipeSet:
30693078
"""The RecipeSet corresponds to the project root directory.
30703079

test/unit/test_archive.py

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import threading
1919
import urllib.parse
2020
import sys
21+
from mocks.http_server import HttpServerMock
2122

2223
from bob.archive import DummyArchive, HttpArchive, getArchiver
2324
from bob.errors import BuildError
@@ -60,7 +61,7 @@ def run(coro):
6061
with patch('bob.archive.signal.signal'):
6162
return runInEventLoop(coro)
6263

63-
class BaseTester:
64+
class Base:
6465

6566
def __createArtifact(self, bid, version="1"):
6667
bid = hexlify(bid).decode("ascii")
@@ -90,6 +91,23 @@ def __createBuildId(self, bid):
9091
f.write(b'\x00'*20)
9192
return name
9293

94+
def setUp(self):
95+
# create repo
96+
self.repo = TemporaryDirectory()
97+
98+
# add artifacts
99+
self.dummyFileName = self.__createArtifact(DOWNLOAD_ARITFACT)
100+
self.__createArtifact(WRONG_VERSION_ARTIFACT, "0")
101+
self.__createBuildId(DOWNLOAD_ARITFACT)
102+
103+
self.executor = getProcessPoolExecutor()
104+
105+
def tearDown(self):
106+
self.executor.shutdown()
107+
self.repo.cleanup()
108+
109+
class BaseTester(Base):
110+
93111
def __testArtifact(self, bid):
94112
bid = hexlify(bid).decode("ascii")
95113
artifact = os.path.join(self.repo.name, bid[0:2], bid[2:4], bid[4:] + "-1.tgz")
@@ -152,13 +170,7 @@ def __getSingleArchiveInstance(self, spec):
152170
return getArchiver(recipes)
153171

154172
def setUp(self):
155-
# create repo
156-
self.repo = TemporaryDirectory()
157-
158-
# add artifacts
159-
self.dummyFileName = self.__createArtifact(DOWNLOAD_ARITFACT)
160-
self.__createArtifact(WRONG_VERSION_ARTIFACT, "0")
161-
self.__createBuildId(DOWNLOAD_ARITFACT)
173+
super().setUp()
162174

163175
# create ERROR_DOWNLOAD_ARTIFACT that is there but cannot be opened
164176
bid = hexlify(ERROR_DOWNLOAD_ARTIFACT).decode("ascii")
@@ -178,12 +190,6 @@ def setUp(self):
178190
with open(name, "wb") as f:
179191
f.write(b'\x00')
180192

181-
self.executor = getProcessPoolExecutor()
182-
183-
def tearDown(self):
184-
self.executor.shutdown()
185-
self.repo.cleanup()
186-
187193
# standard tests for options
188194
def testOptions(self):
189195
"""Test that wantDownload/wantUpload options work"""
@@ -672,3 +678,43 @@ def _setArchiveSpec(self, spec):
672678
spec["download"] = "cp {}/$BOB_REMOTE_ARTIFACT $BOB_LOCAL_ARTIFACT".format(self.repo.name)
673679
spec["upload"] = "mkdir -p {P}/${{BOB_REMOTE_ARTIFACT%/*}} && cp $BOB_LOCAL_ARTIFACT {P}/$BOB_REMOTE_ARTIFACT".format(P=self.repo.name)
674680

681+
class TestHttpArchiveRetries(Base, TestCase):
682+
683+
def setUp(self):
684+
super().setUp()
685+
self.spec = {'backend' : 'http', 'name' : 'http-archive', 'flags' : ['download', 'upload', 'managed']}
686+
687+
def _getHttpArchiveInstance(self, port):
688+
self.spec["url"] = "http://localhost:{}/".format(port)
689+
recipes = DummyRecipeSet(self.spec)
690+
return getArchiver(recipes)
691+
692+
def _testRetries(self, r):
693+
self.spec['retries'] = r
694+
# server will fail as often as retries in spec
695+
with HttpServerMock(repoPath=self.repo.name, retries=r) as srv:
696+
archive = self._getHttpArchiveInstance(srv.port)
697+
archive.wantDownloadLocal(True)
698+
with TemporaryDirectory() as tmp:
699+
audit = os.path.join(tmp, "audit.json.gz")
700+
content = os.path.join(tmp, "workspace")
701+
self.assertTrue(run(archive.downloadPackage(DummyStep(), DOWNLOAD_ARITFACT, audit, content,
702+
executor=self.executor)))
703+
# server fails one more time than retries in archive sepc -> fail
704+
with HttpServerMock(repoPath=self.repo.name, retries=r+1) as srv:
705+
archive = self._getHttpArchiveInstance(srv.port)
706+
archive.wantDownloadLocal(True)
707+
with TemporaryDirectory() as tmp:
708+
audit = os.path.join(tmp, "audit.json.gz")
709+
content = os.path.join(tmp, "workspace")
710+
self.assertFalse(run(archive.downloadPackage(DummyStep(), DOWNLOAD_ARITFACT, audit, content,
711+
executor=self.executor)))
712+
713+
def testRetriesWithNoRetries(self):
714+
self._testRetries(0)
715+
716+
def testRetriesWithOneRetry(self):
717+
self._testRetries(1)
718+
719+
def testRetriesWithMultipleRetries(self):
720+
self._testRetries(5)

test/unit/test_input_recipeset.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,53 @@ def testArchiveAppendPrepend(self):
299299
},
300300
], recipeSet.archiveSpec())
301301

302+
def testArchiveEntries(self):
303+
# valid config
304+
self.writeDefault(
305+
{
306+
"archive": {
307+
"name": "remote",
308+
"backend": "http",
309+
"url": "http://bob.test/main",
310+
"retries": 1
311+
}
312+
})
313+
recipeSet = RecipeSet()
314+
recipeSet.parse()
315+
self.assertEqual(
316+
[{
317+
"name": "remote",
318+
"backend": "http",
319+
"url": "http://bob.test/main",
320+
"retries": 1
321+
}], recipeSet.archiveSpec())
322+
# string not allowed
323+
self.writeDefault(
324+
{
325+
"archive": {
326+
"name": "remote",
327+
"backend": "http",
328+
"url": "http://bob.test/main",
329+
"retries": "str"
330+
}
331+
})
332+
recipeSet = RecipeSet()
333+
with self.assertRaises(ParseError):
334+
recipeSet.parse()
335+
# negative integer not allowed
336+
self.writeDefault(
337+
{
338+
"archive": {
339+
"name": "remote",
340+
"backend": "http",
341+
"url": "http://bob.test/main",
342+
"retries": -1
343+
}
344+
})
345+
recipeSet = RecipeSet()
346+
with self.assertRaises(ParseError):
347+
recipeSet.parse()
348+
302349
def testMirrorsAppendPrepend(self):
303350
"""Test pre/fallbackMirrorAppend/Prepend keywords"""
304351
for prefix in ["pre", "fallback"]:

0 commit comments

Comments
 (0)