##// END OF EJS Templates
transaction: document the contents of `tr.backupentries`...
Pierre-Yves David -
r23248:e754b623 default
parent child Browse files
Show More
@@ -1,421 +1,422 b''
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 18 version = 1
19 19
20 20 def active(func):
21 21 def _active(self, *args, **kwds):
22 22 if self.count == 0:
23 23 raise error.Abort(_(
24 24 'cannot use transaction when it is already committed/aborted'))
25 25 return func(self, *args, **kwds)
26 26 return _active
27 27
28 28 def _playback(journal, report, opener, entries, backupentries, unlink=True):
29 29 for f, o, _ignore in entries:
30 30 if o or not unlink:
31 31 try:
32 32 fp = opener(f, 'a')
33 33 fp.truncate(o)
34 34 fp.close()
35 35 except IOError:
36 36 report(_("failed to truncate %s\n") % f)
37 37 raise
38 38 else:
39 39 try:
40 40 opener.unlink(f)
41 41 except (IOError, OSError), inst:
42 42 if inst.errno != errno.ENOENT:
43 43 raise
44 44
45 45 backupfiles = []
46 46 for f, b in backupentries:
47 47 filepath = opener.join(f)
48 48 backuppath = opener.join(b)
49 49 try:
50 50 util.copyfile(backuppath, filepath)
51 51 backupfiles.append(b)
52 52 except IOError:
53 53 report(_("failed to recover %s\n") % f)
54 54 raise
55 55
56 56 opener.unlink(journal)
57 57 backuppath = "%s.backupfiles" % journal
58 58 if opener.exists(backuppath):
59 59 opener.unlink(backuppath)
60 60 for f in backupfiles:
61 61 opener.unlink(f)
62 62
63 63 class transaction(object):
64 64 def __init__(self, report, opener, journal, after=None, createmode=None,
65 65 onclose=None, onabort=None):
66 66 """Begin a new transaction
67 67
68 68 Begins a new transaction that allows rolling back writes in the event of
69 69 an exception.
70 70
71 71 * `after`: called after the transaction has been committed
72 72 * `createmode`: the mode of the journal file that will be created
73 73 * `onclose`: called as the transaction is closing, but before it is
74 74 closed
75 75 * `onabort`: called as the transaction is aborting, but before any files
76 76 have been truncated
77 77 """
78 78 self.count = 1
79 79 self.usages = 1
80 80 self.report = report
81 81 self.opener = opener
82 82 self.after = after
83 83 self.onclose = onclose
84 84 self.onabort = onabort
85 85 self.entries = []
86 # a list of ('path', 'backuppath') entries.
86 87 self.backupentries = []
87 88 self.map = {}
88 89 self.backupmap = {}
89 90 self.journal = journal
90 91 self._queue = []
91 92 # a dict of arguments to be passed to hooks
92 93 self.hookargs = {}
93 94
94 95 self.backupjournal = "%s.backupfiles" % journal
95 96 self.file = opener.open(self.journal, "w")
96 97 self.backupsfile = opener.open(self.backupjournal, 'w')
97 98 self.backupsfile.write('%d\n' % version)
98 99 if createmode is not None:
99 100 opener.chmod(self.journal, createmode & 0666)
100 101 opener.chmod(self.backupjournal, createmode & 0666)
101 102
102 103 # hold file generations to be performed on commit
103 104 self._filegenerators = {}
104 105 # hold callbalk to write pending data for hooks
105 106 self._pendingcallback = {}
106 107 # True is any pending data have been written ever
107 108 self._anypending = False
108 109 # holds callback to call when writing the transaction
109 110 self._finalizecallback = {}
110 111 # hold callbalk for post transaction close
111 112 self._postclosecallback = {}
112 113
113 114 def __del__(self):
114 115 if self.journal:
115 116 self._abort()
116 117
117 118 @active
118 119 def startgroup(self):
119 120 self._queue.append(([], []))
120 121
121 122 @active
122 123 def endgroup(self):
123 124 q = self._queue.pop()
124 125 self.entries.extend(q[0])
125 126 self.backupentries.extend(q[1])
126 127
127 128 offsets = []
128 129 backups = []
129 130 for f, o, _data in q[0]:
130 131 offsets.append((f, o))
131 132
132 133 for f, b in q[1]:
133 134 backups.append((f, b))
134 135
135 136 d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets])
136 137 self.file.write(d)
137 138 self.file.flush()
138 139
139 140 d = ''.join(['%s\0%s\n' % (f, b) for f, b in backups])
140 141 self.backupsfile.write(d)
141 142 self.backupsfile.flush()
142 143
143 144 @active
144 145 def add(self, file, offset, data=None):
145 146 if file in self.map or file in self.backupmap:
146 147 return
147 148 if self._queue:
148 149 self._queue[-1][0].append((file, offset, data))
149 150 return
150 151
151 152 self.entries.append((file, offset, data))
152 153 self.map[file] = len(self.entries) - 1
153 154 # add enough data to the journal to do the truncate
154 155 self.file.write("%s\0%d\n" % (file, offset))
155 156 self.file.flush()
156 157
157 158 @active
158 159 def addbackup(self, file, hardlink=True, vfs=None):
159 160 """Adds a backup of the file to the transaction
160 161
161 162 Calling addbackup() creates a hardlink backup of the specified file
162 163 that is used to recover the file in the event of the transaction
163 164 aborting.
164 165
165 166 * `file`: the file path, relative to .hg/store
166 167 * `hardlink`: use a hardlink to quickly create the backup
167 168 """
168 169
169 170 if file in self.map or file in self.backupmap:
170 171 return
171 172 backupfile = "%s.backup.%s" % (self.journal, file)
172 173 if vfs is None:
173 174 vfs = self.opener
174 175 if vfs.exists(file):
175 176 filepath = vfs.join(file)
176 177 backuppath = self.opener.join(backupfile)
177 178 util.copyfiles(filepath, backuppath, hardlink=hardlink)
178 179 else:
179 180 self.add(file, 0)
180 181 return
181 182
182 183 if self._queue:
183 184 self._queue[-1][1].append((file, backupfile))
184 185 return
185 186
186 187 self.backupentries.append((file, backupfile))
187 188 self.backupmap[file] = len(self.backupentries) - 1
188 189 self.backupsfile.write("%s\0%s\n" % (file, backupfile))
189 190 self.backupsfile.flush()
190 191
191 192 @active
192 193 def addfilegenerator(self, genid, filenames, genfunc, order=0, vfs=None):
193 194 """add a function to generates some files at transaction commit
194 195
195 196 The `genfunc` argument is a function capable of generating proper
196 197 content of each entry in the `filename` tuple.
197 198
198 199 At transaction close time, `genfunc` will be called with one file
199 200 object argument per entries in `filenames`.
200 201
201 202 The transaction itself is responsible for the backup, creation and
202 203 final write of such file.
203 204
204 205 The `genid` argument is used to ensure the same set of file is only
205 206 generated once. Call to `addfilegenerator` for a `genid` already
206 207 present will overwrite the old entry.
207 208
208 209 The `order` argument may be used to control the order in which multiple
209 210 generator will be executed.
210 211 """
211 212 # For now, we are unable to do proper backup and restore of custom vfs
212 213 # but for bookmarks that are handled outside this mechanism.
213 214 assert vfs is None or filenames == ('bookmarks',)
214 215 self._filegenerators[genid] = (order, filenames, genfunc, vfs)
215 216
216 217 def _generatefiles(self):
217 218 # write files registered for generation
218 219 for entry in sorted(self._filegenerators.values()):
219 220 order, filenames, genfunc, vfs = entry
220 221 if vfs is None:
221 222 vfs = self.opener
222 223 files = []
223 224 try:
224 225 for name in filenames:
225 226 # Some files are already backed up when creating the
226 227 # localrepo. Until this is properly fixed we disable the
227 228 # backup for them.
228 229 if name not in ('phaseroots', 'bookmarks'):
229 230 self.addbackup(name)
230 231 files.append(vfs(name, 'w', atomictemp=True))
231 232 genfunc(*files)
232 233 finally:
233 234 for f in files:
234 235 f.close()
235 236
236 237 @active
237 238 def find(self, file):
238 239 if file in self.map:
239 240 return self.entries[self.map[file]]
240 241 if file in self.backupmap:
241 242 return self.backupentries[self.backupmap[file]]
242 243 return None
243 244
244 245 @active
245 246 def replace(self, file, offset, data=None):
246 247 '''
247 248 replace can only replace already committed entries
248 249 that are not pending in the queue
249 250 '''
250 251
251 252 if file not in self.map:
252 253 raise KeyError(file)
253 254 index = self.map[file]
254 255 self.entries[index] = (file, offset, data)
255 256 self.file.write("%s\0%d\n" % (file, offset))
256 257 self.file.flush()
257 258
258 259 @active
259 260 def nest(self):
260 261 self.count += 1
261 262 self.usages += 1
262 263 return self
263 264
264 265 def release(self):
265 266 if self.count > 0:
266 267 self.usages -= 1
267 268 # if the transaction scopes are left without being closed, fail
268 269 if self.count > 0 and self.usages == 0:
269 270 self._abort()
270 271
271 272 def running(self):
272 273 return self.count > 0
273 274
274 275 def addpending(self, category, callback):
275 276 """add a callback to be called when the transaction is pending
276 277
277 278 Category is a unique identifier to allow overwriting an old callback
278 279 with a newer callback.
279 280 """
280 281 self._pendingcallback[category] = callback
281 282
282 283 @active
283 284 def writepending(self):
284 285 '''write pending file to temporary version
285 286
286 287 This is used to allow hooks to view a transaction before commit'''
287 288 categories = sorted(self._pendingcallback)
288 289 for cat in categories:
289 290 # remove callback since the data will have been flushed
290 291 any = self._pendingcallback.pop(cat)()
291 292 self._anypending = self._anypending or any
292 293 return self._anypending
293 294
294 295 @active
295 296 def addfinalize(self, category, callback):
296 297 """add a callback to be called when the transaction is closed
297 298
298 299 Category is a unique identifier to allow overwriting old callbacks with
299 300 newer callbacks.
300 301 """
301 302 self._finalizecallback[category] = callback
302 303
303 304 @active
304 305 def addpostclose(self, category, callback):
305 306 """add a callback to be called after the transaction is closed
306 307
307 308 Category is a unique identifier to allow overwriting an old callback
308 309 with a newer callback.
309 310 """
310 311 self._postclosecallback[category] = callback
311 312
312 313 @active
313 314 def close(self):
314 315 '''commit the transaction'''
315 316 if self.count == 1 and self.onclose is not None:
316 317 self._generatefiles()
317 318 categories = sorted(self._finalizecallback)
318 319 for cat in categories:
319 320 self._finalizecallback[cat]()
320 321 self.onclose()
321 322
322 323 self.count -= 1
323 324 if self.count != 0:
324 325 return
325 326 self.file.close()
326 327 self.backupsfile.close()
327 328 self.entries = []
328 329 if self.after:
329 330 self.after()
330 331 if self.opener.isfile(self.journal):
331 332 self.opener.unlink(self.journal)
332 333 if self.opener.isfile(self.backupjournal):
333 334 self.opener.unlink(self.backupjournal)
334 335 for _f, b in self.backupentries:
335 336 self.opener.unlink(b)
336 337 self.backupentries = []
337 338 self.journal = None
338 339 # run post close action
339 340 categories = sorted(self._postclosecallback)
340 341 for cat in categories:
341 342 self._postclosecallback[cat]()
342 343
343 344 @active
344 345 def abort(self):
345 346 '''abort the transaction (generally called on error, or when the
346 347 transaction is not explicitly committed before going out of
347 348 scope)'''
348 349 self._abort()
349 350
350 351 def _abort(self):
351 352 self.count = 0
352 353 self.usages = 0
353 354 self.file.close()
354 355 self.backupsfile.close()
355 356
356 357 if self.onabort is not None:
357 358 self.onabort()
358 359
359 360 try:
360 361 if not self.entries and not self.backupentries:
361 362 if self.journal:
362 363 self.opener.unlink(self.journal)
363 364 if self.backupjournal:
364 365 self.opener.unlink(self.backupjournal)
365 366 return
366 367
367 368 self.report(_("transaction abort!\n"))
368 369
369 370 try:
370 371 _playback(self.journal, self.report, self.opener,
371 372 self.entries, self.backupentries, False)
372 373 self.report(_("rollback completed\n"))
373 374 except Exception:
374 375 self.report(_("rollback failed - please run hg recover\n"))
375 376 finally:
376 377 self.journal = None
377 378
378 379
379 380 def rollback(opener, file, report):
380 381 """Rolls back the transaction contained in the given file
381 382
382 383 Reads the entries in the specified file, and the corresponding
383 384 '*.backupfiles' file, to recover from an incomplete transaction.
384 385
385 386 * `file`: a file containing a list of entries, specifying where
386 387 to truncate each file. The file should contain a list of
387 388 file\0offset pairs, delimited by newlines. The corresponding
388 389 '*.backupfiles' file should contain a list of file\0backupfile
389 390 pairs, delimited by \0.
390 391 """
391 392 entries = []
392 393 backupentries = []
393 394
394 395 fp = opener.open(file)
395 396 lines = fp.readlines()
396 397 fp.close()
397 398 for l in lines:
398 399 try:
399 400 f, o = l.split('\0')
400 401 entries.append((f, int(o), None))
401 402 except ValueError:
402 403 report(_("couldn't read journal entry %r!\n") % l)
403 404
404 405 backupjournal = "%s.backupfiles" % file
405 406 if opener.exists(backupjournal):
406 407 fp = opener.open(backupjournal)
407 408 lines = fp.readlines()
408 409 if lines:
409 410 ver = lines[0][:-1]
410 411 if ver == str(version):
411 412 for line in lines[1:]:
412 413 if line:
413 414 # Shave off the trailing newline
414 415 line = line[:-1]
415 416 f, b = line.split('\0')
416 417 backupentries.append((f, b))
417 418 else:
418 419 report(_("journal was created by a newer version of "
419 420 "Mercurial"))
420 421
421 422 _playback(file, report, opener, entries, backupentries)
General Comments 0
You need to be logged in to leave comments. Login now