##// END OF EJS Templates
transaction: work around and document issue with file backup...
Pierre-Yves David -
r22662:c4d63f67 default
parent child Browse files
Show More
@@ -1,344 +1,348 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, util
16 import error, util
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, backupentries, unlink=True):
26 def _playback(journal, report, opener, entries, backupentries, 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
42
43 backupfiles = []
43 backupfiles = []
44 for f, b, _ignore in backupentries:
44 for f, b, _ignore in backupentries:
45 filepath = opener.join(f)
45 filepath = opener.join(f)
46 backuppath = opener.join(b)
46 backuppath = opener.join(b)
47 try:
47 try:
48 util.copyfile(backuppath, filepath)
48 util.copyfile(backuppath, filepath)
49 backupfiles.append(b)
49 backupfiles.append(b)
50 except IOError:
50 except IOError:
51 report(_("failed to recover %s\n") % f)
51 report(_("failed to recover %s\n") % f)
52 raise
52 raise
53
53
54 opener.unlink(journal)
54 opener.unlink(journal)
55 backuppath = "%s.backupfiles" % journal
55 backuppath = "%s.backupfiles" % journal
56 if opener.exists(backuppath):
56 if opener.exists(backuppath):
57 opener.unlink(backuppath)
57 opener.unlink(backuppath)
58 for f in backupfiles:
58 for f in backupfiles:
59 opener.unlink(f)
59 opener.unlink(f)
60
60
61 class transaction(object):
61 class transaction(object):
62 def __init__(self, report, opener, journal, after=None, createmode=None,
62 def __init__(self, report, opener, journal, after=None, createmode=None,
63 onclose=None, onabort=None):
63 onclose=None, onabort=None):
64 """Begin a new transaction
64 """Begin a new transaction
65
65
66 Begins a new transaction that allows rolling back writes in the event of
66 Begins a new transaction that allows rolling back writes in the event of
67 an exception.
67 an exception.
68
68
69 * `after`: called after the transaction has been committed
69 * `after`: called after the transaction has been committed
70 * `createmode`: the mode of the journal file that will be created
70 * `createmode`: the mode of the journal file that will be created
71 * `onclose`: called as the transaction is closing, but before it is
71 * `onclose`: called as the transaction is closing, but before it is
72 closed
72 closed
73 * `onabort`: called as the transaction is aborting, but before any files
73 * `onabort`: called as the transaction is aborting, but before any files
74 have been truncated
74 have been truncated
75 """
75 """
76 self.count = 1
76 self.count = 1
77 self.usages = 1
77 self.usages = 1
78 self.report = report
78 self.report = report
79 self.opener = opener
79 self.opener = opener
80 self.after = after
80 self.after = after
81 self.onclose = onclose
81 self.onclose = onclose
82 self.onabort = onabort
82 self.onabort = onabort
83 self.entries = []
83 self.entries = []
84 self.backupentries = []
84 self.backupentries = []
85 self.map = {}
85 self.map = {}
86 self.backupmap = {}
86 self.backupmap = {}
87 self.journal = journal
87 self.journal = journal
88 self._queue = []
88 self._queue = []
89 # a dict of arguments to be passed to hooks
89 # a dict of arguments to be passed to hooks
90 self.hookargs = {}
90 self.hookargs = {}
91
91
92 self.backupjournal = "%s.backupfiles" % journal
92 self.backupjournal = "%s.backupfiles" % journal
93 self.file = opener.open(self.journal, "w")
93 self.file = opener.open(self.journal, "w")
94 self.backupsfile = opener.open(self.backupjournal, 'w')
94 self.backupsfile = opener.open(self.backupjournal, 'w')
95 if createmode is not None:
95 if createmode is not None:
96 opener.chmod(self.journal, createmode & 0666)
96 opener.chmod(self.journal, createmode & 0666)
97 opener.chmod(self.backupjournal, createmode & 0666)
97 opener.chmod(self.backupjournal, createmode & 0666)
98
98
99 # hold file generations to be performed on commit
99 # hold file generations to be performed on commit
100 self._filegenerators = {}
100 self._filegenerators = {}
101
101
102 def __del__(self):
102 def __del__(self):
103 if self.journal:
103 if self.journal:
104 self._abort()
104 self._abort()
105
105
106 @active
106 @active
107 def startgroup(self):
107 def startgroup(self):
108 self._queue.append(([], []))
108 self._queue.append(([], []))
109
109
110 @active
110 @active
111 def endgroup(self):
111 def endgroup(self):
112 q = self._queue.pop()
112 q = self._queue.pop()
113 self.entries.extend(q[0])
113 self.entries.extend(q[0])
114 self.backupentries.extend(q[1])
114 self.backupentries.extend(q[1])
115
115
116 offsets = []
116 offsets = []
117 backups = []
117 backups = []
118 for f, o, _data in q[0]:
118 for f, o, _data in q[0]:
119 offsets.append((f, o))
119 offsets.append((f, o))
120
120
121 for f, b, _data in q[1]:
121 for f, b, _data in q[1]:
122 backups.append((f, b))
122 backups.append((f, b))
123
123
124 d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets])
124 d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets])
125 self.file.write(d)
125 self.file.write(d)
126 self.file.flush()
126 self.file.flush()
127
127
128 d = ''.join(['%s\0%s\0' % (f, b) for f, b in backups])
128 d = ''.join(['%s\0%s\0' % (f, b) for f, b in backups])
129 self.backupsfile.write(d)
129 self.backupsfile.write(d)
130 self.backupsfile.flush()
130 self.backupsfile.flush()
131
131
132 @active
132 @active
133 def add(self, file, offset, data=None):
133 def add(self, file, offset, data=None):
134 if file in self.map or file in self.backupmap:
134 if file in self.map or file in self.backupmap:
135 return
135 return
136 if self._queue:
136 if self._queue:
137 self._queue[-1][0].append((file, offset, data))
137 self._queue[-1][0].append((file, offset, data))
138 return
138 return
139
139
140 self.entries.append((file, offset, data))
140 self.entries.append((file, offset, data))
141 self.map[file] = len(self.entries) - 1
141 self.map[file] = len(self.entries) - 1
142 # add enough data to the journal to do the truncate
142 # add enough data to the journal to do the truncate
143 self.file.write("%s\0%d\n" % (file, offset))
143 self.file.write("%s\0%d\n" % (file, offset))
144 self.file.flush()
144 self.file.flush()
145
145
146 @active
146 @active
147 def addbackup(self, file, hardlink=True):
147 def addbackup(self, file, hardlink=True):
148 """Adds a backup of the file to the transaction
148 """Adds a backup of the file to the transaction
149
149
150 Calling addbackup() creates a hardlink backup of the specified file
150 Calling addbackup() creates a hardlink backup of the specified file
151 that is used to recover the file in the event of the transaction
151 that is used to recover the file in the event of the transaction
152 aborting.
152 aborting.
153
153
154 * `file`: the file path, relative to .hg/store
154 * `file`: the file path, relative to .hg/store
155 * `hardlink`: use a hardlink to quickly create the backup
155 * `hardlink`: use a hardlink to quickly create the backup
156 """
156 """
157
157
158 if file in self.map or file in self.backupmap:
158 if file in self.map or file in self.backupmap:
159 return
159 return
160 backupfile = "%s.backup.%s" % (self.journal, file)
160 backupfile = "%s.backup.%s" % (self.journal, file)
161 if self.opener.exists(file):
161 if self.opener.exists(file):
162 filepath = self.opener.join(file)
162 filepath = self.opener.join(file)
163 backuppath = self.opener.join(backupfile)
163 backuppath = self.opener.join(backupfile)
164 util.copyfiles(filepath, backuppath, hardlink=hardlink)
164 util.copyfiles(filepath, backuppath, hardlink=hardlink)
165 else:
165 else:
166 self.add(file, 0)
166 self.add(file, 0)
167 return
167 return
168
168
169 if self._queue:
169 if self._queue:
170 self._queue[-1][1].append((file, backupfile))
170 self._queue[-1][1].append((file, backupfile))
171 return
171 return
172
172
173 self.backupentries.append((file, backupfile, None))
173 self.backupentries.append((file, backupfile, None))
174 self.backupmap[file] = len(self.backupentries) - 1
174 self.backupmap[file] = len(self.backupentries) - 1
175 self.backupsfile.write("%s\0%s\0" % (file, backupfile))
175 self.backupsfile.write("%s\0%s\0" % (file, backupfile))
176 self.backupsfile.flush()
176 self.backupsfile.flush()
177
177
178 @active
178 @active
179 def addfilegenerator(self, genid, filenames, genfunc, order=0):
179 def addfilegenerator(self, genid, filenames, genfunc, order=0):
180 """add a function to generates some files at transaction commit
180 """add a function to generates some files at transaction commit
181
181
182 The `genfunc` argument is a function capable of generating proper
182 The `genfunc` argument is a function capable of generating proper
183 content of each entry in the `filename` tuple.
183 content of each entry in the `filename` tuple.
184
184
185 At transaction close time, `genfunc` will be called with one file
185 At transaction close time, `genfunc` will be called with one file
186 object argument per entries in `filenames`.
186 object argument per entries in `filenames`.
187
187
188 The transaction itself is responsible for the backup, creation and
188 The transaction itself is responsible for the backup, creation and
189 final write of such file.
189 final write of such file.
190
190
191 The `genid` argument is used to ensure the same set of file is only
191 The `genid` argument is used to ensure the same set of file is only
192 generated once. Call to `addfilegenerator` for a `genid` already
192 generated once. Call to `addfilegenerator` for a `genid` already
193 present will overwrite the old entry.
193 present will overwrite the old entry.
194
194
195 The `order` argument may be used to control the order in which multiple
195 The `order` argument may be used to control the order in which multiple
196 generator will be executed.
196 generator will be executed.
197 """
197 """
198 self._filegenerators[genid] = (order, filenames, genfunc)
198 self._filegenerators[genid] = (order, filenames, genfunc)
199
199
200 @active
200 @active
201 def find(self, file):
201 def find(self, file):
202 if file in self.map:
202 if file in self.map:
203 return self.entries[self.map[file]]
203 return self.entries[self.map[file]]
204 if file in self.backupmap:
204 if file in self.backupmap:
205 return self.backupentries[self.backupmap[file]]
205 return self.backupentries[self.backupmap[file]]
206 return None
206 return None
207
207
208 @active
208 @active
209 def replace(self, file, offset, data=None):
209 def replace(self, file, offset, data=None):
210 '''
210 '''
211 replace can only replace already committed entries
211 replace can only replace already committed entries
212 that are not pending in the queue
212 that are not pending in the queue
213 '''
213 '''
214
214
215 if file not in self.map:
215 if file not in self.map:
216 raise KeyError(file)
216 raise KeyError(file)
217 index = self.map[file]
217 index = self.map[file]
218 self.entries[index] = (file, offset, data)
218 self.entries[index] = (file, offset, data)
219 self.file.write("%s\0%d\n" % (file, offset))
219 self.file.write("%s\0%d\n" % (file, offset))
220 self.file.flush()
220 self.file.flush()
221
221
222 @active
222 @active
223 def nest(self):
223 def nest(self):
224 self.count += 1
224 self.count += 1
225 self.usages += 1
225 self.usages += 1
226 return self
226 return self
227
227
228 def release(self):
228 def release(self):
229 if self.count > 0:
229 if self.count > 0:
230 self.usages -= 1
230 self.usages -= 1
231 # if the transaction scopes are left without being closed, fail
231 # if the transaction scopes are left without being closed, fail
232 if self.count > 0 and self.usages == 0:
232 if self.count > 0 and self.usages == 0:
233 self._abort()
233 self._abort()
234
234
235 def running(self):
235 def running(self):
236 return self.count > 0
236 return self.count > 0
237
237
238 @active
238 @active
239 def close(self):
239 def close(self):
240 '''commit the transaction'''
240 '''commit the transaction'''
241 # write files registered for generation
241 # write files registered for generation
242 for order, filenames, genfunc in sorted(self._filegenerators.values()):
242 for order, filenames, genfunc in sorted(self._filegenerators.values()):
243 files = []
243 files = []
244 try:
244 try:
245 for name in filenames:
245 for name in filenames:
246 self.addbackup(name)
246 # Some files are already backed up when creating the
247 # localrepo. Until this is properly fixed we disable the
248 # backup for them.
249 if name not in ('phaseroots',):
250 self.addbackup(name)
247 files.append(self.opener(name, 'w', atomictemp=True))
251 files.append(self.opener(name, 'w', atomictemp=True))
248 genfunc(*files)
252 genfunc(*files)
249 finally:
253 finally:
250 for f in files:
254 for f in files:
251 f.close()
255 f.close()
252
256
253 if self.count == 1 and self.onclose is not None:
257 if self.count == 1 and self.onclose is not None:
254 self.onclose()
258 self.onclose()
255
259
256 self.count -= 1
260 self.count -= 1
257 if self.count != 0:
261 if self.count != 0:
258 return
262 return
259 self.file.close()
263 self.file.close()
260 self.backupsfile.close()
264 self.backupsfile.close()
261 self.entries = []
265 self.entries = []
262 if self.after:
266 if self.after:
263 self.after()
267 self.after()
264 if self.opener.isfile(self.journal):
268 if self.opener.isfile(self.journal):
265 self.opener.unlink(self.journal)
269 self.opener.unlink(self.journal)
266 if self.opener.isfile(self.backupjournal):
270 if self.opener.isfile(self.backupjournal):
267 self.opener.unlink(self.backupjournal)
271 self.opener.unlink(self.backupjournal)
268 for _f, b, _ignore in self.backupentries:
272 for _f, b, _ignore in self.backupentries:
269 self.opener.unlink(b)
273 self.opener.unlink(b)
270 self.backupentries = []
274 self.backupentries = []
271 self.journal = None
275 self.journal = None
272
276
273 @active
277 @active
274 def abort(self):
278 def abort(self):
275 '''abort the transaction (generally called on error, or when the
279 '''abort the transaction (generally called on error, or when the
276 transaction is not explicitly committed before going out of
280 transaction is not explicitly committed before going out of
277 scope)'''
281 scope)'''
278 self._abort()
282 self._abort()
279
283
280 def _abort(self):
284 def _abort(self):
281 self.count = 0
285 self.count = 0
282 self.usages = 0
286 self.usages = 0
283 self.file.close()
287 self.file.close()
284 self.backupsfile.close()
288 self.backupsfile.close()
285
289
286 if self.onabort is not None:
290 if self.onabort is not None:
287 self.onabort()
291 self.onabort()
288
292
289 try:
293 try:
290 if not self.entries and not self.backupentries:
294 if not self.entries and not self.backupentries:
291 if self.journal:
295 if self.journal:
292 self.opener.unlink(self.journal)
296 self.opener.unlink(self.journal)
293 if self.backupjournal:
297 if self.backupjournal:
294 self.opener.unlink(self.backupjournal)
298 self.opener.unlink(self.backupjournal)
295 return
299 return
296
300
297 self.report(_("transaction abort!\n"))
301 self.report(_("transaction abort!\n"))
298
302
299 try:
303 try:
300 _playback(self.journal, self.report, self.opener,
304 _playback(self.journal, self.report, self.opener,
301 self.entries, self.backupentries, False)
305 self.entries, self.backupentries, False)
302 self.report(_("rollback completed\n"))
306 self.report(_("rollback completed\n"))
303 except Exception:
307 except Exception:
304 self.report(_("rollback failed - please run hg recover\n"))
308 self.report(_("rollback failed - please run hg recover\n"))
305 finally:
309 finally:
306 self.journal = None
310 self.journal = None
307
311
308
312
309 def rollback(opener, file, report):
313 def rollback(opener, file, report):
310 """Rolls back the transaction contained in the given file
314 """Rolls back the transaction contained in the given file
311
315
312 Reads the entries in the specified file, and the corresponding
316 Reads the entries in the specified file, and the corresponding
313 '*.backupfiles' file, to recover from an incomplete transaction.
317 '*.backupfiles' file, to recover from an incomplete transaction.
314
318
315 * `file`: a file containing a list of entries, specifying where
319 * `file`: a file containing a list of entries, specifying where
316 to truncate each file. The file should contain a list of
320 to truncate each file. The file should contain a list of
317 file\0offset pairs, delimited by newlines. The corresponding
321 file\0offset pairs, delimited by newlines. The corresponding
318 '*.backupfiles' file should contain a list of file\0backupfile
322 '*.backupfiles' file should contain a list of file\0backupfile
319 pairs, delimited by \0.
323 pairs, delimited by \0.
320 """
324 """
321 entries = []
325 entries = []
322 backupentries = []
326 backupentries = []
323
327
324 fp = opener.open(file)
328 fp = opener.open(file)
325 lines = fp.readlines()
329 lines = fp.readlines()
326 fp.close()
330 fp.close()
327 for l in lines:
331 for l in lines:
328 try:
332 try:
329 f, o = l.split('\0')
333 f, o = l.split('\0')
330 entries.append((f, int(o), None))
334 entries.append((f, int(o), None))
331 except ValueError:
335 except ValueError:
332 report(_("couldn't read journal entry %r!\n") % l)
336 report(_("couldn't read journal entry %r!\n") % l)
333
337
334 backupjournal = "%s.backupfiles" % file
338 backupjournal = "%s.backupfiles" % file
335 if opener.exists(backupjournal):
339 if opener.exists(backupjournal):
336 fp = opener.open(backupjournal)
340 fp = opener.open(backupjournal)
337 data = fp.read()
341 data = fp.read()
338 if len(data) > 0:
342 if len(data) > 0:
339 parts = data.split('\0')
343 parts = data.split('\0')
340 for i in xrange(0, len(parts), 2):
344 for i in xrange(0, len(parts), 2):
341 f, b = parts[i:i + 1]
345 f, b = parts[i:i + 1]
342 backupentries.append((f, b, None))
346 backupentries.append((f, b, None))
343
347
344 _playback(file, report, opener, entries, backupentries)
348 _playback(file, report, opener, entries, backupentries)
General Comments 0
You need to be logged in to leave comments. Login now