Skip to content
This repository was archived by the owner on Aug 1, 2019. It is now read-only.

Commit ba1c8c0

Browse files
author
James E. Blair
committed
Add git timeout
Timeout remote git operations after 300 seconds. Because it could be in an invalid state, delete the local repo if a timeout occurs (subsequent operations will recreate it). This replaces our use of the clone_from() and fetch() methods from GitPython with lower-level equivalents. The high-level methods do not currently permit the hard timeout. The GitPython requirement is changed to a temporary fork until both gitpython-developers/GitPython#682 and gitpython-developers/GitPython#686 end up in a release. Change-Id: I7f680472a8d67ff2dbe7956a8585fb3714119e65
1 parent bccdfcf commit ba1c8c0

File tree

4 files changed

+86
-23
lines changed

4 files changed

+86
-23
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ PyYAML>=3.1.0
77
Paste
88
WebOb>=1.2.3
99
paramiko>=1.8.0,<2.0.0
10-
GitPython>=0.3.3,<2.1.2
10+
-e git+https://github.com/jeblair/GitPython.git@zuul#egg=GitPython
1111
python-daemon>=2.0.4,<2.1.0
1212
extras
1313
statsd>=1.0.0,<3.0

tests/fixtures/fake_git.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/sh
2+
3+
echo $*
4+
case "$1" in
5+
clone)
6+
dest=$3
7+
mkdir -p $dest/.git
8+
;;
9+
version)
10+
echo "git version 1.0.0"
11+
exit 0
12+
;;
13+
esac
14+
sleep 30

tests/unit/test_merger_repo.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
import os
2020

2121
import git
22+
import testtools
2223

2324
from zuul.merger.merger import Repo
24-
from tests.base import ZuulTestCase
25+
from tests.base import ZuulTestCase, FIXTURE_DIR
2526

2627

2728
class TestMergerRepo(ZuulTestCase):
@@ -74,3 +75,28 @@ def test_ensure_cloned(self):
7475
os.path.join(self.upstream_root, 'org/project2'),
7576
sub_repo.createRepoObject().remotes[0].url,
7677
message="Sub repository points to upstream project2")
78+
79+
def test_clone_timeout(self):
80+
parent_path = os.path.join(self.upstream_root, 'org/project1')
81+
self.patch(git.Git, 'GIT_PYTHON_GIT_EXECUTABLE',
82+
os.path.join(FIXTURE_DIR, 'fake_git.sh'))
83+
work_repo = Repo(parent_path, self.workspace_root,
84+
'[email protected]', 'User Name', '0', '0',
85+
git_timeout=0.001)
86+
# TODO: have the merger and repo classes catch fewer
87+
# exceptions, including this one on initialization. For the
88+
# test, we try cloning again.
89+
with testtools.ExpectedException(git.exc.GitCommandError,
90+
'.*exit code\(-9\)'):
91+
work_repo._ensure_cloned()
92+
93+
def test_fetch_timeout(self):
94+
parent_path = os.path.join(self.upstream_root, 'org/project1')
95+
work_repo = Repo(parent_path, self.workspace_root,
96+
'[email protected]', 'User Name', '0', '0')
97+
work_repo.git_timeout = 0.001
98+
self.patch(git.Git, 'GIT_PYTHON_GIT_EXECUTABLE',
99+
os.path.join(FIXTURE_DIR, 'fake_git.sh'))
100+
with testtools.ExpectedException(git.exc.GitCommandError,
101+
'.*exit code\(-9\)'):
102+
work_repo.update()

zuul/merger/merger.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
# License for the specific language governing permissions and limitations
1414
# under the License.
1515

16+
from contextlib import contextmanager
17+
import logging
18+
import os
19+
import shutil
20+
1621
import git
1722
import gitdb
18-
import os
19-
import logging
2023

2124
import zuul.model
2225

@@ -38,14 +41,25 @@ def reset_repo_to_head(repo):
3841
raise
3942

4043

44+
@contextmanager
45+
def timeout_handler(path):
46+
try:
47+
yield
48+
except git.exc.GitCommandError as e:
49+
if e.status == -9:
50+
# Timeout. The repo could be in a bad state, so delete it.
51+
shutil.rmtree(path)
52+
raise
53+
54+
4155
class ZuulReference(git.Reference):
4256
_common_path_default = "refs/zuul"
4357
_points_to_commits_only = True
4458

4559

