transaction.py
344 lines
| 10.9 KiB
| text/x-python
|
PythonLexer
/ mercurial / transaction.py
|
r17424 | # transaction.py - simple journaling scheme for mercurial | ||
|
r0 | # | ||
# This transaction scheme is intended to gracefully handle program | ||||
# errors and interruptions. More serious failures like system crashes | ||||
# can be recovered with an fsck-like tool. As the whole repository is | ||||
# effectively log-structured, this should amount to simply truncating | ||||
# anything that isn't referenced in the changelog. | ||||
# | ||||
|
r2859 | # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | ||
|
r0 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r0 | |||
|
r3891 | from i18n import _ | ||
|
r20886 | import errno | ||
|
r20882 | import error, util | ||
|
r8289 | |||
def active(func): | ||||
def _active(self, *args, **kwds): | ||||
if self.count == 0: | ||||
raise error.Abort(_( | ||||
'cannot use transaction when it is already committed/aborted')) | ||||
return func(self, *args, **kwds) | ||||
return _active | ||||
|
r0 | |||
|
r20882 | def _playback(journal, report, opener, entries, backupentries, unlink=True): | ||
|
r8294 | for f, o, ignore in entries: | ||
if o or not unlink: | ||||
try: | ||||
|
r13400 | fp = opener(f, 'a') | ||
fp.truncate(o) | ||||
fp.close() | ||||
|
r9686 | except IOError: | ||
|
r8294 | report(_("failed to truncate %s\n") % f) | ||
raise | ||||
else: | ||||
try: | ||||
|
r20084 | opener.unlink(f) | ||
|
r9686 | except (IOError, OSError), inst: | ||
|
r8294 | if inst.errno != errno.ENOENT: | ||
raise | ||||
|
r20882 | |||
backupfiles = [] | ||||
for f, b, ignore in backupentries: | ||||
filepath = opener.join(f) | ||||
backuppath = opener.join(b) | ||||
try: | ||||
util.copyfile(backuppath, filepath) | ||||
backupfiles.append(b) | ||||
except IOError: | ||||
report(_("failed to recover %s\n") % f) | ||||
raise | ||||
|
r20087 | opener.unlink(journal) | ||
|
r20882 | backuppath = "%s.backupfiles" % journal | ||
if opener.exists(backuppath): | ||||
opener.unlink(backuppath) | ||||
for f in backupfiles: | ||||
opener.unlink(f) | ||||
|
r8294 | |||
|
r1559 | class transaction(object): | ||
|
r20881 | def __init__(self, report, opener, journal, after=None, createmode=None, | ||
onclose=None, onabort=None): | ||||
"""Begin a new transaction | ||||
Begins a new transaction that allows rolling back writes in the event of | ||||
an exception. | ||||
* `after`: called after the transaction has been committed | ||||
* `createmode`: the mode of the journal file that will be created | ||||
* `onclose`: called as the transaction is closing, but before it is | ||||
closed | ||||
* `onabort`: called as the transaction is aborting, but before any files | ||||
have been truncated | ||||
""" | ||||
|
r1806 | self.count = 1 | ||
|
r11230 | self.usages = 1 | ||
|
r582 | self.report = report | ||
|
r0 | self.opener = opener | ||
|
r95 | self.after = after | ||
|
r20881 | self.onclose = onclose | ||
self.onabort = onabort | ||||
|
r0 | self.entries = [] | ||
|
r20882 | self.backupentries = [] | ||
|
r42 | self.map = {} | ||
|
r20882 | self.backupmap = {} | ||
|
r0 | self.journal = journal | ||
|
r8363 | self._queue = [] | ||
|
r21150 | # a dict of arguments to be passed to hooks | ||
self.hookargs = {} | ||||
|
r0 | |||
|
r20882 | self.backupjournal = "%s.backupfiles" % journal | ||
|
r20087 | self.file = opener.open(self.journal, "w") | ||
|
r20882 | self.backupsfile = opener.open(self.backupjournal, 'w') | ||
|
r6065 | if createmode is not None: | ||
|
r20087 | opener.chmod(self.journal, createmode & 0666) | ||
|
r20882 | opener.chmod(self.backupjournal, createmode & 0666) | ||
|
r0 | |||
|
r22078 | # hold file generations to be performed on commit | ||
self._filegenerators = {} | ||||
|
r0 | def __del__(self): | ||
|
r558 | if self.journal: | ||
|
r9693 | self._abort() | ||
|
r0 | |||
|
r8289 | @active | ||
|
r8363 | def startgroup(self): | ||
|
r20882 | self._queue.append(([], [])) | ||
|
r8363 | |||
@active | ||||
def endgroup(self): | ||||
q = self._queue.pop() | ||||
|
r20882 | self.entries.extend(q[0]) | ||
self.backupentries.extend(q[1]) | ||||
offsets = [] | ||||
backups = [] | ||||
for f, o, _ in q[0]: | ||||
offsets.append((f, o)) | ||||
for f, b, _ in q[1]: | ||||
backups.append((f, b)) | ||||
d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets]) | ||||
|
r8363 | self.file.write(d) | ||
self.file.flush() | ||||
|
r20882 | d = ''.join(['%s\0%s\0' % (f, b) for f, b in backups]) | ||
self.backupsfile.write(d) | ||||
self.backupsfile.flush() | ||||
|
r8363 | @active | ||
|
r2084 | def add(self, file, offset, data=None): | ||
|
r20882 | if file in self.map or file in self.backupmap: | ||
|
r10282 | return | ||
|
r8363 | if self._queue: | ||
|
r20882 | self._queue[-1][0].append((file, offset, data)) | ||
|
r8363 | return | ||
|
r2084 | self.entries.append((file, offset, data)) | ||
self.map[file] = len(self.entries) - 1 | ||||
|
r0 | # add enough data to the journal to do the truncate | ||
self.file.write("%s\0%d\n" % (file, offset)) | ||||
self.file.flush() | ||||
|
r8289 | @active | ||
|
r20882 | def addbackup(self, file, hardlink=True): | ||
"""Adds a backup of the file to the transaction | ||||
Calling addbackup() creates a hardlink backup of the specified file | ||||
that is used to recover the file in the event of the transaction | ||||
aborting. | ||||
* `file`: the file path, relative to .hg/store | ||||
* `hardlink`: use a hardlink to quickly create the backup | ||||
""" | ||||
if file in self.map or file in self.backupmap: | ||||
return | ||||
|
r22077 | backupfile = "%s.backup.%s" % (self.journal, file) | ||
|
r20882 | if self.opener.exists(file): | ||
filepath = self.opener.join(file) | ||||
backuppath = self.opener.join(backupfile) | ||||
util.copyfiles(filepath, backuppath, hardlink=hardlink) | ||||
else: | ||||
self.add(file, 0) | ||||
return | ||||
if self._queue: | ||||
self._queue[-1][1].append((file, backupfile)) | ||||
return | ||||
self.backupentries.append((file, backupfile, None)) | ||||
self.backupmap[file] = len(self.backupentries) - 1 | ||||
self.backupsfile.write("%s\0%s\0" % (file, backupfile)) | ||||
self.backupsfile.flush() | ||||
@active | ||||
|
r22078 | def addfilegenerator(self, genid, filenames, genfunc, order=0): | ||
"""add a function to generates some files at transaction commit | ||||
The `genfunc` argument is a function capable of generating proper | ||||
content of each entry in the `filename` tuple. | ||||
At transaction close time, `genfunc` will be called with one file | ||||
object argument per entries in `filenames`. | ||||
The transaction itself is responsible for the backup, creation and | ||||
final write of such file. | ||||
The `genid` argument is used to ensure the same set of file is only | ||||
generated once. Call to `addfilegenerator` for a `genid` already | ||||
present will overwrite the old entry. | ||||
The `order` argument may be used to control the order in which multiple | ||||
generator will be executed. | ||||
""" | ||||
self._filegenerators[genid] = (order, filenames, genfunc) | ||||
@active | ||||
|
r2084 | def find(self, file): | ||
if file in self.map: | ||||
return self.entries[self.map[file]] | ||||
|
r20882 | if file in self.backupmap: | ||
return self.backupentries[self.backupmap[file]] | ||||
|
r2084 | return None | ||
|
r8289 | @active | ||
|
r2084 | def replace(self, file, offset, data=None): | ||
|
r8363 | ''' | ||
replace can only replace already committed entries | ||||
that are not pending in the queue | ||||
''' | ||||
|
r2084 | if file not in self.map: | ||
raise KeyError(file) | ||||
index = self.map[file] | ||||
self.entries[index] = (file, offset, data) | ||||
self.file.write("%s\0%d\n" % (file, offset)) | ||||
self.file.flush() | ||||
|
r8289 | @active | ||
|
r1806 | def nest(self): | ||
self.count += 1 | ||||
|
r11230 | self.usages += 1 | ||
|
r1806 | return self | ||
|
r11230 | def release(self): | ||
if self.count > 0: | ||||
self.usages -= 1 | ||||
|
r11685 | # if the transaction scopes are left without being closed, fail | ||
|
r11230 | if self.count > 0 and self.usages == 0: | ||
self._abort() | ||||
|
r1806 | def running(self): | ||
return self.count > 0 | ||||
|
r8289 | @active | ||
|
r0 | def close(self): | ||
|
r9220 | '''commit the transaction''' | ||
|
r22078 | # write files registered for generation | ||
for order, filenames, genfunc in sorted(self._filegenerators.values()): | ||||
files = [] | ||||
try: | ||||
for name in filenames: | ||||
self.addbackup(name) | ||||
files.append(self.opener(name, 'w', atomictemp=True)) | ||||
genfunc(*files) | ||||
finally: | ||||
for f in files: | ||||
f.close() | ||||
|
r20881 | if self.count == 1 and self.onclose is not None: | ||
self.onclose() | ||||
|
r1806 | self.count -= 1 | ||
if self.count != 0: | ||||
return | ||||
|
r0 | self.file.close() | ||
|
r21206 | self.backupsfile.close() | ||
|
r0 | self.entries = [] | ||
|
r95 | if self.after: | ||
|
r785 | self.after() | ||
|
r20087 | if self.opener.isfile(self.journal): | ||
self.opener.unlink(self.journal) | ||||
|
r20882 | if self.opener.isfile(self.backupjournal): | ||
self.opener.unlink(self.backupjournal) | ||||
for f, b, _ in self.backupentries: | ||||
self.opener.unlink(b) | ||||
self.backupentries = [] | ||||
|
r573 | self.journal = None | ||
|
r0 | |||
|
r8289 | @active | ||
|
r0 | def abort(self): | ||
|
r9220 | '''abort the transaction (generally called on error, or when the | ||
transaction is not explicitly committed before going out of | ||||
scope)''' | ||||
|
r8289 | self._abort() | ||
def _abort(self): | ||||
|
r8290 | self.count = 0 | ||
|
r11230 | self.usages = 0 | ||
|
r8290 | self.file.close() | ||
|
r21206 | self.backupsfile.close() | ||
|
r8290 | |||
|
r20881 | if self.onabort is not None: | ||
self.onabort() | ||||
|
r10228 | try: | ||
|
r20882 | if not self.entries and not self.backupentries: | ||
|
r10228 | if self.journal: | ||
|
r20087 | self.opener.unlink(self.journal) | ||
|
r20882 | if self.backupjournal: | ||
self.opener.unlink(self.backupjournal) | ||||
|
r10228 | return | ||
|
r0 | |||
|
r10228 | self.report(_("transaction abort!\n")) | ||
|
r0 | |||
|
r108 | try: | ||
|
r10282 | _playback(self.journal, self.report, self.opener, | ||
|
r20882 | self.entries, self.backupentries, False) | ||
|
r8294 | self.report(_("rollback completed\n")) | ||
|
r16689 | except Exception: | ||
|
r8294 | self.report(_("rollback failed - please run hg recover\n")) | ||
finally: | ||||
self.journal = None | ||||
|
r8290 | |||
|
r8294 | def rollback(opener, file, report): | ||
|
r20882 | """Rolls back the transaction contained in the given file | ||
Reads the entries in the specified file, and the corresponding | ||||
'*.backupfiles' file, to recover from an incomplete transaction. | ||||
* `file`: a file containing a list of entries, specifying where | ||||
to truncate each file. The file should contain a list of | ||||
file\0offset pairs, delimited by newlines. The corresponding | ||||
'*.backupfiles' file should contain a list of file\0backupfile | ||||
pairs, delimited by \0. | ||||
""" | ||||
|
r8294 | entries = [] | ||
|
r20882 | backupentries = [] | ||
|
r8294 | |||
|
r20087 | fp = opener.open(file) | ||
|
r13400 | lines = fp.readlines() | ||
fp.close() | ||||
for l in lines: | ||||
|
r20524 | try: | ||
f, o = l.split('\0') | ||||
entries.append((f, int(o), None)) | ||||
except ValueError: | ||||
report(_("couldn't read journal entry %r!\n") % l) | ||||
|
r0 | |||
|
r20882 | backupjournal = "%s.backupfiles" % file | ||
if opener.exists(backupjournal): | ||||
fp = opener.open(backupjournal) | ||||
data = fp.read() | ||||
if len(data) > 0: | ||||
parts = data.split('\0') | ||||
for i in xrange(0, len(parts), 2): | ||||
f, b = parts[i:i + 1] | ||||
backupentries.append((f, b, None)) | ||||
_playback(file, report, opener, entries, backupentries) | ||||