Skip to content

Commit 11ccfd9

Browse files
committed
archive: add retry to more functions
the archive cmd relies on those functions. they can also fail with an unreliable server. applying the retry mechanism here might mitigate those issues
1 parent 970f4b3 commit 11ccfd9

File tree

2 files changed

+78
-18
lines changed

2 files changed

+78
-18
lines changed

pym/bob/archive.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -934,18 +934,18 @@ def _putUploadFile(self, path, tmp, overwrite):
934934
return self.__retry(lambda: self._webdav.upload(path, tmp, overwrite))
935935

936936
def _listDir(self, path):
937-
path_info = self._webdav.listdir(path)
937+
path_info = self.__retry(lambda: self._webdav.listdir(path))
938938
entries = []
939939
for info in path_info:
940940
if info["path"]:
941941
entries.append(removePrefix(info["path"], path + "/"))
942942
return entries
943943

944944
def _delete(self, filename):
945-
self._webdav.delete(filename)
945+
self.__retry(lambda: self._webdav.delete(filename))
946946

947947
def _stat(self, filename):
948-
stats = self._webdav.stat(filename)
948+
stats = self.__retry(lambda: self._webdav.stat(filename))
949949
if stats['etag'] is not None and not stats['etag'].startswith('W/'):
950950
return stats['etag']
951951
if not stats['mdate'] or not stats['len']:
@@ -954,14 +954,14 @@ def _stat(self, filename):
954954
return struct.pack('=dL', parsedate_to_datetime(stats['mdate']).timestamp(), stats['len'])
955955

956956
def _getAudit(self, filename):
957-
downloader = self._webdav.getPartialDownloader("/".join([self.__url.path, filename]))
957+
downloader = self.__retry(lambda: self._webdav.getPartialDownloader("/".join([self.__url.path, filename])))
958958
while True:
959959
try:
960960
file = io.BytesIO(downloader.get())
961961
return self._extractAudit(fileobj=file)
962962
except (EOFError, tarfile.ReadError):
963963
# partial downloader reached EOF or could not extract the audit from the tarfile, so we get more data
964-
downloader.more()
964+
self.__retry(lambda: downloader.more())
965965
pass
966966

967967
def _getArchiveUri(self):

test/unit/test_archive.py

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from unittest.mock import patch
1010
import asyncio
1111
import base64
12+
import gzip
1213
import http.server
14+
import posixpath
1315
import os, os.path
1416
import socketserver
1517
import stat
@@ -23,13 +25,16 @@
2325
from bob.archive import DummyArchive, HttpArchive, getArchiver
2426
from bob.errors import BuildError
2527
from bob.utils import runInEventLoop, getProcessPoolExecutor
28+
from bob.webdav import HTTPException
2629

2730
DOWNLOAD_ARITFACT = b'\x00'*20
2831
NOT_EXISTS_ARTIFACT = b'\x01'*20
2932
WRONG_VERSION_ARTIFACT = b'\x02'*20
3033
ERROR_UPLOAD_ARTIFACT = b'\x03'*20
3134
ERROR_DOWNLOAD_ARTIFACT = b'\x04'*20
3235
BROKEN_ARTIFACT = b'\xba\xdc\x0f\xfe'*5
36+
VALID_ARTIFACT = b'\x05'*20
37+
EMPTY_AUDIT = '{"artifact":{"variant-id":"1","build-id":"1","artifact-id":"1","result-hash":"1","meta":"1","build":"1","dependencies":{}}, "references":[]}'
3338

3439
UPLOAD1_ARTIFACT = b'\x10'*20
3540
UPLOAD2_ARTIFACT = b'\x11'*20
@@ -63,17 +68,23 @@ def run(coro):
6368

6469
class Base:
6570

66-
def __createArtifact(self, bid, version="1"):
71+
def _createArtifact(self, bid, version="1", valid_data=False):
6772
bid = hexlify(bid).decode("ascii")
6873
name = os.path.join(self.repo.name, bid[0:2], bid[2:4], bid[4:] + "-1.tgz")
6974
os.makedirs(os.path.dirname(name), exist_ok=True)
70-
return self.__createArtifactByName(name, version)
75+
return self.__createArtifactByName(name, version, valid_data)
7176

72-
def __createArtifactByName(self, name, version="1"):
77+
def __createArtifactByName(self, name, version="1", valid_data=False):
7378
pax = { 'bob-archive-vsn' : version }
7479
with tarfile.open(name, "w|gz", format=tarfile.PAX_FORMAT, pax_headers=pax) as tar:
7580
with NamedTemporaryFile() as audit:
76-
audit.write(b'AUDIT')
81+
if valid_data:
82+
# add valid empty audit file
83+
gzf = gzip.GzipFile(mode='wb', fileobj=audit)
84+
gzf.write(EMPTY_AUDIT.encode('utf-8'))
85+
gzf.close()
86+
else:
87+
audit.write(b'AUDIT')
7788
audit.seek(0)
7889
tar.addfile(tar.gettarinfo(arcname="meta/audit.json.gz", fileobj=audit), audit)
7990
with TemporaryDirectory() as content:
@@ -83,7 +94,7 @@ def __createArtifactByName(self, name, version="1"):
8394

8495
return name
8596

86-
def __createBuildId(self, bid):
97+
def _createBuildId(self, bid):
8798
bid = hexlify(bid).decode("ascii")
8899
name = os.path.join(self.repo.name, bid[0:2], bid[2:4], bid[4:] + "-1.buildid")
89100
os.makedirs(os.path.dirname(name), exist_ok=True)
@@ -94,12 +105,6 @@ def __createBuildId(self, bid):
94105
def setUp(self):
95106
# create repo
96107
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-
103108
self.executor = getProcessPoolExecutor()
104109

