summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorNick Mathewson <nickm@torproject.org>2008-11-20 23:33:09 +0000
committerNick Mathewson <nickm@torproject.org>2008-11-20 23:33:09 +0000
commit8418684170c1f5abedbce814591a5ce528f91520 (patch)
treeacaad6b0539e37a208c5d8968ba4cacdb005f76d /lib
parent4c892dc50c74c7a177aa80786b110159c2a1f509 (diff)
a little more documentation.
git-svn-id: file:///home/or/svnrepo/updater/trunk@17346 55e972cd-5a19-0410-ae62-a4d7a52db4cd
Diffstat (limited to 'lib')
-rw-r--r--lib/thandy/download.py131
-rw-r--r--lib/thandy/util.py21
2 files changed, 107 insertions, 45 deletions
diff --git a/lib/thandy/download.py b/lib/thandy/download.py
index 3e4c3dc..0c0d383 100644
--- a/lib/thandy/download.py
+++ b/lib/thandy/download.py
@@ -111,7 +111,10 @@ class DownloadManager:
self.downloadQueue.put(job)
def getRetryTime(self, mirrorList, relPath):
- """DOCDOC"""
+ """Given a mirrorlist and a filename relative to the repository root,
+ return the next time at which we are willing to retry fetching
+ that file from any mirror, or 0 if we are willing to try immediately.
+ """
readyAt = None
for m in mirrorsThatSupport(mirrorList, relPath):
r = self.statusLog.getDelayTime(m['urlbase'])
@@ -123,8 +126,8 @@ class DownloadManager:
return 0
def downloadFailed(self, mirror, relpath):
- """DOCDOC"""
- pass # Track failure; don't try the same mirror right away.
+ """Callback: invoked when a download fails."""
+ pass
def _thread(self, idx):
# Run in the background per thread. idx is the number of the thread.
@@ -158,27 +161,35 @@ class DownloadManager:
self.done.release()
class DownloadFailure:
- # Broadly speaking, these errors are possible:
- #
- # A - The whole internet is down for us, either because our network
- # connection sucks, our proxy is down, or whatever.
- # - A particular mirror is down or nonfunctional.
- #
- # DownloadFailure.connectionFailed()
- #
- # B - The mirror is giving us errors we don't understand.
- # - A particular mirror is missing a file we need.
- # - A particular mirror served us something that was allegedly a
- # file we need, but that file was no good.
- #
- # DownloadFailure.mirrorFailed()
- #
- # C - We finished a partial download and it was no good, but we
- # can't tell who was at fault, because we don't know which
- # part was corrupt.
- #
- # DownloadFailure.badCompoundFile()
- #
+ """Helper class: represents a failure to download an item from a
+ mirror.
+
+ Broadly speaking, these errors are possible:
+
+ A - The whole internet is down for us, either because our network
+ connection sucks, our proxy is down, or whatever.
+ - A particular mirror is down or nonfunctional.
+
+ For these, use DownloadFailure.connectionFailed(): we can't
+ easily tell a mirror failure from a network failure when we
+ have only a single mirror to look at.
+
+ B - The mirror is giving us errors we don't understand.
+ - A particular mirror is missing a file we need.
+ - A particular mirror served us something that was allegedly a
+ file we need, but that file was no good.
+
+ For these, use DownloadFailure.mirrorFailed(): Whether the
+ mirror is broken or we're misunderstanding it, don't try
+ to use it for a while.
+
+ C - We finished a partial download and it was no good, but we
+ can't tell who was at fault, because we don't know which
+ part was corrupt.
+
+ Use DownloadFailure.badCompoundFile(). We don't know who
+ to blame, so we treat the whole network as having gone bibbledy.
+ """
def __init__(self, urlbase, relPath, networkError=False):
self._urlbase = urlbase
self._relPath = relPath
@@ -199,25 +210,35 @@ class DownloadFailure:
return DownloadFailure(urlbase, None, True)
S = thandy.checkJson
-_FAIL_SCHEMA = S.Struct([S.Int(), S.Int()], allowMore=True)
+_FAIL_SCHEMA = S.Struct([S.Int(), thandy.formats.TIME_SCHEMA], allowMore=True)
_STATUS_LOG_SCHEMA = S.Obj(
v=S.Int(),
mirrorFailures=S.DictOf(S.AnyStr(), _FAIL_SCHEMA),
networkFailures=_FAIL_SCHEMA)
del S
+
class DownloadStatusLog:
- """DOCDOC"""
+ """Tracks when we can retry downloading from various mirrors.
+
+ Currently, we treat every failure as affecting a mirror, the
+ network, or both. When a mirror or the network fails, we back
+ off for a while before we attempt it again. For each failure
+ with no intervening success, we back off longer.
+ """
# XXXX get smarter.
# XXXX make this persistent.
def __init__(self, mirrorFailures={}, networkFailures=[0,0]):
self._lock = threading.RLock()
- # urlbase -> [ nFailures, dontTryUntil ]
+ # Map from urlbase to [ nFailures, lastFailureTime ]
self._mirrorFailures = dict(mirrorFailures)
- # None, or [ nFailures, dontTryUntil ]
+ # [ nFailures, lastFailureTime ] for the network as a while.
self._netFailure = list(networkFailures)
def _getDelay(self, isMirror, failureCount):
+ """Return how long we should wait since the 'failureCount'th
+ consecutive failure of something before we try it again.
+ If isMirror, it's a mirror. Otherwise, it's the network."""
if isMirror:
DELAYS = [ 0, 300, 300, 600, 900, 8400, 8400, 9000 ]
else:
@@ -229,17 +250,28 @@ class DownloadStatusLog:
return DELAYS[-1]
def toJSON(self):
+ """Return an object suitable for encoding with json to represent the
+ state of this DownloadStatusLog."""
+ def formatEnt(e):
+ return [ e[0], thandy.formats.formatTime(e[1]) ]
return { 'v': 1,
- 'networkFailures' : self._netFailure,
- 'mirrorFailures' : self._mirrorFailures }
+ 'networkFailures' : formatEnt(self._netFailure),
+ 'mirrorFailures' :
+ dict((k, formatEnt(v)) for k, v
+ in self._mirrorFailures.iteritems())
+ }
@staticmethod
def fromJSON(obj):
_STATUS_LOG_SCHEMA.checkMatch(obj)
- return DownloadStatusLog( obj['mirrorFailures'],
- obj['networkFailures'] )
+ def parseEnt(e):
+ return [ e[0], thandy.formats.parseTime(e[1]) ]
+ return DownloadStatusLog( dict((k, parseEnt(v)) for k,v
+ in obj['mirrorFailures'].iteritems()),
+ parseEnt(obj['networkFailures']))
def failed(self, failure):
+ """Note that 'failure', a DownloadFailure object, has occurred."""
self._lock.acquire()
try:
when = long(failure._when)
@@ -247,7 +279,13 @@ class DownloadStatusLog:
# If there's a mirror to blame, blame it.
if failure._urlbase != None:
s = self._mirrorFailures.setdefault(failure._urlbase, [0, 0])
- if s[1] + 5 < when: # Two failures within 5s count as one.
+ # XXXX This "+ 5" business is a hack. The idea is to keep
+ # multiple failure within 5 seconds from counting as one,
+ # since it's common for us to launch multiple downloads
+ # simultaneously. If we launch 3, and they all fail because
+ # the network is down, we want that to count as 1 failure,
+ # not 3.
+ if s[1] + 5 < when:
s[0] += 1
s[1] = when
@@ -255,13 +293,16 @@ class DownloadStatusLog:
# blame the network too.
if failure._urlbase == None or failure._network:
s = self._netFailure
- if s[1] + 5 < when: # Two failures within 5s count as one.
+ # see note above.
+ if s[1] + 5 < when:
s[0] += 1
s[1] = when
finally:
self._lock.release()
def succeeded(self, urlbase, url):
+ """Note that we have successfully fetched url from the mirror
+ at urlbase."""
self._lock.acquire()
try:
try:
@@ -273,6 +314,7 @@ class DownloadStatusLog:
self._lock.release()
def canRetry(self, urlbase=None, now=None):
+ """Return True iff we are willing to retry the mirror at urlbase."""
if now == None:
now = time.time()
@@ -280,6 +322,8 @@ class DownloadStatusLog:
return d <= now
def getDelayTime(self, urlbase=None):
+ """Return the time after which we're willing to retry fetching from
+ urlbase. 0 also means "we're ready now". """
self._lock.acquire()
try:
readyAt = 0
@@ -320,7 +364,9 @@ class DownloadJob:
self._failure = lambda : None
def setCallbacks(self, success, failure):
- """DOCDOC"""
+ """Make sure that 'success' gets called if this download succeeds,
+ and 'failure' gets called when it fails. Both are called from the
+ downloader thread, so make sure to be threadsafe."""
self._success = success
self._failure = failure
@@ -330,7 +376,8 @@ class DownloadJob:
raise NotImplemented()
def getMirror(self):
- """DOCDOC"""
+ """Return a string identifying the mirror that's doing the downloading,
+ if we know it."""
return None
def getRelativePath(self):
@@ -349,7 +396,8 @@ class DownloadJob:
def download(self):
"""Main interface function: Start the download, and return
- when complete. DOCDOC return value.
+ when complete. Return None on success, and a
+ DownloadFailure on failure.
"""
try:
self._download()
@@ -375,7 +423,10 @@ class DownloadJob:
return DownloadFailure.connectionFailed(None)
def setDownloadStatusLog(self, log):
- """DOCDOC"""
+ """Base our URL-picking decisions on the DownloadStatusLog in
+ 'log'. The caller is still reposnsible for invoking the
+ logs failed() or succeeded methods. XXXX is that bad API
+ design?"""
pass
def _checkTmpFile(self):
@@ -492,6 +543,10 @@ class SimpleDownloadJob(DownloadJob):
return self._url
def mirrorsThatSupport(mirrorList, relPath, urlTypes=None, statusLog=None):
+ """Generator: yields all the mirrors from mirrorList that let us
+ fetch from relPath, whose URL type is in urlTypes (if present),
+ and who are not marked as failed-too-recently in statusLog (if
+ present)."""
now = time.time()
for m in mirrorList['mirrors']:
if urlTypes != None:
diff --git a/lib/thandy/util.py b/lib/thandy/util.py
index ad69fa0..6b75ece 100644
--- a/lib/thandy/util.py
+++ b/lib/thandy/util.py
@@ -46,24 +46,29 @@ def replaceFile(fname, contents, textMode=False):
moveFile(fname_tmp, fname)
def userFilename(name):
+ """Return a path relative to $THANDY_HOME or ~/.thandy whose final path
+ component is 'name', creating parent directories as needed."""
try:
base = os.environ["THANDY_HOME"]
except KeyError:
base = "~/.thandy"
- base = os.path.expanduser(base)
- if not os.path.exists(base):
- os.makedirs(base, 0700)
- return os.path.normpath(os.path.join(base, name))
+
+ result = os.path.normpath(os.path.join(base, name))
+ ensureParentDir(result)
+ return result
def ensureParentDir(name):
- """DOCDOC"""
+ """If the parent directory of 'name' does not exist, create it."""
directory = os.path.split(name)[0]
if not os.path.exists(directory):
os.makedirs(directory, 0700)
def getKeylist(keys_fname, checkKeys=True):
- import thandy.master_keys
-
+ """Return a Keylist() containing all the keys in master_keys, plus
+ all the keys in $THANDY_HOME.preload_keys, plus all the keys stored
+ in keys_fname. If check_keys, exclude from keys_fname any keys not
+ signed by enough master keys. Do not allow master keys to occur in
+ keys_fname."""
keydb = thandy.formats.Keylist()
for key in thandy.master_keys.MASTER_KEYS:
@@ -110,6 +115,8 @@ def randChooseWeighted(lst):
return lst[-1][1]
class NoRegistry(thandy.Exception):
+ """Exception raised when we try to access the registry on a
+ non-win32 machine."""
pass
def getRegistryValue(keyname):