PageRenderTime 43ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/transaction.py

https://bitbucket.org/mirror/mercurial/
Python | 307 lines | 269 code | 13 blank | 25 comment | 19 complexity | bd30cee75cf3155eac60e1ea2e71aeb2 MD5 | raw file
Possible License(s): GPL-2.0
  1. # transaction.py - simple journaling scheme for mercurial
  2. #
  3. # This transaction scheme is intended to gracefully handle program
  4. # errors and interruptions. More serious failures like system crashes
  5. # can be recovered with an fsck-like tool. As the whole repository is
  6. # effectively log-structured, this should amount to simply truncating
  7. # anything that isn't referenced in the changelog.
  8. #
  9. # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
  10. #
  11. # This software may be used and distributed according to the terms of the
  12. # GNU General Public License version 2 or any later version.
  13. from i18n import _
  14. import errno
  15. import error, util
  16. def active(func):
  17. def _active(self, *args, **kwds):
  18. if self.count == 0:
  19. raise error.Abort(_(
  20. 'cannot use transaction when it is already committed/aborted'))
  21. return func(self, *args, **kwds)
  22. return _active
  23. def _playback(journal, report, opener, entries, backupentries, unlink=True):
  24. for f, o, ignore in entries:
  25. if o or not unlink:
  26. try:
  27. fp = opener(f, 'a')
  28. fp.truncate(o)
  29. fp.close()
  30. except IOError:
  31. report(_("failed to truncate %s\n") % f)
  32. raise
  33. else:
  34. try:
  35. opener.unlink(f)
  36. except (IOError, OSError), inst:
  37. if inst.errno != errno.ENOENT:
  38. raise
  39. backupfiles = []
  40. for f, b, ignore in backupentries:
  41. filepath = opener.join(f)
  42. backuppath = opener.join(b)
  43. try:
  44. util.copyfile(backuppath, filepath)
  45. backupfiles.append(b)
  46. except IOError:
  47. report(_("failed to recover %s\n") % f)
  48. raise
  49. opener.unlink(journal)
  50. backuppath = "%s.backupfiles" % journal
  51. if opener.exists(backuppath):
  52. opener.unlink(backuppath)
  53. for f in backupfiles:
  54. opener.unlink(f)
  55. class transaction(object):
  56. def __init__(self, report, opener, journal, after=None, createmode=None,
  57. onclose=None, onabort=None):
  58. """Begin a new transaction
  59. Begins a new transaction that allows rolling back writes in the event of
  60. an exception.
  61. * `after`: called after the transaction has been committed
  62. * `createmode`: the mode of the journal file that will be created
  63. * `onclose`: called as the transaction is closing, but before it is
  64. closed
  65. * `onabort`: called as the transaction is aborting, but before any files
  66. have been truncated
  67. """
  68. self.count = 1
  69. self.usages = 1
  70. self.report = report
  71. self.opener = opener
  72. self.after = after
  73. self.onclose = onclose
  74. self.onabort = onabort
  75. self.entries = []
  76. self.backupentries = []
  77. self.map = {}
  78. self.backupmap = {}
  79. self.journal = journal
  80. self._queue = []
  81. # a dict of arguments to be passed to hooks
  82. self.hookargs = {}
  83. self.backupjournal = "%s.backupfiles" % journal
  84. self.file = opener.open(self.journal, "w")
  85. self.backupsfile = opener.open(self.backupjournal, 'w')
  86. if createmode is not None:
  87. opener.chmod(self.journal, createmode & 0666)
  88. opener.chmod(self.backupjournal, createmode & 0666)
  89. def __del__(self):
  90. if self.journal:
  91. self._abort()
  92. @active
  93. def startgroup(self):
  94. self._queue.append(([], []))
  95. @active
  96. def endgroup(self):
  97. q = self._queue.pop()
  98. self.entries.extend(q[0])
  99. self.backupentries.extend(q[1])
  100. offsets = []
  101. backups = []
  102. for f, o, _ in q[0]:
  103. offsets.append((f, o))
  104. for f, b, _ in q[1]:
  105. backups.append((f, b))
  106. d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets])
  107. self.file.write(d)
  108. self.file.flush()
  109. d = ''.join(['%s\0%s\0' % (f, b) for f, b in backups])
  110. self.backupsfile.write(d)
  111. self.backupsfile.flush()
  112. @active
  113. def add(self, file, offset, data=None):
  114. if file in self.map or file in self.backupmap:
  115. return
  116. if self._queue:
  117. self._queue[-1][0].append((file, offset, data))
  118. return
  119. self.entries.append((file, offset, data))
  120. self.map[file] = len(self.entries) - 1
  121. # add enough data to the journal to do the truncate
  122. self.file.write("%s\0%d\n" % (file, offset))
  123. self.file.flush()
  124. @active
  125. def addbackup(self, file, hardlink=True):
  126. """Adds a backup of the file to the transaction
  127. Calling addbackup() creates a hardlink backup of the specified file
  128. that is used to recover the file in the event of the transaction
  129. aborting.
  130. * `file`: the file path, relative to .hg/store
  131. * `hardlink`: use a hardlink to quickly create the backup
  132. """
  133. if file in self.map or file in self.backupmap:
  134. return
  135. backupfile = "journal.%s" % file
  136. if self.opener.exists(file):
  137. filepath = self.opener.join(file)
  138. backuppath = self.opener.join(backupfile)
  139. util.copyfiles(filepath, backuppath, hardlink=hardlink)
  140. else:
  141. self.add(file, 0)
  142. return
  143. if self._queue:
  144. self._queue[-1][1].append((file, backupfile))
  145. return
  146. self.backupentries.append((file, backupfile, None))
  147. self.backupmap[file] = len(self.backupentries) - 1
  148. self.backupsfile.write("%s\0%s\0" % (file, backupfile))
  149. self.backupsfile.flush()
  150. @active
  151. def find(self, file):
  152. if file in self.map:
  153. return self.entries[self.map[file]]
  154. if file in self.backupmap:
  155. return self.backupentries[self.backupmap[file]]
  156. return None
  157. @active
  158. def replace(self, file, offset, data=None):
  159. '''
  160. replace can only replace already committed entries
  161. that are not pending in the queue
  162. '''
  163. if file not in self.map:
  164. raise KeyError(file)
  165. index = self.map[file]
  166. self.entries[index] = (file, offset, data)
  167. self.file.write("%s\0%d\n" % (file, offset))
  168. self.file.flush()
  169. @active
  170. def nest(self):
  171. self.count += 1
  172. self.usages += 1
  173. return self
  174. def release(self):
  175. if self.count > 0:
  176. self.usages -= 1
  177. # if the transaction scopes are left without being closed, fail
  178. if self.count > 0 and self.usages == 0:
  179. self._abort()
  180. def running(self):
  181. return self.count > 0
  182. @active
  183. def close(self):
  184. '''commit the transaction'''
  185. if self.count == 1 and self.onclose is not None:
  186. self.onclose()
  187. self.count -= 1
  188. if self.count != 0:
  189. return
  190. self.file.close()
  191. self.backupsfile.close()
  192. self.entries = []
  193. if self.after:
  194. self.after()
  195. if self.opener.isfile(self.journal):
  196. self.opener.unlink(self.journal)
  197. if self.opener.isfile(self.backupjournal):
  198. self.opener.unlink(self.backupjournal)
  199. for f, b, _ in self.backupentries:
  200. self.opener.unlink(b)
  201. self.backupentries = []
  202. self.journal = None
  203. @active
  204. def abort(self):
  205. '''abort the transaction (generally called on error, or when the
  206. transaction is not explicitly committed before going out of
  207. scope)'''
  208. self._abort()
  209. def _abort(self):
  210. self.count = 0
  211. self.usages = 0
  212. self.file.close()
  213. self.backupsfile.close()
  214. if self.onabort is not None:
  215. self.onabort()
  216. try:
  217. if not self.entries and not self.backupentries:
  218. if self.journal:
  219. self.opener.unlink(self.journal)
  220. if self.backupjournal:
  221. self.opener.unlink(self.backupjournal)
  222. return
  223. self.report(_("transaction abort!\n"))
  224. try:
  225. _playback(self.journal, self.report, self.opener,
  226. self.entries, self.backupentries, False)
  227. self.report(_("rollback completed\n"))
  228. except Exception:
  229. self.report(_("rollback failed - please run hg recover\n"))
  230. finally:
  231. self.journal = None
  232. def rollback(opener, file, report):
  233. """Rolls back the transaction contained in the given file
  234. Reads the entries in the specified file, and the corresponding
  235. '*.backupfiles' file, to recover from an incomplete transaction.
  236. * `file`: a file containing a list of entries, specifying where
  237. to truncate each file. The file should contain a list of
  238. file\0offset pairs, delimited by newlines. The corresponding
  239. '*.backupfiles' file should contain a list of file\0backupfile
  240. pairs, delimited by \0.
  241. """
  242. entries = []
  243. backupentries = []
  244. fp = opener.open(file)
  245. lines = fp.readlines()
  246. fp.close()
  247. for l in lines:
  248. try:
  249. f, o = l.split('\0')
  250. entries.append((f, int(o), None))
  251. except ValueError:
  252. report(_("couldn't read journal entry %r!\n") % l)
  253. backupjournal = "%s.backupfiles" % file
  254. if opener.exists(backupjournal):
  255. fp = opener.open(backupjournal)
  256. data = fp.read()
  257. if len(data) > 0:
  258. parts = data.split('\0')
  259. for i in xrange(0, len(parts), 2):
  260. f, b = parts[i:i + 1]
  261. backupentries.append((f, b, None))
  262. _playback(file, report, opener, entries, backupentries)