From 9967f3f54b85055827f6f9464ebc93a023539011 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sun, 28 Feb 2010 18:53:56 +0000 Subject: [PATCH] Added a simple generic file cache This is to allow Distribution objects to cache package queries, rather than looking up everything at the start. This should be useful for querying for versions installable using the distribution package manager, where the complete list is very large and the query can be quite slow. --- tests/testdistro.py | 28 +++++++++++++++- zeroinstall/injector/distro.py | 74 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/tests/testdistro.py b/tests/testdistro.py index 8defcf2..8729114 100755 --- a/tests/testdistro.py +++ b/tests/testdistro.py @@ -1,6 +1,6 @@ #!/usr/bin/env python from basetest import BaseTest, empty_feed -import sys, os +import sys, os, tempfile from StringIO import StringIO import unittest @@ -26,6 +26,32 @@ class TestDistro(BaseTest): def tearDown(self): BaseTest.tearDown(self) + def testCache(self): + src = tempfile.NamedTemporaryFile() + try: + cache = distro.Cache('test-cache', src.name) + self.assertEquals(None, cache.get("foo")) + cache.put("foo", "1") + self.assertEquals("1", cache.get("foo")) + cache.put("foo", "2") + self.assertEquals("2", cache.get("foo")) + + # new cache... + cache = distro.Cache('test-cache', src.name) + self.assertEquals("2", cache.get("foo")) + + src.write("hi") + src.flush() + + self.assertEquals("2", cache.get("foo")) + + # new cache... + cache = distro.Cache('test-cache', src.name) + self.assertEquals(None, cache.get("foo")) + + finally: + src.close() + def factory(self, id): impl = model.DistributionImplementation(self.feed, id) assert id not in self.feed.implementations diff --git a/zeroinstall/injector/distro.py b/zeroinstall/injector/distro.py index d9f5d9d..3b21998 100644 --- a/zeroinstall/injector/distro.py +++ b/zeroinstall/injector/distro.py @@ -20,6 +20,80 @@ _zeroinstall_regexp = '(?:%s)(?:-(?:pre|rc|post|)(?:%s))*' % (_dotted_ints, _dot # This matches the interesting bits of distribution version numbers _version_regexp = '(%s)(-r%s)?' % (_zeroinstall_regexp, _dotted_ints) +# We try to do updates atomically without locking, but we don't worry too much about +# duplicate entries or being a little out of sync with the on-disk copy. +class Cache(object): + def __init__(self, cache_leaf, source): + """Maintain a cache file (e.g. ~/.cache/0install.net/injector/$name). + If the size or mtime of $source has changed, reset the cache first.""" + self.cache_leaf = cache_leaf + self.source = source + self.cache_dir = basedir.save_cache_path(namespaces.config_site, + namespaces.config_prog) + try: + self._load_cache() + except Exception, ex: + info(_("Failed to load cache (%s). Flushing..."), ex) + self.flush() + + def flush(self): + try: + info = os.stat(self.source) + mtime = int(info.st_mtime) + size = info.st_size + except Exception, ex: + warn("Failed to stat %s: %s", self.source, ex) + mtime = size = 0 + self.cache = {} + import tempfile + tmp, tmp_name = tempfile.mkstemp(dir = self.cache_dir) + data = "mtime=%d\nsize=%d\n\n" % (mtime, size) + while data: + wrote = os.write(tmp, data) + data = data[wrote:] + os.rename(tmp_name, os.path.join(self.cache_dir, self.cache_leaf)) + + # Populate self.cache from our saved cache file. + # Throws an exception if the cache doesn't exist or has the wrong format. + def _load_cache(self): + self.cache = cache = {} + stream = file(os.path.join(self.cache_dir, self.cache_leaf)) + try: + info = os.stat(self.source) + meta = {} + for line in stream: + line = line.strip() + if not line: + break + key, value = line.split('=', 1) + if key == 'mtime': + if int(value) != int(info.st_mtime): + raise Exception("Modification time of %s has changed" % self.source) + if key == 'size': + if int(value) != info.st_size: + raise Exception("Size of %s has changed" % self.source) + + for line in stream: + key, value = line.split('=', 1) + cache[key] = value[:-1] + finally: + stream.close() + + def get(self, key): + return self.cache.get(key, None) + + def put(self, key, value): + cache_path = os.path.join(self.cache_dir, self.cache_leaf) + self.cache[key] = value + try: + stream = file(cache_path, 'a') + try: + stream.write('%s=%s\n' % (key, value)) + finally: + stream.close() + except Exception, ex: + warn("Failed to write to cache %s: %s=%s: %s", cache_path, key, value, ex) + def try_cleanup_distro_version(version): """Try to turn a distribution version string into one readable by Zero Install. We do this by stripping off anything we can't parse. -- 2.11.4.GIT