##// END OF EJS Templates
transaction: work around and document issue with file backup...
transaction: work around and document issue with file backup The backup restoration is actually hard-coded for the main file. And this hard-coded list is the only one used when repairing an interrupted transaction from another process. Solving this problem is out of the scope of this series so we document it and work around its implications.

File last commit:

r22662:c4d63f67 default
r22662:c4d63f67 default
Show More
transaction.py
348 lines | 11.2 KiB | text/x-python | PythonLexer
Mads Kiilerich
fix trivial spelling errors
r17424 # transaction.py - simple journaling scheme for mercurial
mpm@selenic.com
Add back links from file revisions to changeset revisions...
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.
#
Vadim Gelfer
update copyrights.
r2859 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 #
Martin Geisler
updated license to be explicit about GPL version 2
r8225 # This software may be used and distributed according to the terms of the
Matt Mackall
Update license to GPLv2+
r10263 # GNU General Public License version 2 or any later version.
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Matt Mackall
Simplify i18n imports
r3891 from i18n import _
Matt Mackall
transaction: drop extra import caught by pyflakes
r20886 import errno
Durham Goode
transaction: add support for non-append files...
r20882 import error, util
Henrik Stuart
transaction: ensure finished transactions are not reused...
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
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Durham Goode
transaction: add support for non-append files...
r20882 def _playback(journal, report, opener, entries, backupentries, unlink=True):
Mads Kiilerich
cleanup: name unused variables using convention of leading _...
r22204 for f, o, _ignore in entries:
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294 if o or not unlink:
try:
Dan Villiom Podlaski Christiansen
explicitly close files...
r13400 fp = opener(f, 'a')
fp.truncate(o)
fp.close()
Benoit Boissinot
transaction: more specific exceptions, os.unlink can raise OSError
r9686 except IOError:
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294 report(_("failed to truncate %s\n") % f)
raise
else:
try:
FUJIWARA Katsunori
transaction: unlink target file via vfs...
r20084 opener.unlink(f)
Benoit Boissinot
transaction: more specific exceptions, os.unlink can raise OSError
r9686 except (IOError, OSError), inst:
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294 if inst.errno != errno.ENOENT:
raise
Durham Goode
transaction: add support for non-append files...
r20882
backupfiles = []
Mads Kiilerich
cleanup: name unused variables using convention of leading _...
r22204 for f, b, _ignore in backupentries:
Durham Goode
transaction: add support for non-append files...
r20882 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
FUJIWARA Katsunori
transaction: take journal file path relative to vfs to use file API via vfs
r20087 opener.unlink(journal)
Durham Goode
transaction: add support for non-append files...
r20882 backuppath = "%s.backupfiles" % journal
if opener.exists(backuppath):
opener.unlink(backuppath)
for f in backupfiles:
opener.unlink(f)
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294
Eric Hopper
Convert all classes to new-style classes by deriving them from object.
r1559 class transaction(object):
Durham Goode
transaction: add onclose/onabort hook for pre-close logic...
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
"""
mason@suse.com
Automatic nesting into running transactions in the same repository....
r1806 self.count = 1
Ronny Pfannschmidt
make transactions work on non-refcounted python implementations
r11230 self.usages = 1
mpm@selenic.com
Remove all remaining print statements...
r582 self.report = report
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 self.opener = opener
mpm@selenic.com
Beginnings of transaction undo support
r95 self.after = after
Durham Goode
transaction: add onclose/onabort hook for pre-close logic...
r20881 self.onclose = onclose
self.onabort = onabort
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 self.entries = []
Durham Goode
transaction: add support for non-append files...
r20882 self.backupentries = []
mpm@selenic.com
Fix multiple changes to file per transaction
r42 self.map = {}
Durham Goode
transaction: add support for non-append files...
r20882 self.backupmap = {}
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 self.journal = journal
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363 self._queue = []
Pierre-Yves David
transaction: add a notion of hook arguments...
r21150 # a dict of arguments to be passed to hooks
self.hookargs = {}
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Durham Goode
transaction: add support for non-append files...
r20882 self.backupjournal = "%s.backupfiles" % journal
FUJIWARA Katsunori
transaction: take journal file path relative to vfs to use file API via vfs
r20087 self.file = opener.open(self.journal, "w")
Durham Goode
transaction: add support for non-append files...
r20882 self.backupsfile = opener.open(self.backupjournal, 'w')
Alexis S. L. Carvalho
make the journal/undo files from transactions inherit the mode from .hg/store
r6065 if createmode is not None:
FUJIWARA Katsunori
transaction: take journal file path relative to vfs to use file API via vfs
r20087 opener.chmod(self.journal, createmode & 0666)
Durham Goode
transaction: add support for non-append files...
r20882 opener.chmod(self.backupjournal, createmode & 0666)
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Pierre-Yves David
transaction: add a file generation mechanism...
r22078 # hold file generations to be performed on commit
self._filegenerators = {}
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 def __del__(self):
mpm@selenic.com
transaction: __del__ should do nothing if the journal already exists...
r558 if self.journal:
Sune Foldager
transaction: always remove empty journal on abort...
r9693 self._abort()
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Henrik Stuart
transaction: ensure finished transactions are not reused...
r8289 @active
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363 def startgroup(self):
Durham Goode
transaction: add support for non-append files...
r20882 self._queue.append(([], []))
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363
@active
def endgroup(self):
q = self._queue.pop()
Durham Goode
transaction: add support for non-append files...
r20882 self.entries.extend(q[0])
self.backupentries.extend(q[1])
offsets = []
backups = []
Mads Kiilerich
cleanup: avoid _ for local unused tmp variables - that is reserved for i18n...
r22199 for f, o, _data in q[0]:
Durham Goode
transaction: add support for non-append files...
r20882 offsets.append((f, o))
Mads Kiilerich
cleanup: avoid _ for local unused tmp variables - that is reserved for i18n...
r22199 for f, b, _data in q[1]:
Durham Goode
transaction: add support for non-append files...
r20882 backups.append((f, b))
d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets])
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363 self.file.write(d)
self.file.flush()
Durham Goode
transaction: add support for non-append files...
r20882 d = ''.join(['%s\0%s\0' % (f, b) for f, b in backups])
self.backupsfile.write(d)
self.backupsfile.flush()
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363 @active
Chris Mason
...
r2084 def add(self, file, offset, data=None):
Durham Goode
transaction: add support for non-append files...
r20882 if file in self.map or file in self.backupmap:
Matt Mackall
many, many trivial check-code fixups
r10282 return
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363 if self._queue:
Durham Goode
transaction: add support for non-append files...
r20882 self._queue[-1][0].append((file, offset, data))
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363 return
Chris Mason
...
r2084 self.entries.append((file, offset, data))
self.map[file] = len(self.entries) - 1
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 # add enough data to the journal to do the truncate
self.file.write("%s\0%d\n" % (file, offset))
self.file.flush()
Henrik Stuart
transaction: ensure finished transactions are not reused...
r8289 @active
Durham Goode
transaction: add support for non-append files...
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
Pierre-Yves David
transaction: backup file in a dedicated "namespace"...
r22077 backupfile = "%s.backup.%s" % (self.journal, file)
Durham Goode
transaction: add support for non-append files...
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
Pierre-Yves David
transaction: add a file generation mechanism...
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
Chris Mason
...
r2084 def find(self, file):
if file in self.map:
return self.entries[self.map[file]]
Durham Goode
transaction: add support for non-append files...
r20882 if file in self.backupmap:
return self.backupentries[self.backupmap[file]]
Chris Mason
...
r2084 return None
Henrik Stuart
transaction: ensure finished transactions are not reused...
r8289 @active
Chris Mason
...
r2084 def replace(self, file, offset, data=None):
Henrik Stuart
transaction: add atomic groups to transaction logic...
r8363 '''
replace can only replace already committed entries
that are not pending in the queue
'''
Chris Mason
...
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()
Henrik Stuart
transaction: ensure finished transactions are not reused...
r8289 @active
mason@suse.com
Automatic nesting into running transactions in the same repository....
r1806 def nest(self):
self.count += 1
Ronny Pfannschmidt
make transactions work on non-refcounted python implementations
r11230 self.usages += 1
mason@suse.com
Automatic nesting into running transactions in the same repository....
r1806 return self
Ronny Pfannschmidt
make transactions work on non-refcounted python implementations
r11230 def release(self):
if self.count > 0:
self.usages -= 1
Patrick Mezard
cleanup: typos
r11685 # if the transaction scopes are left without being closed, fail
Ronny Pfannschmidt
make transactions work on non-refcounted python implementations
r11230 if self.count > 0 and self.usages == 0:
self._abort()
mason@suse.com
Automatic nesting into running transactions in the same repository....
r1806 def running(self):
return self.count > 0
Henrik Stuart
transaction: ensure finished transactions are not reused...
r8289 @active
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 def close(self):
Greg Ward
transaction: document close(), abort() methods
r9220 '''commit the transaction'''
Pierre-Yves David
transaction: add a file generation mechanism...
r22078 # write files registered for generation
for order, filenames, genfunc in sorted(self._filegenerators.values()):
files = []
try:
for name in filenames:
Pierre-Yves David
transaction: work around and document issue with file backup...
r22662 # Some files are already backed up when creating the
# localrepo. Until this is properly fixed we disable the
# backup for them.
if name not in ('phaseroots',):
self.addbackup(name)
Pierre-Yves David
transaction: add a file generation mechanism...
r22078 files.append(self.opener(name, 'w', atomictemp=True))
genfunc(*files)
finally:
for f in files:
f.close()
Durham Goode
transaction: add onclose/onabort hook for pre-close logic...
r20881 if self.count == 1 and self.onclose is not None:
self.onclose()
mason@suse.com
Automatic nesting into running transactions in the same repository....
r1806 self.count -= 1
if self.count != 0:
return
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 self.file.close()
Durham Goode
transaction: fix file descriptor leak for journal.backupfiles...
r21206 self.backupsfile.close()
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 self.entries = []
mpm@selenic.com
Beginnings of transaction undo support
r95 if self.after:
mpm@selenic.com
Fix undo after aborted commit bug...
r785 self.after()
FUJIWARA Katsunori
transaction: take journal file path relative to vfs to use file API via vfs
r20087 if self.opener.isfile(self.journal):
self.opener.unlink(self.journal)
Durham Goode
transaction: add support for non-append files...
r20882 if self.opener.isfile(self.backupjournal):
self.opener.unlink(self.backupjournal)
Mads Kiilerich
cleanup: name unused variables using convention of leading _...
r22204 for _f, b, _ignore in self.backupentries:
Durham Goode
transaction: add support for non-append files...
r20882 self.opener.unlink(b)
self.backupentries = []
mpm@selenic.com
transaction: nullify journal after close()...
r573 self.journal = None
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Henrik Stuart
transaction: ensure finished transactions are not reused...
r8289 @active
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0 def abort(self):
Greg Ward
transaction: document close(), abort() methods
r9220 '''abort the transaction (generally called on error, or when the
transaction is not explicitly committed before going out of
scope)'''
Henrik Stuart
transaction: ensure finished transactions are not reused...
r8289 self._abort()
def _abort(self):
Henrik Stuart
transaction: reset transaction on abort...
r8290 self.count = 0
Ronny Pfannschmidt
make transactions work on non-refcounted python implementations
r11230 self.usages = 0
Henrik Stuart
transaction: reset transaction on abort...
r8290 self.file.close()
Durham Goode
transaction: fix file descriptor leak for journal.backupfiles...
r21206 self.backupsfile.close()
Henrik Stuart
transaction: reset transaction on abort...
r8290
Durham Goode
transaction: add onclose/onabort hook for pre-close logic...
r20881 if self.onabort is not None:
self.onabort()
Benoit Boissinot
transaction: initialize self.journal to None after deletion...
r10228 try:
Durham Goode
transaction: add support for non-append files...
r20882 if not self.entries and not self.backupentries:
Benoit Boissinot
transaction: initialize self.journal to None after deletion...
r10228 if self.journal:
FUJIWARA Katsunori
transaction: take journal file path relative to vfs to use file API via vfs
r20087 self.opener.unlink(self.journal)
Durham Goode
transaction: add support for non-append files...
r20882 if self.backupjournal:
self.opener.unlink(self.backupjournal)
Benoit Boissinot
transaction: initialize self.journal to None after deletion...
r10228 return
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Benoit Boissinot
transaction: initialize self.journal to None after deletion...
r10228 self.report(_("transaction abort!\n"))
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
mpm@selenic.com
Warn if we fail to truncate something
r108 try:
Matt Mackall
many, many trivial check-code fixups
r10282 _playback(self.journal, self.report, self.opener,
Durham Goode
transaction: add support for non-append files...
r20882 self.entries, self.backupentries, False)
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294 self.report(_("rollback completed\n"))
Brodie Rao
cleanup: replace naked excepts with except Exception: ...
r16689 except Exception:
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294 self.report(_("rollback failed - please run hg recover\n"))
finally:
self.journal = None
Henrik Stuart
transaction: reset transaction on abort...
r8290
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294 def rollback(opener, file, report):
Durham Goode
transaction: add support for non-append files...
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.
"""
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294 entries = []
Durham Goode
transaction: add support for non-append files...
r20882 backupentries = []
Henrik Stuart
transaction: refactor transaction.abort and rollback to use the same code...
r8294
FUJIWARA Katsunori
transaction: take journal file path relative to vfs to use file API via vfs
r20087 fp = opener.open(file)
Dan Villiom Podlaski Christiansen
explicitly close files...
r13400 lines = fp.readlines()
fp.close()
for l in lines:
Matt Mackall
journal: report parsing errors on recover/rollback (issue4172)
r20524 try:
f, o = l.split('\0')
entries.append((f, int(o), None))
except ValueError:
report(_("couldn't read journal entry %r!\n") % l)
mpm@selenic.com
Add back links from file revisions to changeset revisions...
r0
Durham Goode
transaction: add support for non-append files...
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)