source: tailor/vcpx/statefile.py @ 984

Revision 984, 7.7 KB checked in by lele@…, 8 years ago (diff)

Added a logger to the statefile, to emit some informational noise

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Persistent information details
3# :Creato:   ven 19 ago 2005 22:53:18 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9Tailor needs a generic way to remember which was the last migrated
10revision between the two repositories.  For some backends it could
11derive such information directly from the working dir/repository,
12others have no such capability.
13
14Moreover, since fetching and digesting the history to produce a
15sequence of ChangeSets it's time and bandwidth consuming, the state
16file helps keeping a cache of pending changes.
17"""
18
19__docformat__ = 'reStructuredText'
20
21from cPickle import load, dump
22from signal import signal, SIGINT, SIG_IGN
23
24class StateFile(object):
25    """
26    State file that stores current revision and pending changesets.
27
28    It behaves as an iterator, and source backends loop over not yet
29    applied changesets, calling .applied() after each one: that writes
30    the applied changeset in a *journal* file, much more atomic than
31    rewriting the whole archive each time.
32
33    When the source backend finishes it's job, either because there
34    are no more pending changeset or stopped by an error, it calls
35    .finalize(), that in presence of a journal file adjust the
36    archive filtering out already applied changesets.
37
38    Should an hard error prevent .finalize() call, it will happen
39    automatically next time the state file is loaded.
40    """
41
42    def __init__(self, fname, config):
43        """
44        Initialize a new instance, logging to `tailor.statefile`.
45        """
46
47        from logging import getLogger
48
49        self.filename = fname
50        self.archive = None
51        self.last_applied = None
52        self.current = None
53        self.log = getLogger('tailor.statefile')
54
55    def _load(self):
56        """
57        Open the pickle file and load the last applied changeset.
58        The second pickled object is ignored for backward compatibility.
59        """
60
61        # Take care of the journal file, if present.
62        self.finalize()
63
64        self.current = None
65        try:
66            self.archive = open(self.filename)
67            self.last_applied = load(self.archive)
68            # compatibility dummity: there was the queuelen here
69            load(self.archive)
70        except IOError:
71            self.archive = None
72            self.last_applied = None
73
74    def _write(self, changesets):
75        """
76        Write the state file, that is dump last applied changeset,
77        a dummy None, then one changeset at a time.
78        """
79
80        count = 0
81        previous = signal(SIGINT, SIG_IGN)
82        try:
83            sf = open(self.filename, 'w')
84            dump(self.last_applied, sf)
85            dump(None, sf)
86            for cs in changesets:
87                dump(cs, sf)
88                count += 1
89            sf.close()
90        finally:
91            signal(SIGINT, previous)
92        self.log.info('Cached information about %d pending changesets', count)
93
94    def __str__(self):
95        return self.filename
96
97    def __iter__(self):
98        return self
99
100    def next(self):
101        if not self.archive:
102            raise StopIteration
103        try:
104            self.current = load(self.archive)
105        except EOFError:
106            self.archive.close()
107            self.archive = None
108            raise StopIteration
109        return self.current
110
111    def reversed(self):
112        """
113        Iterate over the changesets, going backward.
114        """
115
116        if self.archive is None:
117            self._load()
118
119        index = []
120        while True:
121            pos = self.archive.tell()
122            try:
123                load(self.archive)
124                index.append(pos)
125            except EOFError:
126                break
127
128        index.reverse()
129
130        for pos in index:
131            self.archive.seek(pos)
132            yield load(self.archive)
133
134    def pending(self):
135        """
136        Verify if there's at least one changeset still pending.
137        """
138
139        if self.archive is None:
140            self._load()
141        if self.archive is None:
142            return False
143
144        pos = self.archive.tell()
145        try:
146            next = load(self.archive)
147        except EOFError:
148            next = None
149        self.archive.seek(pos)
150        return next is not None
151
152    def applied(self, current=None):
153        """
154        Write the applied changeset to the journal file.
155        """
156
157        previous = signal(SIGINT, SIG_IGN)
158        try:
159            self.last_applied = current or self.current
160            journal = open(self.filename + '.journal', 'w')
161            dump(self.last_applied, journal)
162            journal.close()
163        finally:
164            signal(SIGINT, previous)
165
166    def finalize(self):
167        """
168        If there is a journal file, adjust the archive accordingly,
169        dropping already applied changesets.
170        """
171
172        from os.path import exists
173        from os import unlink, rename
174
175        previous = signal(SIGINT, SIG_IGN)
176        try:
177            if self.archive is not None:
178                self.archive.close()
179                self.archive = None
180
181            if exists(self.filename + '.journal'):
182                self.log.debug('Adjusting the state accordingly to journal')
183                # Load last applied changeset from the journal
184                journal = open(self.filename + '.journal')
185                last_applied = load(journal)
186                journal.close()
187
188                # If there is an actual archive (ie, this is not
189                # bootstrap time) load the changesets from there,
190                # skipping the changesets until the last_applied one,
191                # then transfer the remaining to the new archive.
192                if exists(self.filename):
193                    old = open(self.filename)
194                    load(old) # last applied
195                    load(old) # dummy queuelen
196                    try:
197                        cs = load(old)
198                        # Skip already applied changesets
199                        while cs <> last_applied:
200                            cs = load(old)
201                    except EOFError:
202                        cs = None
203                    sf = open(self.filename + '.new', 'w')
204                    dump(last_applied, sf)
205                    dump(None, sf)
206                    if cs is not None:
207                        count = 0
208                        while True:
209                            try:
210                                cs = load(old)
211                            except EOFError:
212                                break
213                            dump(cs, sf)
214                            count += 1
215                        self.log.info('%d pending changesets in state file',
216                                       count)
217                    sf.close()
218                    old.close()
219
220                    oldname = self.filename + '.old'
221                    if exists(oldname):
222                        unlink(oldname)
223                    rename(self.filename, oldname)
224                    rename(sf.name, self.filename)
225                else:
226                    sf = open(self.filename, 'w')
227                    dump(last_applied, sf)
228                    dump(None, sf)
229                    sf.close()
230
231                unlink(journal.name)
232        finally:
233            signal(SIGINT, previous)
234
235    def lastAppliedChangeset(self):
236        """
237        Return the last applied changeset, if any, None otherwise.
238        """
239
240        if self.archive is None:
241            self._load()
242        return self.last_applied
243
244    def setPendingChangesets(self, changesets):
245        """
246        Write pending changesets to the state file.
247        """
248
249        if self.archive is not None:
250            self.archive.close()
251            self.archive = None
252
253        self._write(changesets)
254        self._load()
Note: See TracBrowser for help on using the repository browser.