##// END OF EJS Templates
transactions: add version number to journal.backupfiles...
Durham Goode -
r23064:5dc888b7 stable
parent child Browse files
Show More
@@ -1,357 +1,370
1 1 # transaction.py - simple journaling scheme for mercurial
2 2 #
3 3 # This transaction scheme is intended to gracefully handle program
4 4 # errors and interruptions. More serious failures like system crashes
5 5 # can be recovered with an fsck-like tool. As the whole repository is
6 6 # effectively log-structured, this should amount to simply truncating
7 7 # anything that isn't referenced in the changelog.
8 8 #
9 9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
10 10 #
11 11 # This software may be used and distributed according to the terms of the
12 12 # GNU General Public License version 2 or any later version.
13 13
14 14 from i18n import _
15 15 import errno
16 16 import error, util
17 17
18 version = 1
19
18 20 def active(func):
19 21 def _active(self, *args, **kwds):
20 22 if self.count == 0:
21 23 raise error.Abort(_(
22 24 'cannot use transaction when it is already committed/aborted'))
23 25 return func(self, *args, **kwds)
24 26 return _active
25 27
26 28 def _playback(journal, report, opener, entries, backupentries, unlink=True):
27 29 for f, o, _ignore in entries:
28 30 if o or not unlink:
29 31 try:
30 32 fp = opener(f, 'a')
31 33 fp.truncate(o)
32 34 fp.close()
33 35 except IOError:
34 36 report(_("failed to truncate %s\n") % f)
35 37 raise
36 38 else:
37 39 try:
38 40 opener.unlink(f)
39 41 except (IOError, OSError), inst:
40 42 if inst.errno != errno.ENOENT:
41 43 raise
42 44
43 45 backupfiles = []
44 46 for f, b, _ignore in backupentries:
45 47 filepath = opener.join(f)
46 48 backuppath = opener.join(b)
47 49 try:
48 50 util.copyfile(backuppath, filepath)
49 51 backupfiles.append(b)
50 52 except IOError:
51 53 report(_("failed to recover %s\n") % f)
52 54 raise
53 55
54 56 opener.unlink(journal)
55 57 backuppath = "%s.backupfiles" % journal
56 58 if opener.exists(backuppath):
57 59 opener.unlink(backuppath)
58 60 for f in backupfiles:
59 61 opener.unlink(f)
60 62
61 63 class transaction(object):
62 64 def __init__(self, report, opener, journal, after=None, createmode=None,
63 65 onclose=None, onabort=None):
64 66 """Begin a new transaction
65 67
66 68 Begins a new transaction that allows rolling back writes in the event of
67 69 an exception.
68 70
69 71 * `after`: called after the transaction has been committed
70 72 * `createmode`: the mode of the journal file that will be created
71 73 * `onclose`: called as the transaction is closing, but before it is
72 74 closed
73 75 * `onabort`: called as the transaction is aborting, but before any files
74 76 have been truncated
75 77 """
76 78 self.count = 1
77 79 self.usages = 1
78 80 self.report = report
79 81 self.opener = opener
80 82 self.after = after
81 83 self.onclose = onclose
82 84 self.onabort = onabort
83 85 self.entries = []
84 86 self.backupentries = []
85 87 self.map = {}
86 88 self.backupmap = {}
87 89 self.journal = journal
88 90 self._queue = []
89 91 # a dict of arguments to be passed to hooks
90 92 self.hookargs = {}
91 93
92 94 self.backupjournal = "%s.backupfiles" % journal
93 95 self.file = opener.open(self.journal, "w")
94 96 self.backupsfile = opener.open(self.backupjournal, 'w')
97 self.backupsfile.write('%d\n' % version)
95 98 if createmode is not None:
96 99 opener.chmod(self.journal, createmode & 0666)
97 100 opener.chmod(self.backupjournal, createmode & 0666)
98 101
99 102 # hold file generations to be performed on commit
100 103 self._filegenerators = {}
101 104
102 105 def __del__(self):
103 106 if self.journal:
104 107 self._abort()
105 108
106 109 @active
107 110 def startgroup(self):
108 111 self._queue.append(([], []))
109 112
110 113 @active
111 114 def endgroup(self):
112 115 q = self._queue.pop()
113 116 self.entries.extend(q[0])
114 117 self.backupentries.extend(q[1])
115 118
116 119 offsets = []
117 120 backups = []
118 121 for f, o, _data in q[0]:
119 122 offsets.append((f, o))
120 123
121 124 for f, b, _data in q[1]:
122 125 backups.append((f, b))
123 126
124 127 d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets])
125 128 self.file.write(d)
126 129 self.file.flush()
127 130
128 131 d = ''.join(['%s\0%s\0' % (f, b) for f, b in backups])
129 132 self.backupsfile.write(d)
130 133 self.backupsfile.flush()
131 134
132 135 @active
133 136 def add(self, file, offset, data=None):
134 137 if file in self.map or file in self.backupmap:
135 138 return
136 139 if self._queue:
137 140 self._queue[-1][0].append((file, offset, data))
138 141 return
139 142
140 143 self.entries.append((file, offset, data))
141 144 self.map[file] = len(self.entries) - 1
142 145 # add enough data to the journal to do the truncate
143 146 self.file.write("%s\0%d\n" % (file, offset))
144 147 self.file.flush()
145 148
146 149 @active
147 150 def addbackup(self, file, hardlink=True, vfs=None):
148 151 """Adds a backup of the file to the transaction
149 152
150 153 Calling addbackup() creates a hardlink backup of the specified file
151 154 that is used to recover the file in the event of the transaction
152 155 aborting.
153 156
154 157 * `file`: the file path, relative to .hg/store
155 158 * `hardlink`: use a hardlink to quickly create the backup
156 159 """
157 160
158 161 if file in self.map or file in self.backupmap:
159 162 return
160 163 backupfile = "%s.backup.%s" % (self.journal, file)
161 164 if vfs is None:
162 165 vfs = self.opener
163 166 if vfs.exists(file):
164 167 filepath = vfs.join(file)
165 168 backuppath = self.opener.join(backupfile)
166 169 util.copyfiles(filepath, backuppath, hardlink=hardlink)
167 170 else:
168 171 self.add(file, 0)
169 172 return
170 173
171 174 if self._queue:
172 175 self._queue[-1][1].append((file, backupfile))
173 176 return
174 177
175 178 self.backupentries.append((file, backupfile, None))
176 179 self.backupmap[file] = len(self.backupentries) - 1
177 180 self.backupsfile.write("%s\0%s\0" % (file, backupfile))
178 181 self.backupsfile.flush()
179 182
180 183 @active
181 184 def addfilegenerator(self, genid, filenames, genfunc, order=0, vfs=None):
182 185 """add a function to generates some files at transaction commit
183 186
184 187 The `genfunc` argument is a function capable of generating proper
185 188 content of each entry in the `filename` tuple.
186 189
187 190 At transaction close time, `genfunc` will be called with one file
188 191 object argument per entries in `filenames`.
189 192
190 193 The transaction itself is responsible for the backup, creation and
191 194 final write of such file.
192 195
193 196 The `genid` argument is used to ensure the same set of file is only
194 197 generated once. Call to `addfilegenerator` for a `genid` already
195 198 present will overwrite the old entry.
196 199
197 200 The `order` argument may be used to control the order in which multiple
198 201 generator will be executed.
199 202 """
200 203 # For now, we are unable to do proper backup and restore of custom vfs
201 204 # but for bookmarks that are handled outside this mechanism.
202 205 assert vfs is None or filenames == ('bookmarks',)
203 206 self._filegenerators[genid] = (order, filenames, genfunc, vfs)
204 207
205 208 @active
206 209 def find(self, file):
207 210 if file in self.map:
208 211 return self.entries[self.map[file]]
209 212 if file in self.backupmap:
210 213 return self.backupentries[self.backupmap[file]]
211 214 return None
212 215
213 216 @active
214 217 def replace(self, file, offset, data=None):
215 218 '''
216 219 replace can only replace already committed entries
217 220 that are not pending in the queue
218 221 '''
219 222
220 223 if file not in self.map:
221 224 raise KeyError(file)
222 225 index = self.map[file]
223 226 self.entries[index] = (file, offset, data)
224 227 self.file.write("%s\0%d\n" % (file, offset))
225 228 self.file.flush()
226 229
227 230 @active
228 231 def nest(self):
229 232 self.count += 1
230 233 self.usages += 1
231 234 return self
232 235
233 236 def release(self):
234 237 if self.count > 0:
235 238 self.usages -= 1
236 239 # if the transaction scopes are left without being closed, fail
237 240 if self.count > 0 and self.usages == 0:
238 241 self._abort()
239 242
240 243 def running(self):
241 244 return self.count > 0
242 245
243 246 @active
244 247 def close(self):
245 248 '''commit the transaction'''
246 249 # write files registered for generation
247 250 for entry in sorted(self._filegenerators.values()):
248 251 order, filenames, genfunc, vfs = entry
249 252 if vfs is None:
250 253 vfs = self.opener
251 254 files = []
252 255 try:
253 256 for name in filenames:
254 257 # Some files are already backed up when creating the
255 258 # localrepo. Until this is properly fixed we disable the
256 259 # backup for them.
257 260 if name not in ('phaseroots', 'bookmarks'):
258 261 self.addbackup(name)
259 262 files.append(vfs(name, 'w', atomictemp=True))
260 263 genfunc(*files)
261 264 finally:
262 265 for f in files:
263 266 f.close()
264 267
265 268 if self.count == 1 and self.onclose is not None:
266 269 self.onclose()
267 270
268 271 self.count -= 1
269 272 if self.count != 0:
270 273 return
271 274 self.file.close()
272 275 self.backupsfile.close()
273 276 self.entries = []
274 277 if self.after:
275 278 self.after()
276 279 if self.opener.isfile(self.journal):
277 280 self.opener.unlink(self.journal)
278 281 if self.opener.isfile(self.backupjournal):
279 282 self.opener.unlink(self.backupjournal)
280 283 for _f, b, _ignore in self.backupentries:
281 284 self.opener.unlink(b)
282 285 self.backupentries = []
283 286 self.journal = None
284 287
285 288 @active
286 289 def abort(self):
287 290 '''abort the transaction (generally called on error, or when the
288 291 transaction is not explicitly committed before going out of
289 292 scope)'''
290 293 self._abort()
291 294
292 295 def _abort(self):
293 296 self.count = 0
294 297 self.usages = 0
295 298 self.file.close()
296 299 self.backupsfile.close()
297 300
298 301 if self.onabort is not None:
299 302 self.onabort()
300 303
301 304 try:
302 305 if not self.entries and not self.backupentries:
303 306 if self.journal:
304 307 self.opener.unlink(self.journal)
305 308 if self.backupjournal:
306 309 self.opener.unlink(self.backupjournal)
307 310 return
308 311
309 312 self.report(_("transaction abort!\n"))
310 313
311 314 try:
312 315 _playback(self.journal, self.report, self.opener,
313 316 self.entries, self.backupentries, False)
314 317 self.report(_("rollback completed\n"))
315 318 except Exception:
316 319 self.report(_("rollback failed - please run hg recover\n"))
317 320 finally:
318 321 self.journal = None
319 322
320 323
321 324 def rollback(opener, file, report):
322 325 """Rolls back the transaction contained in the given file
323 326
324 327 Reads the entries in the specified file, and the corresponding
325 328 '*.backupfiles' file, to recover from an incomplete transaction.
326 329
327 330 * `file`: a file containing a list of entries, specifying where
328 331 to truncate each file. The file should contain a list of
329 332 file\0offset pairs, delimited by newlines. The corresponding
330 333 '*.backupfiles' file should contain a list of file\0backupfile
331 334 pairs, delimited by \0.
332 335 """
333 336 entries = []
334 337 backupentries = []
335 338
336 339 fp = opener.open(file)
337 340 lines = fp.readlines()
338 341 fp.close()
339 342 for l in lines:
340 343 try:
341 344 f, o = l.split('\0')
342 345 entries.append((f, int(o), None))
343 346 except ValueError:
344 347 report(_("couldn't read journal entry %r!\n") % l)
345 348
346 349 backupjournal = "%s.backupfiles" % file
347 350 if opener.exists(backupjournal):
348 351 fp = opener.open(backupjournal)
349 352 data = fp.read()
350 353 if len(data) > 0:
351 parts = data.split('\0')
352 # Skip the final part, since it's just a trailing empty space
353 for i in xrange(0, len(parts) - 1, 2):
354 f, b = parts[i:i + 2]
355 backupentries.append((f, b, None))
354 ver = version
355 versionend = data.find('\n')
356 if versionend != -1:
357 ver = data[:versionend]
358 data = data[versionend + 1:]
359
360 if ver == str(version):
361 parts = data.split('\0')
362 # Skip the final part, since it's just a trailing empty space
363 for i in xrange(0, len(parts) - 1, 2):
364 f, b = parts[i:i + 2]
365 backupentries.append((f, b, None))
366 else:
367 report(_("journal was created by a newer version of "
368 "Mercurial"))
356 369
357 370 _playback(file, report, opener, entries, backupentries)
General Comments 0
You need to be logged in to leave comments. Login now