##// END OF EJS Templates
transaction: always remove empty journal on abort...
Sune Foldager -
r9693:c40a1ee2 default
parent child Browse files
Show More
@@ -1,165 +1,167 b''
1 # transaction.py - simple journalling scheme for mercurial
1 # transaction.py - simple journalling scheme for mercurial
2 #
2 #
3 # This transaction scheme is intended to gracefully handle program
3 # This transaction scheme is intended to gracefully handle program
4 # errors and interruptions. More serious failures like system crashes
4 # errors and interruptions. More serious failures like system crashes
5 # can be recovered with an fsck-like tool. As the whole repository is
5 # can be recovered with an fsck-like tool. As the whole repository is
6 # effectively log-structured, this should amount to simply truncating
6 # effectively log-structured, this should amount to simply truncating
7 # anything that isn't referenced in the changelog.
7 # anything that isn't referenced in the changelog.
8 #
8 #
9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
10 #
10 #
11 # This software may be used and distributed according to the terms of the
11 # This software may be used and distributed according to the terms of the
12 # GNU General Public License version 2, incorporated herein by reference.
12 # GNU General Public License version 2, incorporated herein by reference.
13
13
14 from i18n import _
14 from i18n import _
15 import os, errno
15 import os, errno
16 import error
16 import error
17
17
18 def active(func):
18 def active(func):
19 def _active(self, *args, **kwds):
19 def _active(self, *args, **kwds):
20 if self.count == 0:
20 if self.count == 0:
21 raise error.Abort(_(
21 raise error.Abort(_(
22 'cannot use transaction when it is already committed/aborted'))
22 'cannot use transaction when it is already committed/aborted'))
23 return func(self, *args, **kwds)
23 return func(self, *args, **kwds)
24 return _active
24 return _active
25
25
26 def _playback(journal, report, opener, entries, unlink=True):
26 def _playback(journal, report, opener, entries, unlink=True):
27 for f, o, ignore in entries:
27 for f, o, ignore in entries:
28 if o or not unlink:
28 if o or not unlink:
29 try:
29 try:
30 opener(f, 'a').truncate(o)
30 opener(f, 'a').truncate(o)
31 except IOError:
31 except IOError:
32 report(_("failed to truncate %s\n") % f)
32 report(_("failed to truncate %s\n") % f)
33 raise
33 raise
34 else:
34 else:
35 try:
35 try:
36 fn = opener(f).name
36 fn = opener(f).name
37 os.unlink(fn)
37 os.unlink(fn)
38 except (IOError, OSError), inst:
38 except (IOError, OSError), inst:
39 if inst.errno != errno.ENOENT:
39 if inst.errno != errno.ENOENT:
40 raise
40 raise
41 os.unlink(journal)
41 os.unlink(journal)
42
42
43 class transaction(object):
43 class transaction(object):
44 def __init__(self, report, opener, journal, after=None, createmode=None):
44 def __init__(self, report, opener, journal, after=None, createmode=None):
45 self.journal = None
45 self.journal = None
46
46
47 self.count = 1
47 self.count = 1
48 self.report = report
48 self.report = report
49 self.opener = opener
49 self.opener = opener
50 self.after = after
50 self.after = after
51 self.entries = []
51 self.entries = []
52 self.map = {}
52 self.map = {}
53 self.journal = journal
53 self.journal = journal
54 self._queue = []
54 self._queue = []
55
55
56 self.file = open(self.journal, "w")
56 self.file = open(self.journal, "w")
57 if createmode is not None:
57 if createmode is not None:
58 os.chmod(self.journal, createmode & 0666)
58 os.chmod(self.journal, createmode & 0666)
59
59
60 def __del__(self):
60 def __del__(self):
61 if self.journal:
61 if self.journal:
62 if self.entries: self._abort()
62 self._abort()
63 self.file.close()
64
63
65 @active
64 @active
66 def startgroup(self):
65 def startgroup(self):
67 self._queue.append([])
66 self._queue.append([])
68
67
69 @active
68 @active
70 def endgroup(self):
69 def endgroup(self):
71 q = self._queue.pop()
70 q = self._queue.pop()
72 d = ''.join(['%s\0%d\n' % (x[0], x[1]) for x in q])
71 d = ''.join(['%s\0%d\n' % (x[0], x[1]) for x in q])
73 self.entries.extend(q)
72 self.entries.extend(q)
74 self.file.write(d)
73 self.file.write(d)
75 self.file.flush()
74 self.file.flush()
76
75
77 @active
76 @active
78 def add(self, file, offset, data=None):
77 def add(self, file, offset, data=None):
79 if file in self.map: return
78 if file in self.map: return
80
79
81 if self._queue:
80 if self._queue:
82 self._queue[-1].append((file, offset, data))
81 self._queue[-1].append((file, offset, data))
83 return
82 return
84
83
85 self.entries.append((file, offset, data))
84 self.entries.append((file, offset, data))
86 self.map[file] = len(self.entries) - 1
85 self.map[file] = len(self.entries) - 1
87 # add enough data to the journal to do the truncate
86 # add enough data to the journal to do the truncate
88 self.file.write("%s\0%d\n" % (file, offset))
87 self.file.write("%s\0%d\n" % (file, offset))
89 self.file.flush()
88 self.file.flush()
90
89
91 @active
90 @active
92 def find(self, file):
91 def find(self, file):
93 if file in self.map:
92 if file in self.map:
94 return self.entries[self.map[file]]
93 return self.entries[self.map[file]]
95 return None
94 return None
96
95
97 @active
96 @active
98 def replace(self, file, offset, data=None):
97 def replace(self, file, offset, data=None):
99 '''
98 '''
100 replace can only replace already committed entries
99 replace can only replace already committed entries
101 that are not pending in the queue
100 that are not pending in the queue
102 '''
101 '''
103
102
104 if file not in self.map:
103 if file not in self.map:
105 raise KeyError(file)
104 raise KeyError(file)
106 index = self.map[file]
105 index = self.map[file]
107 self.entries[index] = (file, offset, data)
106 self.entries[index] = (file, offset, data)
108 self.file.write("%s\0%d\n" % (file, offset))
107 self.file.write("%s\0%d\n" % (file, offset))
109 self.file.flush()
108 self.file.flush()
110
109
111 @active
110 @active
112 def nest(self):
111 def nest(self):
113 self.count += 1
112 self.count += 1
114 return self
113 return self
115
114
116 def running(self):
115 def running(self):
117 return self.count > 0
116 return self.count > 0
118
117
119 @active
118 @active
120 def close(self):
119 def close(self):
121 '''commit the transaction'''
120 '''commit the transaction'''
122 self.count -= 1
121 self.count -= 1
123 if self.count != 0:
122 if self.count != 0:
124 return
123 return
125 self.file.close()
124 self.file.close()
126 self.entries = []
125 self.entries = []
127 if self.after:
126 if self.after:
128 self.after()
127 self.after()
129 else:
128 if os.path.isfile(self.journal):
130 os.unlink(self.journal)
129 os.unlink(self.journal)
131 self.journal = None
130 self.journal = None
132
131
133 @active
132 @active
134 def abort(self):
133 def abort(self):
135 '''abort the transaction (generally called on error, or when the
134 '''abort the transaction (generally called on error, or when the
136 transaction is not explicitly committed before going out of
135 transaction is not explicitly committed before going out of
137 scope)'''
136 scope)'''
138 self._abort()
137 self._abort()
139
138
140 def _abort(self):
139 def _abort(self):
141 self.count = 0
140 self.count = 0
142 self.file.close()
141 self.file.close()
143
142
144 if not self.entries: return
143 if not self.entries:
144 if self.journal:
145 os.unlink(self.journal)
146 return
145
147
146 self.report(_("transaction abort!\n"))
148 self.report(_("transaction abort!\n"))
147
149
148 try:
150 try:
149 try:
151 try:
150 _playback(self.journal, self.report, self.opener, self.entries, False)
152 _playback(self.journal, self.report, self.opener, self.entries, False)
151 self.report(_("rollback completed\n"))
153 self.report(_("rollback completed\n"))
152 except:
154 except:
153 self.report(_("rollback failed - please run hg recover\n"))
155 self.report(_("rollback failed - please run hg recover\n"))
154 finally:
156 finally:
155 self.journal = None
157 self.journal = None
156
158
157
159
158 def rollback(opener, file, report):
160 def rollback(opener, file, report):
159 entries = []
161 entries = []
160
162
161 for l in open(file).readlines():
163 for l in open(file).readlines():
162 f, o = l.split('\0')
164 f, o = l.split('\0')
163 entries.append((f, int(o), None))
165 entries.append((f, int(o), None))
164
166
165 _playback(file, report, opener, entries)
167 _playback(file, report, opener, entries)
@@ -1,12 +1,20 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 hg init
3 hg init
4 echo a > a
4 echo a > a
5 hg ci -Am0
5 hg ci -Am0
6 hg -q clone . foo
6
7
7 touch .hg/store/journal
8 touch .hg/store/journal
8
9
9 echo foo > a
10 echo foo > a
10 hg ci -Am0
11 hg ci -Am0
11
12
12 hg recover
13 hg recover
14
15 echo % check that zero-size journals are correctly aborted
16 hg bundle -qa repo.hg
17 chmod -w foo/.hg/store/00changelog.i
18 hg -R foo unbundle repo.hg 2>&1 | sed 's/\(abort: Permission denied\).*/\1/'
19 if test -f foo/.hg/store/journal; then echo 'journal exists :-('; fi
20 exit 0
@@ -1,8 +1,11 b''
1 adding a
1 adding a
2 abort: abandoned transaction found - run hg recover!
2 abort: abandoned transaction found - run hg recover!
3 rolling back interrupted transaction
3 rolling back interrupted transaction
4 checking changesets
4 checking changesets
5 checking manifests
5 checking manifests
6 crosschecking files in changesets and manifests
6 crosschecking files in changesets and manifests
7 checking files
7 checking files
8 1 files, 1 changesets, 1 total revisions
8 1 files, 1 changesets, 1 total revisions
9 % check that zero-size journals are correctly aborted
10 adding changesets
11 abort: Permission denied
General Comments 0
You need to be logged in to leave comments. Login now