##// END OF EJS Templates
transaction: add onclose/onabort hook for pre-close logic...
Durham Goode -
r20881:3c47677a default
parent child Browse files
Show More
@@ -1,183 +1,204 b''
1 # transaction.py - simple journaling scheme for mercurial
1 # transaction.py - simple journaling 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 or any later version.
12 # GNU General Public License version 2 or any later version.
13
13
14 from i18n import _
14 from i18n import _
15 import errno
15 import 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 fp = opener(f, 'a')
30 fp = opener(f, 'a')
31 fp.truncate(o)
31 fp.truncate(o)
32 fp.close()
32 fp.close()
33 except IOError:
33 except IOError:
34 report(_("failed to truncate %s\n") % f)
34 report(_("failed to truncate %s\n") % f)
35 raise
35 raise
36 else:
36 else:
37 try:
37 try:
38 opener.unlink(f)
38 opener.unlink(f)
39 except (IOError, OSError), inst:
39 except (IOError, OSError), inst:
40 if inst.errno != errno.ENOENT:
40 if inst.errno != errno.ENOENT:
41 raise
41 raise
42 opener.unlink(journal)
42 opener.unlink(journal)
43
43
44 class transaction(object):
44 class transaction(object):
45 def __init__(self, report, opener, journal, after=None, createmode=None):
45 def __init__(self, report, opener, journal, after=None, createmode=None,
46 onclose=None, onabort=None):
47 """Begin a new transaction
48
49 Begins a new transaction that allows rolling back writes in the event of
50 an exception.
51
52 * `after`: called after the transaction has been committed
53 * `createmode`: the mode of the journal file that will be created
54 * `onclose`: called as the transaction is closing, but before it is
55 closed
56 * `onabort`: called as the transaction is aborting, but before any files
57 have been truncated
58 """
46 self.count = 1
59 self.count = 1
47 self.usages = 1
60 self.usages = 1
48 self.report = report
61 self.report = report
49 self.opener = opener
62 self.opener = opener
50 self.after = after
63 self.after = after
64 self.onclose = onclose
65 self.onabort = onabort
51 self.entries = []
66 self.entries = []
52 self.map = {}
67 self.map = {}
53 self.journal = journal
68 self.journal = journal
54 self._queue = []
69 self._queue = []
55
70
56 self.file = opener.open(self.journal, "w")
71 self.file = opener.open(self.journal, "w")
57 if createmode is not None:
72 if createmode is not None:
58 opener.chmod(self.journal, createmode & 0666)
73 opener.chmod(self.journal, createmode & 0666)
59
74
60 def __del__(self):
75 def __del__(self):
61 if self.journal:
76 if self.journal:
62 self._abort()
77 self._abort()
63
78
64 @active
79 @active
65 def startgroup(self):
80 def startgroup(self):
66 self._queue.append([])
81 self._queue.append([])
67
82
68 @active
83 @active
69 def endgroup(self):
84 def endgroup(self):
70 q = self._queue.pop()
85 q = self._queue.pop()
71 d = ''.join(['%s\0%d\n' % (x[0], x[1]) for x in q])
86 d = ''.join(['%s\0%d\n' % (x[0], x[1]) for x in q])
72 self.entries.extend(q)
87 self.entries.extend(q)
73 self.file.write(d)
88 self.file.write(d)
74 self.file.flush()
89 self.file.flush()
75
90
76 @active
91 @active
77 def add(self, file, offset, data=None):
92 def add(self, file, offset, data=None):
78 if file in self.map:
93 if file in self.map:
79 return
94 return
80 if self._queue:
95 if self._queue:
81 self._queue[-1].append((file, offset, data))
96 self._queue[-1].append((file, offset, data))
82 return
97 return
83
98
84 self.entries.append((file, offset, data))
99 self.entries.append((file, offset, data))
85 self.map[file] = len(self.entries) - 1
100 self.map[file] = len(self.entries) - 1
86 # add enough data to the journal to do the truncate
101 # add enough data to the journal to do the truncate
87 self.file.write("%s\0%d\n" % (file, offset))
102 self.file.write("%s\0%d\n" % (file, offset))
88 self.file.flush()
103 self.file.flush()
89
104
90 @active
105 @active
91 def find(self, file):
106 def find(self, file):
92 if file in self.map:
107 if file in self.map:
93 return self.entries[self.map[file]]
108 return self.entries[self.map[file]]
94 return None
109 return None
95
110
96 @active
111 @active
97 def replace(self, file, offset, data=None):
112 def replace(self, file, offset, data=None):
98 '''
113 '''
99 replace can only replace already committed entries
114 replace can only replace already committed entries
100 that are not pending in the queue
115 that are not pending in the queue
101 '''
116 '''
102
117
103 if file not in self.map:
118 if file not in self.map:
104 raise KeyError(file)
119 raise KeyError(file)
105 index = self.map[file]
120 index = self.map[file]
106 self.entries[index] = (file, offset, data)
121 self.entries[index] = (file, offset, data)
107 self.file.write("%s\0%d\n" % (file, offset))
122 self.file.write("%s\0%d\n" % (file, offset))
108 self.file.flush()
123 self.file.flush()
109
124
110 @active
125 @active
111 def nest(self):
126 def nest(self):
112 self.count += 1
127 self.count += 1
113 self.usages += 1
128 self.usages += 1
114 return self
129 return self
115
130
116 def release(self):
131 def release(self):
117 if self.count > 0:
132 if self.count > 0:
118 self.usages -= 1
133 self.usages -= 1
119 # if the transaction scopes are left without being closed, fail
134 # if the transaction scopes are left without being closed, fail
120 if self.count > 0 and self.usages == 0:
135 if self.count > 0 and self.usages == 0:
121 self._abort()
136 self._abort()
122
137
123 def running(self):
138 def running(self):
124 return self.count > 0
139 return self.count > 0
125
140
126 @active
141 @active
127 def close(self):
142 def close(self):
128 '''commit the transaction'''
143 '''commit the transaction'''
144 if self.count == 1 and self.onclose is not None:
145 self.onclose()
146
129 self.count -= 1
147 self.count -= 1
130 if self.count != 0:
148 if self.count != 0:
131 return
149 return
132 self.file.close()
150 self.file.close()
133 self.entries = []
151 self.entries = []
134 if self.after:
152 if self.after:
135 self.after()
153 self.after()
136 if self.opener.isfile(self.journal):
154 if self.opener.isfile(self.journal):
137 self.opener.unlink(self.journal)
155 self.opener.unlink(self.journal)
138 self.journal = None
156 self.journal = None
139
157
140 @active
158 @active
141 def abort(self):
159 def abort(self):
142 '''abort the transaction (generally called on error, or when the
160 '''abort the transaction (generally called on error, or when the
143 transaction is not explicitly committed before going out of
161 transaction is not explicitly committed before going out of
144 scope)'''
162 scope)'''
145 self._abort()
163 self._abort()
146
164
147 def _abort(self):
165 def _abort(self):
148 self.count = 0
166 self.count = 0
149 self.usages = 0
167 self.usages = 0
150 self.file.close()
168 self.file.close()
151
169
170 if self.onabort is not None:
171 self.onabort()
172
152 try:
173 try:
153 if not self.entries:
174 if not self.entries:
154 if self.journal:
175 if self.journal:
155 self.opener.unlink(self.journal)
176 self.opener.unlink(self.journal)
156 return
177 return
157
178
158 self.report(_("transaction abort!\n"))
179 self.report(_("transaction abort!\n"))
159
180
160 try:
181 try:
161 _playback(self.journal, self.report, self.opener,
182 _playback(self.journal, self.report, self.opener,
162 self.entries, False)
183 self.entries, False)
163 self.report(_("rollback completed\n"))
184 self.report(_("rollback completed\n"))
164 except Exception:
185 except Exception:
165 self.report(_("rollback failed - please run hg recover\n"))
186 self.report(_("rollback failed - please run hg recover\n"))
166 finally:
187 finally:
167 self.journal = None
188 self.journal = None
168
189
169
190
170 def rollback(opener, file, report):
191 def rollback(opener, file, report):
171 entries = []
192 entries = []
172
193
173 fp = opener.open(file)
194 fp = opener.open(file)
174 lines = fp.readlines()
195 lines = fp.readlines()
175 fp.close()
196 fp.close()
176 for l in lines:
197 for l in lines:
177 try:
198 try:
178 f, o = l.split('\0')
199 f, o = l.split('\0')
179 entries.append((f, int(o), None))
200 entries.append((f, int(o), None))
180 except ValueError:
201 except ValueError:
181 report(_("couldn't read journal entry %r!\n") % l)
202 report(_("couldn't read journal entry %r!\n") % l)
182
203
183 _playback(file, report, opener, entries)
204 _playback(file, report, opener, entries)
General Comments 0
You need to be logged in to leave comments. Login now