105110
def tearDown(self):
@@ -172,6 +177,10 @@ def __getSingleArchiveInstance(self, spec):
172177
def setUp(self):
173178
super().setUp()
174179

180+
# add artifacts
181+
self.dummyFileName = self._createArtifact(DOWNLOAD_ARITFACT)
182+
self._createArtifact(WRONG_VERSION_ARTIFACT, "0")
183+
self._createBuildId(DOWNLOAD_ARITFACT)
175184
# create ERROR_DOWNLOAD_ARTIFACT that is there but cannot be opened
176185
bid = hexlify(ERROR_DOWNLOAD_ARTIFACT).decode("ascii")
177186
os.makedirs(os.path.join(self.repo.name, bid[0:2], bid[2:4], bid[4:] + "-1.tgz"), exist_ok=True)
@@ -682,6 +691,7 @@ class TestHttpArchiveRetries(Base, TestCase):
682691

683692
def setUp(self):
684693
super().setUp()
694+
self.VALID_FILE = self._createArtifact(VALID_ARTIFACT, valid_data=True)
685695
self.spec = {'backend' : 'http', 'name' : 'http-archive', 'flags' : ['download', 'upload', 'managed']}
686696

687697
def _getHttpArchiveInstance(self, port):
@@ -698,7 +708,7 @@ def _testRetries(self, r):
698708
with TemporaryDirectory() as tmp:
699709
audit = os.path.join(tmp, "audit.json.gz")
700710
content = os.path.join(tmp, "workspace")
701-
self.assertTrue(run(archive.downloadPackage(DummyStep(), DOWNLOAD_ARITFACT, audit, content,
711+
self.assertTrue(run(archive.downloadPackage(DummyStep(), VALID_ARTIFACT, audit, content,
702712
executor=self.executor)))
703713
# server fails one more time than retries in archive sepc -> fail
704714
with HttpServerMock(repoPath=self.repo.name, retries=r+1) as srv:
@@ -707,7 +717,7 @@ def _testRetries(self, r):
707717
with TemporaryDirectory() as tmp:
708718
audit = os.path.join(tmp, "audit.json.gz")
709719
content = os.path.join(tmp, "workspace")
710-
self.assertFalse(run(archive.downloadPackage(DummyStep(), DOWNLOAD_ARITFACT, audit, content,
720+
self.assertFalse(run(archive.downloadPackage(DummyStep(), VALID_ARTIFACT, audit, content,
711721
executor=self.executor)))
712722

713723
def testRetriesWithNoRetries(self):
@@ -718,3 +728,53 @@ def testRetriesWithOneRetry(self):
718728

719729
def testRetriesWithMultipleRetries(self):
720730
self._testRetries(5)
731+
732+
def testRetriesList(self):
733+
self.spec['retries'] = 1
734+
with HttpServerMock(repoPath=self.repo.name, retries=1) as srv:
735+
archive = self._getHttpArchiveInstance(srv.port)
736+
self.assertIsNotNone(archive._listDir('/'))
737+
with HttpServerMock(repoPath=self.repo.name, retries=2) as srv:
738+
archive = self._getHttpArchiveInstance(srv.port)
739+
with self.assertRaises(HTTPException):
740+
archive._listDir('/')
741+
742+
def testRetriesStat(self):
743+
bid = hexlify(VALID_ARTIFACT).decode("ascii")
744+
filepath = posixpath.join(bid[0:2], bid[2:4], bid[4:] + "-1.tgz")
745+
self.spec['retries'] = 1
746+
with HttpServerMock(repoPath=self.repo.name, retries=1) as srv:
747+
archive = self._getHttpArchiveInstance(srv.port)
748+
self.assertIsNotNone(archive._stat(filepath))
749+
with HttpServerMock(repoPath=self.repo.name, retries=2) as srv:
750+
archive = self._getHttpArchiveInstance(srv.port)
751+
with self.assertRaises(HTTPException):
752+
archive._stat(filepath)
753+
754+
def testRetriesDelete(self):
755+
filename = 'test'
756+
filepath = os.path.join(self.repo.name, filename)
757+
with open(filepath, 'w') as f:
758+
f.write('test')
759+
self.spec['retries'] = 1
760+
with HttpServerMock(repoPath=self.repo.name, retries=1) as srv:
761+
archive = self._getHttpArchiveInstance(srv.port)
762+
self.assertIsNone(archive._delete(filename))
763+
with open(filepath, 'w') as f:
764+
f.write('test')
765+
with HttpServerMock(repoPath=self.repo.name, retries=2) as srv:
766+
archive = self._getHttpArchiveInstance(srv.port)
767+
with self.assertRaises(HTTPException):
768+
archive._delete(filename)
769+
770+
def testRetriesAudit(self):
771+
bid = hexlify(VALID_ARTIFACT).decode("ascii")
772+
filepath = posixpath.join(bid[0:2], bid[2:4], bid[4:] + "-1.tgz")
773+
self.spec['retries'] = 1
774+
with HttpServerMock(repoPath=self.repo.name, retries=1) as srv:
775+
archive = self._getHttpArchiveInstance(srv.port)
776+
self.assertIsNotNone(archive._getAudit(filepath))
777+
with HttpServerMock(repoPath=self.repo.name, retries=2) as srv:
778+
archive = self._getHttpArchiveInstance(srv.port)
779+
with self.assertRaises(HTTPException):
780+
archive._getAudit(filepath)

0 commit comments

Comments
 (0)