4660
class Repo(object):
4761
def __init__(self, remote, local, email, username, speed_limit, speed_time,
48-
sshkey=None, cache_path=None, logger=None):
62+
sshkey=None, cache_path=None, logger=None, git_timeout=300):
4963
if logger is None:
5064
self.log = logging.getLogger("zuul.Repo")
5165
else:
@@ -54,6 +68,7 @@ def __init__(self, remote, local, email, username, speed_limit, speed_time,
5468
'GIT_HTTP_LOW_SPEED_LIMIT': speed_limit,
5569
'GIT_HTTP_LOW_SPEED_TIME': speed_time,
5670
}
71+
self.git_timeout = git_timeout
5772
if sshkey:
5873
self.env['GIT_SSH_COMMAND'] = 'ssh -i %s' % (sshkey,)
5974

@@ -65,7 +80,7 @@ def __init__(self, remote, local, email, username, speed_limit, speed_time,
6580
self._initialized = False
6681
try:
6782
self._ensure_cloned()
68-
except:
83+
except Exception:
6984
self.log.exception("Unable to initialize repo for %s" % remote)
7085

7186
def _ensure_cloned(self):
@@ -78,12 +93,10 @@ def _ensure_cloned(self):
7893
self.log.debug("Cloning from %s to %s" % (self.remote_url,
7994
self.local_path))
8095
if self.cache_path:
81-
git.Repo.clone_from(self.cache_path, self.local_path,
82-
env=self.env)
96+
self._git_clone(self.cache_path)
8397
rewrite_url = True
8498
else:
85-
git.Repo.clone_from(self.remote_url, self.local_path,
86-
env=self.env)
99+
self._git_clone(self.remote_url)
87100
repo = git.Repo(self.local_path)
88101
repo.git.update_environment(**self.env)
89102
# Create local branches corresponding to all the remote branches
@@ -107,6 +120,18 @@ def _ensure_cloned(self):
107120
def isInitialized(self):
108121
return self._initialized
109122

123+
def _git_clone(self, url):
124+
mygit = git.cmd.Git(os.getcwd())
125+
mygit.update_environment(**self.env)
126+
with timeout_handler(self.local_path):
127+
mygit.clone(git.cmd.Git.polish_url(url), self.local_path,
128+
kill_after_timeout=self.git_timeout)
129+
130+
def _git_fetch(self, repo, remote, ref=None, **kwargs):
131+
with timeout_handler(self.local_path):
132+
repo.git.fetch(remote, ref, kill_after_timeout=self.git_timeout,
133+
**kwargs)
134+
110135
def createRepoObject(self):
111136
self._ensure_cloned()
112137
repo = git.Repo(self.local_path)
@@ -228,19 +253,18 @@ def merge(self, ref, strategy=None):
228253

229254
def fetch(self, ref):
230255
repo = self.createRepoObject()
231-
# The git.remote.fetch method may read in git progress info and
232-
# interpret it improperly causing an AssertionError. Because the
233-
# data was fetched properly subsequent fetches don't seem to fail.
234-
# So try again if an AssertionError is caught.
235-
origin = repo.remotes.origin
236-
try:
237-
origin.fetch(ref)
238-
except AssertionError:
239-
origin.fetch(ref)
256+
# NOTE: The following is currently not applicable, but if we
257+
# switch back to fetch methods from GitPython, we need to
258+
# consider it:
259+
# The git.remote.fetch method may read in git progress info and
260+
# interpret it improperly causing an AssertionError. Because the
261+
# data was fetched properly subsequent fetches don't seem to fail.
262+
# So try again if an AssertionError is caught.
263+
self._git_fetch(repo, 'origin', ref)
240264

241265
def fetchFrom(self, repository, ref):
242266
repo = self.createRepoObject()
243-
repo.git.fetch(repository, ref)
267+
self._git_fetch(repo, repository, ref)
244268

245269
def createZuulRef(self, ref, commit='HEAD'):
246270
repo = self.createRepoObject()
@@ -257,15 +281,14 @@ def push(self, local, remote):
257281
def update(self):
258282
repo = self.createRepoObject()
259283
self.log.debug("Updating repository %s" % self.local_path)
260-
origin = repo.remotes.origin
261284
if repo.git.version_info[:2] < (1, 9):
262285
# Before 1.9, 'git fetch --tags' did not include the
263286
# behavior covered by 'git --fetch', so we run both
264287
# commands in that case. Starting with 1.9, 'git fetch
265288
# --tags' is all that is necessary. See
266289
# https://github.com/git/git/blob/master/Documentation/RelNotes/1.9.0.txt#L18-L20
267-
origin.fetch()
268-
origin.fetch(tags=True)
290+
self._git_fetch(repo, 'origin')
291+
self._git_fetch(repo, 'origin', tags=True)
269292

270293
def getFiles(self, files, dirs=[], branch=None, commit=None):
271294
ret = {}

0 commit comments

Comments
 (0)