~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/changecache.py

  • Committer: Matt Nordhoff
  • Date: 2009-06-02 13:26:49 UTC
  • Revision ID: mnordhoff@mattnordhoff.com-20090602132649-p5s5hcahgr3v9w4p
Update --memory-profile's help message to mention Dozer.

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')
 
32
import tempfile
 
33
import zlib
35
34
 
36
35
try:
37
36
    from sqlite3 import dbapi2
38
37
except ImportError:
39
38
    from pysqlite2 import dbapi2
40
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)
41
54
 
42
55
class FakeShelf(object):
 
56
 
43
57
    def __init__(self, filename):
44
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)")
45
63
        self.connection = dbapi2.connect(filename)
46
64
        self.cursor = self.connection.cursor()
47
 
        if create_table:
48
 
            self._create_table()
49
 
    def _create_table(self):
50
 
        self.cursor.execute(
 
65
 
 
66
    def _create_table(self, filename):
 
67
        con = dbapi2.connect(filename)
 
68
        cur = con.cursor()
 
69
        cur.execute(
51
70
            "create table RevisionData "
52
71
            "(revid binary primary key, data binary)")
53
 
        self.connection.commit()
 
72
        con.commit()
 
73
        con.close()
 
74
 
54
75
    def _serialize(self, obj):
55
 
        r = dbapi2.Binary(cPickle.dumps(obj, protocol=2))
56
 
        return r
 
76
        return dbapi2.Binary(cPickle.dumps(obj, protocol=2))
 
77
 
57
78
    def _unserialize(self, data):
58
79
        return cPickle.loads(str(data))
 
80
 
59
81
    def get(self, revid):
60
82
        self.cursor.execute(
61
 
            "select data from revisiondata where revid = ?", (revid,))
 
83
            "select data from revisiondata where revid = ?", (revid, ))
62
84
        filechange = self.cursor.fetchone()
63
85
        if filechange is None:
64
86
            return None
65
87
        else:
66
88
            return self._unserialize(filechange[0])
67
 
    def add(self, revid_obj_pairs):
68
 
        for  (r, d) in revid_obj_pairs:
 
89
 
 
90
    def add(self, revid, object):
 
91
        try:
69
92
            self.cursor.execute(
70
93
                "insert into revisiondata (revid, data) values (?, ?)",
71
 
                (r, self._serialize(d)))
72
 
        self.connection.commit()
 
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
73
100
 
74
101
 
75
102
class FileChangeCache(object):
76
 
    def __init__(self, history, cache_path):
77
 
        self.history = history
 
103
 
 
104
    def __init__(self, cache_path):
78
105
 
79
106
        if not os.path.exists(cache_path):
80
107
            os.mkdir(cache_path)
81
108
 
82
109
        self._changes_filename = os.path.join(cache_path, 'filechanges.sql')
83
110
 
84
 
        # use a lockfile since the cache folder could be shared across
85
 
        # different processes.
86
 
        self._lock = LockFile(os.path.join(cache_path, 'filechange-lock'))
87
 
 
88
 
    @with_lock
89
 
    def get_file_changes(self, entries):
90
 
        out = []
91
 
        missing_entries = []
92
 
        missing_entry_indices = []
 
111
    def get_file_changes(self, entry):
93
112
        cache = FakeShelf(self._changes_filename)
94
 
        for entry in entries:
95
 
            changes = cache.get(entry.revid)
96
 
            if changes is not None:
97
 
                out.append(changes)
98
 
            else:
99
 
                missing_entries.append(entry)
100
 
                missing_entry_indices.append(len(out))
101
 
                out.append(None)
102
 
        if missing_entries:
103
 
            missing_changes = self.history.get_file_changes_uncached(missing_entries)
104
 
            revid_changes_pairs = []
105
 
            for i, entry, changes in zip(
106
 
                missing_entry_indices, missing_entries, missing_changes):
107
 
                revid_changes_pairs.append((entry.revid, changes))
108
 
                out[i] = changes
109
 
            cache.add(revid_changes_pairs)
110
 
        return out
 
113
        changes = cache.get(entry.revid)
 
114
        if changes is None:
 
115
            changes = self.history.get_file_changes_uncached(entry)
 
116
            cache.add(entry.revid, changes)
 
117
        return changes
 
118
 
 
119
 
 
120
class RevInfoDiskCache(object):
 
121
    """Like `RevInfoMemoryCache` but backed in a sqlite DB."""
 
122
 
 
123
    def __init__(self, cache_path):
 
124
        if not os.path.exists(cache_path):
 
125
            os.mkdir(cache_path)
 
126
        filename = os.path.join(cache_path, 'revinfo.sql')
 
127
        create_table = not os.path.exists(filename)
 
128
        if create_table:
 
129
            safe_init_db(
 
130
                filename, "create table Data "
 
131
                "(key binary primary key, revid binary, data binary)")
 
132
        self.connection = dbapi2.connect(filename)
 
133
        self.cursor = self.connection.cursor()
 
134
 
 
135
    def get(self, key, revid):
 
136
        self.cursor.execute(
 
137
            "select revid, data from data where key = ?", (dbapi2.Binary(key),))
 
138
        row = self.cursor.fetchone()
 
139
        if row is None:
 
140
            return None
 
141
        elif str(row[0]) != revid:
 
142
            return None
 
143
        else:
 
144
            return marshal.loads(zlib.decompress(row[1]))
 
145
 
 
146
    def set(self, key, revid, data):
 
147
        try:
 
148
            self.cursor.execute(
 
149
                'delete from data where key = ?', (dbapi2.Binary(key), ))
 
150
            blob = zlib.compress(marshal.dumps(data))
 
151
            self.cursor.execute(
 
152
                "insert into data (key, revid, data) values (?, ?, ?)",
 
153
                map(dbapi2.Binary, [key, revid, blob]))
 
154
            self.connection.commit()
 
155
        except dbapi2.IntegrityError:
 
156
            # If another thread or process attempted to set the same key, we
 
157
            # don't care too much -- it's only a cache after all!
 
158
            pass