~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/changecache.py

  • Committer: Michael Hudson-Doyle
  • Date: 2012-01-23 21:10:46 UTC
  • mfrom: (461.3.1 bug-390029)
  • Revision ID: michael.hudson@linaro.org-20120123211046-ec3rqt27goxfnxnq
fix a crash in comparing a revision to itself

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
#
18
18
 
19
19
"""
20
 
a cache for chewed-up "change" data structures, which are basically just a
21
 
different way of storing a revision.  the cache improves lookup times 10x
22
 
over bazaar's xml revision structure, though, so currently still worth doing.
 
20
a cache for chewed-up 'file change' data structures, which are basically just
 
21
a different way of storing a revision delta.  the cache improves lookup times
 
22
10x over bazaar's xml revision structure, though, so currently still worth
 
23
doing.
23
24
 
24
25
once a revision is committed in bazaar, it never changes, so once we have
25
26
cached a change, it's good forever.
26
27
"""
27
28
 
28
29
import cPickle
 
30
import marshal
29
31
import os
30
 
 
31
 
from loggerhead import util
32
 
from loggerhead.lockfile import LockFile
33
 
 
34
 
with_lock = util.with_lock('_lock', 'ChangeCache')
35
 
 
36
 
SQLITE_INTERFACE = os.environ.get('SQLITE_INTERFACE', 'sqlite')
37
 
 
38
 
if SQLITE_INTERFACE == 'pysqlite2':
 
32
import tempfile
 
33
import zlib
 
34
 
 
35
try:
 
36
    from sqlite3 import dbapi2
 
37
except ImportError:
39
38
    from pysqlite2 import dbapi2
40
 
    _param_marker = '?'
41
 
elif SQLITE_INTERFACE == 'sqlite':
42
 
    import sqlite as dbapi2
43
 
    _param_marker = '%s'
44
 
 
45
 
 
46
 
_select_stmt = ("select data from revisiondata where revid = ?"
47
 
                ).replace('?', _param_marker)
48
 
_insert_stmt = ("insert into revisiondata (revid, data) "
49
 
                "values (?, ?)").replace('?', _param_marker)
50
 
 
51
 
 
52
 
 
 
39
 
 
40
# We take an optimistic approach to concurrency here: we might do work twice
 
41
# in the case of races, but not crash or corrupt data.
 
42
 
 
43
def safe_init_db(filename, init_sql):
 
44
    # To avoid races around creating the database, we create the db in
 
45
    # a temporary file and rename it into the ultimate location.
 
46
    fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(filename))
 
47
    os.close(fd)
 
48
    con = dbapi2.connect(temp_path)
 
49
    cur = con.cursor()
 
50
    cur.execute(init_sql)
 
51
    con.commit()
 
52
    con.close()
 
53
    os.rename(temp_path, filename)
53
54
 
54
55
class FakeShelf(object):
 
56
 
55
57
    def __init__(self, filename):
56
58
        create_table = not os.path.exists(filename)
 
59
        if create_table:
 
60
            safe_init_db(
 
61
                filename, "create table RevisionData "
 
62
                "(revid binary primary key, data binary)")
57
63
        self.connection = dbapi2.connect(filename)
58
64
        self.cursor = self.connection.cursor()
59
 
        if create_table:
60
 
            self._create_table()
61
 
    def _create_table(self):
62
 
        self.cursor.execute(
 
65
 
 
66
    def _create_table(self, filename):
 
67
        con = dbapi2.connect(filename)
 
68
        cur = con.cursor()
 
69
        cur.execute(
63
70
            "create table RevisionData "
64
71
            "(revid binary primary key, data binary)")
65
 
        self.connection.commit()
 
72
        con.commit()
 
73
        con.close()
 
74
 
66
75
    def _serialize(self, obj):
67
 
        r = dbapi2.Binary(cPickle.dumps(obj, protocol=2))
68
 
        return r
 
76
        return dbapi2.Binary(cPickle.dumps(obj, protocol=2))
 
77
 
69
78
    def _unserialize(self, data):
70
79
        return cPickle.loads(str(data))
 
80
 
71
81
    def get(self, revid):
72
 
        self.cursor.execute(_select_stmt, (revid,))
 
82
        self.cursor.execute(
 
83
            "select data from revisiondata where revid = ?", (revid, ))
73
84
        filechange = self.cursor.fetchone()
74
85
        if filechange is None:
75
86
            return None
76
87
        else:
77
88
            return self._unserialize(filechange[0])
78
 
    def add(self, revid_obj_pairs):
79
 
        for  (r, d) in revid_obj_pairs:
80
 
            self.cursor.execute(_insert_stmt, (r, self._serialize(d)))
81
 
        self.connection.commit()
82
 
 
83
 
 
84
 
class FileChangeCache(object):
85
 
    def __init__(self, history, cache_path):
86
 
        self.history = history
87
 
 
 
89
 
 
90
    def add(self, revid, object):
 
91
        try:
 
92
            self.cursor.execute(
 
93
                "insert into revisiondata (revid, data) values (?, ?)",
 
94
                (revid, self._serialize(object)))
 
95
            self.connection.commit()
 
96
        except dbapi2.IntegrityError:
 
97
            # If another thread or process attempted to set the same key, we
 
98
            # assume it set it to the same value and carry on with our day.
 
99
            pass
 
100
 
 
101
 
 
102
class RevInfoDiskCache(object):
 
103
    """Like `RevInfoMemoryCache` but backed in a sqlite DB."""
 
104
 
 
105
    def __init__(self, cache_path):
88
106
        if not os.path.exists(cache_path):
89
107
            os.mkdir(cache_path)
90
 
 
91
 
        self._changes_filename = os.path.join(cache_path, 'filechanges.sql')
92
 
 
93
 
        # use a lockfile since the cache folder could be shared across
94
 
        # different processes.
95
 
        self._lock = LockFile(os.path.join(cache_path, 'filechange-lock'))
96
 
 
97
 
    @with_lock
98
 
    def get_file_changes(self, entries):
99
 
        out = []
100
 
        missing_entries = []
101
 
        missing_entry_indices = []
102
 
        cache = FakeShelf(self._changes_filename)
103
 
        for entry in entries:
104
 
            changes = cache.get(entry.revid)
105
 
            if changes is not None:
106
 
                out.append(changes)
107
 
            else:
108
 
                missing_entries.append(entry)
109
 
                missing_entry_indices.append(len(out))
110
 
                out.append(None)
111
 
        if missing_entries:
112
 
            missing_changes = self.history.get_file_changes_uncached(missing_entries)
113
 
            revid_changes_pairs = []
114
 
            for i, entry, changes in zip(
115
 
                missing_entry_indices, missing_entries, missing_changes):
116
 
                revid_changes_pairs.append((entry.revid, changes))
117
 
                out[i] = changes
118
 
            cache.add(revid_changes_pairs)
119
 
        return out
 
108
        filename = os.path.join(cache_path, 'revinfo.sql')
 
109
        create_table = not os.path.exists(filename)
 
110
        if create_table:
 
111
            safe_init_db(
 
112
                filename, "create table Data "
 
113
                "(key binary primary key, revid binary, data binary)")
 
114
        self.connection = dbapi2.connect(filename)
 
115
        self.cursor = self.connection.cursor()
 
116
 
 
117
    def get(self, key, revid):
 
118
        self.cursor.execute(
 
119
            "select revid, data from data where key = ?", (dbapi2.Binary(key),))
 
120
        row = self.cursor.fetchone()
 
121
        if row is None:
 
122
            return None
 
123
        elif str(row[0]) != revid:
 
124
            return None
 
125
        else:
 
126
            return marshal.loads(zlib.decompress(row[1]))
 
127
 
 
128
    def set(self, key, revid, data):
 
129
        try:
 
130
            self.cursor.execute(
 
131
                'delete from data where key = ?', (dbapi2.Binary(key), ))
 
132
            blob = zlib.compress(marshal.dumps(data))
 
133
            self.cursor.execute(
 
134
                "insert into data (key, revid, data) values (?, ?, ?)",
 
135
                map(dbapi2.Binary, [key, revid, blob]))
 
136
            self.connection.commit()
 
137
        except dbapi2.IntegrityError:
 
138
            # If another thread or process attempted to set the same key, we
 
139
            # don't care too much -- it's only a cache after all!
 
140
            pass