##// END OF EJS Templates
remotefilelog: fix opening validatecachelog in text mode
Inada Naoki -
r44560:25097b4d default
parent child Browse files
Show More
@@ -1,461 +1,461 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 import errno
3 import errno
4 import os
4 import os
5 import shutil
5 import shutil
6 import stat
6 import stat
7 import time
7 import time
8
8
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10 from mercurial.node import bin, hex
10 from mercurial.node import bin, hex
11 from mercurial.pycompat import open
11 from mercurial.pycompat import open
12 from mercurial import (
12 from mercurial import (
13 error,
13 error,
14 pycompat,
14 pycompat,
15 util,
15 util,
16 )
16 )
17 from mercurial.utils import hashutil
17 from mercurial.utils import hashutil
18 from . import (
18 from . import (
19 constants,
19 constants,
20 shallowutil,
20 shallowutil,
21 )
21 )
22
22
23
23
24 class basestore(object):
24 class basestore(object):
25 def __init__(self, repo, path, reponame, shared=False):
25 def __init__(self, repo, path, reponame, shared=False):
26 """Creates a remotefilelog store object for the given repo name.
26 """Creates a remotefilelog store object for the given repo name.
27
27
28 `path` - The file path where this store keeps its data
28 `path` - The file path where this store keeps its data
29 `reponame` - The name of the repo. This is used to partition data from
29 `reponame` - The name of the repo. This is used to partition data from
30 many repos.
30 many repos.
31 `shared` - True if this store is a shared cache of data from the central
31 `shared` - True if this store is a shared cache of data from the central
32 server, for many repos on this machine. False means this store is for
32 server, for many repos on this machine. False means this store is for
33 the local data for one repo.
33 the local data for one repo.
34 """
34 """
35 self.repo = repo
35 self.repo = repo
36 self.ui = repo.ui
36 self.ui = repo.ui
37 self._path = path
37 self._path = path
38 self._reponame = reponame
38 self._reponame = reponame
39 self._shared = shared
39 self._shared = shared
40 self._uid = os.getuid() if not pycompat.iswindows else None
40 self._uid = os.getuid() if not pycompat.iswindows else None
41
41
42 self._validatecachelog = self.ui.config(
42 self._validatecachelog = self.ui.config(
43 b"remotefilelog", b"validatecachelog"
43 b"remotefilelog", b"validatecachelog"
44 )
44 )
45 self._validatecache = self.ui.config(
45 self._validatecache = self.ui.config(
46 b"remotefilelog", b"validatecache", b'on'
46 b"remotefilelog", b"validatecache", b'on'
47 )
47 )
48 if self._validatecache not in (b'on', b'strict', b'off'):
48 if self._validatecache not in (b'on', b'strict', b'off'):
49 self._validatecache = b'on'
49 self._validatecache = b'on'
50 if self._validatecache == b'off':
50 if self._validatecache == b'off':
51 self._validatecache = False
51 self._validatecache = False
52
52
53 if shared:
53 if shared:
54 shallowutil.mkstickygroupdir(self.ui, path)
54 shallowutil.mkstickygroupdir(self.ui, path)
55
55
56 def getmissing(self, keys):
56 def getmissing(self, keys):
57 missing = []
57 missing = []
58 for name, node in keys:
58 for name, node in keys:
59 filepath = self._getfilepath(name, node)
59 filepath = self._getfilepath(name, node)
60 exists = os.path.exists(filepath)
60 exists = os.path.exists(filepath)
61 if (
61 if (
62 exists
62 exists
63 and self._validatecache == b'strict'
63 and self._validatecache == b'strict'
64 and not self._validatekey(filepath, b'contains')
64 and not self._validatekey(filepath, b'contains')
65 ):
65 ):
66 exists = False
66 exists = False
67 if not exists:
67 if not exists:
68 missing.append((name, node))
68 missing.append((name, node))
69
69
70 return missing
70 return missing
71
71
72 # BELOW THIS ARE IMPLEMENTATIONS OF REPACK SOURCE
72 # BELOW THIS ARE IMPLEMENTATIONS OF REPACK SOURCE
73
73
74 def markledger(self, ledger, options=None):
74 def markledger(self, ledger, options=None):
75 if options and options.get(constants.OPTION_PACKSONLY):
75 if options and options.get(constants.OPTION_PACKSONLY):
76 return
76 return
77 if self._shared:
77 if self._shared:
78 for filename, nodes in self._getfiles():
78 for filename, nodes in self._getfiles():
79 for node in nodes:
79 for node in nodes:
80 ledger.markdataentry(self, filename, node)
80 ledger.markdataentry(self, filename, node)
81 ledger.markhistoryentry(self, filename, node)
81 ledger.markhistoryentry(self, filename, node)
82
82
83 def cleanup(self, ledger):
83 def cleanup(self, ledger):
84 ui = self.ui
84 ui = self.ui
85 entries = ledger.sources.get(self, [])
85 entries = ledger.sources.get(self, [])
86 count = 0
86 count = 0
87 progress = ui.makeprogress(
87 progress = ui.makeprogress(
88 _(b"cleaning up"), unit=b"files", total=len(entries)
88 _(b"cleaning up"), unit=b"files", total=len(entries)
89 )
89 )
90 for entry in entries:
90 for entry in entries:
91 if entry.gced or (entry.datarepacked and entry.historyrepacked):
91 if entry.gced or (entry.datarepacked and entry.historyrepacked):
92 progress.update(count)
92 progress.update(count)
93 path = self._getfilepath(entry.filename, entry.node)
93 path = self._getfilepath(entry.filename, entry.node)
94 util.tryunlink(path)
94 util.tryunlink(path)
95 count += 1
95 count += 1
96 progress.complete()
96 progress.complete()
97
97
98 # Clean up the repo cache directory.
98 # Clean up the repo cache directory.
99 self._cleanupdirectory(self._getrepocachepath())
99 self._cleanupdirectory(self._getrepocachepath())
100
100
101 # BELOW THIS ARE NON-STANDARD APIS
101 # BELOW THIS ARE NON-STANDARD APIS
102
102
103 def _cleanupdirectory(self, rootdir):
103 def _cleanupdirectory(self, rootdir):
104 """Removes the empty directories and unnecessary files within the root
104 """Removes the empty directories and unnecessary files within the root
105 directory recursively. Note that this method does not remove the root
105 directory recursively. Note that this method does not remove the root
106 directory itself. """
106 directory itself. """
107
107
108 oldfiles = set()
108 oldfiles = set()
109 otherfiles = set()
109 otherfiles = set()
110 # osutil.listdir returns stat information which saves some rmdir/listdir
110 # osutil.listdir returns stat information which saves some rmdir/listdir
111 # syscalls.
111 # syscalls.
112 for name, mode in util.osutil.listdir(rootdir):
112 for name, mode in util.osutil.listdir(rootdir):
113 if stat.S_ISDIR(mode):
113 if stat.S_ISDIR(mode):
114 dirpath = os.path.join(rootdir, name)
114 dirpath = os.path.join(rootdir, name)
115 self._cleanupdirectory(dirpath)
115 self._cleanupdirectory(dirpath)
116
116
117 # Now that the directory specified by dirpath is potentially
117 # Now that the directory specified by dirpath is potentially
118 # empty, try and remove it.
118 # empty, try and remove it.
119 try:
119 try:
120 os.rmdir(dirpath)
120 os.rmdir(dirpath)
121 except OSError:
121 except OSError:
122 pass
122 pass
123
123
124 elif stat.S_ISREG(mode):
124 elif stat.S_ISREG(mode):
125 if name.endswith(b'_old'):
125 if name.endswith(b'_old'):
126 oldfiles.add(name[:-4])
126 oldfiles.add(name[:-4])
127 else:
127 else:
128 otherfiles.add(name)
128 otherfiles.add(name)
129
129
130 # Remove the files which end with suffix '_old' and have no
130 # Remove the files which end with suffix '_old' and have no
131 # corresponding file without the suffix '_old'. See addremotefilelognode
131 # corresponding file without the suffix '_old'. See addremotefilelognode
132 # method for the generation/purpose of files with '_old' suffix.
132 # method for the generation/purpose of files with '_old' suffix.
133 for filename in oldfiles - otherfiles:
133 for filename in oldfiles - otherfiles:
134 filepath = os.path.join(rootdir, filename + b'_old')
134 filepath = os.path.join(rootdir, filename + b'_old')
135 util.tryunlink(filepath)
135 util.tryunlink(filepath)
136
136
137 def _getfiles(self):
137 def _getfiles(self):
138 """Return a list of (filename, [node,...]) for all the revisions that
138 """Return a list of (filename, [node,...]) for all the revisions that
139 exist in the store.
139 exist in the store.
140
140
141 This is useful for obtaining a list of all the contents of the store
141 This is useful for obtaining a list of all the contents of the store
142 when performing a repack to another store, since the store API requires
142 when performing a repack to another store, since the store API requires
143 name+node keys and not namehash+node keys.
143 name+node keys and not namehash+node keys.
144 """
144 """
145 existing = {}
145 existing = {}
146 for filenamehash, node in self._listkeys():
146 for filenamehash, node in self._listkeys():
147 existing.setdefault(filenamehash, []).append(node)
147 existing.setdefault(filenamehash, []).append(node)
148
148
149 filenamemap = self._resolvefilenames(existing.keys())
149 filenamemap = self._resolvefilenames(existing.keys())
150
150
151 for filename, sha in pycompat.iteritems(filenamemap):
151 for filename, sha in pycompat.iteritems(filenamemap):
152 yield (filename, existing[sha])
152 yield (filename, existing[sha])
153
153
154 def _resolvefilenames(self, hashes):
154 def _resolvefilenames(self, hashes):
155 """Given a list of filename hashes that are present in the
155 """Given a list of filename hashes that are present in the
156 remotefilelog store, return a mapping from filename->hash.
156 remotefilelog store, return a mapping from filename->hash.
157
157
158 This is useful when converting remotefilelog blobs into other storage
158 This is useful when converting remotefilelog blobs into other storage
159 formats.
159 formats.
160 """
160 """
161 if not hashes:
161 if not hashes:
162 return {}
162 return {}
163
163
164 filenames = {}
164 filenames = {}
165 missingfilename = set(hashes)
165 missingfilename = set(hashes)
166
166
167 # Start with a full manifest, since it'll cover the majority of files
167 # Start with a full manifest, since it'll cover the majority of files
168 for filename in self.repo[b'tip'].manifest():
168 for filename in self.repo[b'tip'].manifest():
169 sha = hashutil.sha1(filename).digest()
169 sha = hashutil.sha1(filename).digest()
170 if sha in missingfilename:
170 if sha in missingfilename:
171 filenames[filename] = sha
171 filenames[filename] = sha
172 missingfilename.discard(sha)
172 missingfilename.discard(sha)
173
173
174 # Scan the changelog until we've found every file name
174 # Scan the changelog until we've found every file name
175 cl = self.repo.unfiltered().changelog
175 cl = self.repo.unfiltered().changelog
176 for rev in pycompat.xrange(len(cl) - 1, -1, -1):
176 for rev in pycompat.xrange(len(cl) - 1, -1, -1):
177 if not missingfilename:
177 if not missingfilename:
178 break
178 break
179 files = cl.readfiles(cl.node(rev))
179 files = cl.readfiles(cl.node(rev))
180 for filename in files:
180 for filename in files:
181 sha = hashutil.sha1(filename).digest()
181 sha = hashutil.sha1(filename).digest()
182 if sha in missingfilename:
182 if sha in missingfilename:
183 filenames[filename] = sha
183 filenames[filename] = sha
184 missingfilename.discard(sha)
184 missingfilename.discard(sha)
185
185
186 return filenames
186 return filenames
187
187
188 def _getrepocachepath(self):
188 def _getrepocachepath(self):
189 return (
189 return (
190 os.path.join(self._path, self._reponame)
190 os.path.join(self._path, self._reponame)
191 if self._shared
191 if self._shared
192 else self._path
192 else self._path
193 )
193 )
194
194
195 def _listkeys(self):
195 def _listkeys(self):
196 """List all the remotefilelog keys that exist in the store.
196 """List all the remotefilelog keys that exist in the store.
197
197
198 Returns a iterator of (filename hash, filecontent hash) tuples.
198 Returns a iterator of (filename hash, filecontent hash) tuples.
199 """
199 """
200
200
201 for root, dirs, files in os.walk(self._getrepocachepath()):
201 for root, dirs, files in os.walk(self._getrepocachepath()):
202 for filename in files:
202 for filename in files:
203 if len(filename) != 40:
203 if len(filename) != 40:
204 continue
204 continue
205 node = filename
205 node = filename
206 if self._shared:
206 if self._shared:
207 # .../1a/85ffda..be21
207 # .../1a/85ffda..be21
208 filenamehash = root[-41:-39] + root[-38:]
208 filenamehash = root[-41:-39] + root[-38:]
209 else:
209 else:
210 filenamehash = root[-40:]
210 filenamehash = root[-40:]
211 yield (bin(filenamehash), bin(node))
211 yield (bin(filenamehash), bin(node))
212
212
213 def _getfilepath(self, name, node):
213 def _getfilepath(self, name, node):
214 node = hex(node)
214 node = hex(node)
215 if self._shared:
215 if self._shared:
216 key = shallowutil.getcachekey(self._reponame, name, node)
216 key = shallowutil.getcachekey(self._reponame, name, node)
217 else:
217 else:
218 key = shallowutil.getlocalkey(name, node)
218 key = shallowutil.getlocalkey(name, node)
219
219
220 return os.path.join(self._path, key)
220 return os.path.join(self._path, key)
221
221
222 def _getdata(self, name, node):
222 def _getdata(self, name, node):
223 filepath = self._getfilepath(name, node)
223 filepath = self._getfilepath(name, node)
224 try:
224 try:
225 data = shallowutil.readfile(filepath)
225 data = shallowutil.readfile(filepath)
226 if self._validatecache and not self._validatedata(data, filepath):
226 if self._validatecache and not self._validatedata(data, filepath):
227 if self._validatecachelog:
227 if self._validatecachelog:
228 with open(self._validatecachelog, b'a+') as f:
228 with open(self._validatecachelog, b'ab+') as f:
229 f.write(b"corrupt %s during read\n" % filepath)
229 f.write(b"corrupt %s during read\n" % filepath)
230 os.rename(filepath, filepath + b".corrupt")
230 os.rename(filepath, filepath + b".corrupt")
231 raise KeyError(b"corrupt local cache file %s" % filepath)
231 raise KeyError(b"corrupt local cache file %s" % filepath)
232 except IOError:
232 except IOError:
233 raise KeyError(
233 raise KeyError(
234 b"no file found at %s for %s:%s" % (filepath, name, hex(node))
234 b"no file found at %s for %s:%s" % (filepath, name, hex(node))
235 )
235 )
236
236
237 return data
237 return data
238
238
239 def addremotefilelognode(self, name, node, data):
239 def addremotefilelognode(self, name, node, data):
240 filepath = self._getfilepath(name, node)
240 filepath = self._getfilepath(name, node)
241
241
242 oldumask = os.umask(0o002)
242 oldumask = os.umask(0o002)
243 try:
243 try:
244 # if this node already exists, save the old version for
244 # if this node already exists, save the old version for
245 # recovery/debugging purposes.
245 # recovery/debugging purposes.
246 if os.path.exists(filepath):
246 if os.path.exists(filepath):
247 newfilename = filepath + b'_old'
247 newfilename = filepath + b'_old'
248 # newfilename can be read-only and shutil.copy will fail.
248 # newfilename can be read-only and shutil.copy will fail.
249 # Delete newfilename to avoid it
249 # Delete newfilename to avoid it
250 if os.path.exists(newfilename):
250 if os.path.exists(newfilename):
251 shallowutil.unlinkfile(newfilename)
251 shallowutil.unlinkfile(newfilename)
252 shutil.copy(filepath, newfilename)
252 shutil.copy(filepath, newfilename)
253
253
254 shallowutil.mkstickygroupdir(self.ui, os.path.dirname(filepath))
254 shallowutil.mkstickygroupdir(self.ui, os.path.dirname(filepath))
255 shallowutil.writefile(filepath, data, readonly=True)
255 shallowutil.writefile(filepath, data, readonly=True)
256
256
257 if self._validatecache:
257 if self._validatecache:
258 if not self._validatekey(filepath, b'write'):
258 if not self._validatekey(filepath, b'write'):
259 raise error.Abort(
259 raise error.Abort(
260 _(b"local cache write was corrupted %s") % filepath
260 _(b"local cache write was corrupted %s") % filepath
261 )
261 )
262 finally:
262 finally:
263 os.umask(oldumask)
263 os.umask(oldumask)
264
264
265 def markrepo(self, path):
265 def markrepo(self, path):
266 """Call this to add the given repo path to the store's list of
266 """Call this to add the given repo path to the store's list of
267 repositories that are using it. This is useful later when doing garbage
267 repositories that are using it. This is useful later when doing garbage
268 collection, since it allows us to insecpt the repos to see what nodes
268 collection, since it allows us to insecpt the repos to see what nodes
269 they want to be kept alive in the store.
269 they want to be kept alive in the store.
270 """
270 """
271 repospath = os.path.join(self._path, b"repos")
271 repospath = os.path.join(self._path, b"repos")
272 with open(repospath, b'ab') as reposfile:
272 with open(repospath, b'ab') as reposfile:
273 reposfile.write(os.path.dirname(path) + b"\n")
273 reposfile.write(os.path.dirname(path) + b"\n")
274
274
275 repospathstat = os.stat(repospath)
275 repospathstat = os.stat(repospath)
276 if repospathstat.st_uid == self._uid:
276 if repospathstat.st_uid == self._uid:
277 os.chmod(repospath, 0o0664)
277 os.chmod(repospath, 0o0664)
278
278
279 def _validatekey(self, path, action):
279 def _validatekey(self, path, action):
280 with open(path, b'rb') as f:
280 with open(path, b'rb') as f:
281 data = f.read()
281 data = f.read()
282
282
283 if self._validatedata(data, path):
283 if self._validatedata(data, path):
284 return True
284 return True
285
285
286 if self._validatecachelog:
286 if self._validatecachelog:
287 with open(self._validatecachelog, b'ab+') as f:
287 with open(self._validatecachelog, b'ab+') as f:
288 f.write(b"corrupt %s during %s\n" % (path, action))
288 f.write(b"corrupt %s during %s\n" % (path, action))
289
289
290 os.rename(path, path + b".corrupt")
290 os.rename(path, path + b".corrupt")
291 return False
291 return False
292
292
293 def _validatedata(self, data, path):
293 def _validatedata(self, data, path):
294 try:
294 try:
295 if len(data) > 0:
295 if len(data) > 0:
296 # see remotefilelogserver.createfileblob for the format
296 # see remotefilelogserver.createfileblob for the format
297 offset, size, flags = shallowutil.parsesizeflags(data)
297 offset, size, flags = shallowutil.parsesizeflags(data)
298 if len(data) <= size:
298 if len(data) <= size:
299 # it is truncated
299 # it is truncated
300 return False
300 return False
301
301
302 # extract the node from the metadata
302 # extract the node from the metadata
303 offset += size
303 offset += size
304 datanode = data[offset : offset + 20]
304 datanode = data[offset : offset + 20]
305
305
306 # and compare against the path
306 # and compare against the path
307 if os.path.basename(path) == hex(datanode):
307 if os.path.basename(path) == hex(datanode):
308 # Content matches the intended path
308 # Content matches the intended path
309 return True
309 return True
310 return False
310 return False
311 except (ValueError, RuntimeError):
311 except (ValueError, RuntimeError):
312 pass
312 pass
313
313
314 return False
314 return False
315
315
316 def gc(self, keepkeys):
316 def gc(self, keepkeys):
317 ui = self.ui
317 ui = self.ui
318 cachepath = self._path
318 cachepath = self._path
319
319
320 # prune cache
320 # prune cache
321 queue = pycompat.queue.PriorityQueue()
321 queue = pycompat.queue.PriorityQueue()
322 originalsize = 0
322 originalsize = 0
323 size = 0
323 size = 0
324 count = 0
324 count = 0
325 removed = 0
325 removed = 0
326
326
327 # keep files newer than a day even if they aren't needed
327 # keep files newer than a day even if they aren't needed
328 limit = time.time() - (60 * 60 * 24)
328 limit = time.time() - (60 * 60 * 24)
329
329
330 progress = ui.makeprogress(
330 progress = ui.makeprogress(
331 _(b"removing unnecessary files"), unit=b"files"
331 _(b"removing unnecessary files"), unit=b"files"
332 )
332 )
333 progress.update(0)
333 progress.update(0)
334 for root, dirs, files in os.walk(cachepath):
334 for root, dirs, files in os.walk(cachepath):
335 for file in files:
335 for file in files:
336 if file == b'repos':
336 if file == b'repos':
337 continue
337 continue
338
338
339 # Don't delete pack files
339 # Don't delete pack files
340 if b'/packs/' in root:
340 if b'/packs/' in root:
341 continue
341 continue
342
342
343 progress.update(count)
343 progress.update(count)
344 path = os.path.join(root, file)
344 path = os.path.join(root, file)
345 key = os.path.relpath(path, cachepath)
345 key = os.path.relpath(path, cachepath)
346 count += 1
346 count += 1
347 try:
347 try:
348 pathstat = os.stat(path)
348 pathstat = os.stat(path)
349 except OSError as e:
349 except OSError as e:
350 # errno.ENOENT = no such file or directory
350 # errno.ENOENT = no such file or directory
351 if e.errno != errno.ENOENT:
351 if e.errno != errno.ENOENT:
352 raise
352 raise
353 msg = _(
353 msg = _(
354 b"warning: file %s was removed by another process\n"
354 b"warning: file %s was removed by another process\n"
355 )
355 )
356 ui.warn(msg % path)
356 ui.warn(msg % path)
357 continue
357 continue
358
358
359 originalsize += pathstat.st_size
359 originalsize += pathstat.st_size
360
360
361 if key in keepkeys or pathstat.st_atime > limit:
361 if key in keepkeys or pathstat.st_atime > limit:
362 queue.put((pathstat.st_atime, path, pathstat))
362 queue.put((pathstat.st_atime, path, pathstat))
363 size += pathstat.st_size
363 size += pathstat.st_size
364 else:
364 else:
365 try:
365 try:
366 shallowutil.unlinkfile(path)
366 shallowutil.unlinkfile(path)
367 except OSError as e:
367 except OSError as e:
368 # errno.ENOENT = no such file or directory
368 # errno.ENOENT = no such file or directory
369 if e.errno != errno.ENOENT:
369 if e.errno != errno.ENOENT:
370 raise
370 raise
371 msg = _(
371 msg = _(
372 b"warning: file %s was removed by another "
372 b"warning: file %s was removed by another "
373 b"process\n"
373 b"process\n"
374 )
374 )
375 ui.warn(msg % path)
375 ui.warn(msg % path)
376 continue
376 continue
377 removed += 1
377 removed += 1
378 progress.complete()
378 progress.complete()
379
379
380 # remove oldest files until under limit
380 # remove oldest files until under limit
381 limit = ui.configbytes(b"remotefilelog", b"cachelimit")
381 limit = ui.configbytes(b"remotefilelog", b"cachelimit")
382 if size > limit:
382 if size > limit:
383 excess = size - limit
383 excess = size - limit
384 progress = ui.makeprogress(
384 progress = ui.makeprogress(
385 _(b"enforcing cache limit"), unit=b"bytes", total=excess
385 _(b"enforcing cache limit"), unit=b"bytes", total=excess
386 )
386 )
387 removedexcess = 0
387 removedexcess = 0
388 while queue and size > limit and size > 0:
388 while queue and size > limit and size > 0:
389 progress.update(removedexcess)
389 progress.update(removedexcess)
390 atime, oldpath, oldpathstat = queue.get()
390 atime, oldpath, oldpathstat = queue.get()
391 try:
391 try:
392 shallowutil.unlinkfile(oldpath)
392 shallowutil.unlinkfile(oldpath)
393 except OSError as e:
393 except OSError as e:
394 # errno.ENOENT = no such file or directory
394 # errno.ENOENT = no such file or directory
395 if e.errno != errno.ENOENT:
395 if e.errno != errno.ENOENT:
396 raise
396 raise
397 msg = _(
397 msg = _(
398 b"warning: file %s was removed by another process\n"
398 b"warning: file %s was removed by another process\n"
399 )
399 )
400 ui.warn(msg % oldpath)
400 ui.warn(msg % oldpath)
401 size -= oldpathstat.st_size
401 size -= oldpathstat.st_size
402 removed += 1
402 removed += 1
403 removedexcess += oldpathstat.st_size
403 removedexcess += oldpathstat.st_size
404 progress.complete()
404 progress.complete()
405
405
406 ui.status(
406 ui.status(
407 _(b"finished: removed %d of %d files (%0.2f GB to %0.2f GB)\n")
407 _(b"finished: removed %d of %d files (%0.2f GB to %0.2f GB)\n")
408 % (
408 % (
409 removed,
409 removed,
410 count,
410 count,
411 float(originalsize) / 1024.0 / 1024.0 / 1024.0,
411 float(originalsize) / 1024.0 / 1024.0 / 1024.0,
412 float(size) / 1024.0 / 1024.0 / 1024.0,
412 float(size) / 1024.0 / 1024.0 / 1024.0,
413 )
413 )
414 )
414 )
415
415
416
416
417 class baseunionstore(object):
417 class baseunionstore(object):
418 def __init__(self, *args, **kwargs):
418 def __init__(self, *args, **kwargs):
419 # If one of the functions that iterates all of the stores is about to
419 # If one of the functions that iterates all of the stores is about to
420 # throw a KeyError, try this many times with a full refresh between
420 # throw a KeyError, try this many times with a full refresh between
421 # attempts. A repack operation may have moved data from one store to
421 # attempts. A repack operation may have moved data from one store to
422 # another while we were running.
422 # another while we were running.
423 self.numattempts = kwargs.get('numretries', 0) + 1
423 self.numattempts = kwargs.get('numretries', 0) + 1
424 # If not-None, call this function on every retry and if the attempts are
424 # If not-None, call this function on every retry and if the attempts are
425 # exhausted.
425 # exhausted.
426 self.retrylog = kwargs.get('retrylog', None)
426 self.retrylog = kwargs.get('retrylog', None)
427
427
428 def markforrefresh(self):
428 def markforrefresh(self):
429 for store in self.stores:
429 for store in self.stores:
430 if util.safehasattr(store, b'markforrefresh'):
430 if util.safehasattr(store, b'markforrefresh'):
431 store.markforrefresh()
431 store.markforrefresh()
432
432
433 @staticmethod
433 @staticmethod
434 def retriable(fn):
434 def retriable(fn):
435 def noop(*args):
435 def noop(*args):
436 pass
436 pass
437
437
438 def wrapped(self, *args, **kwargs):
438 def wrapped(self, *args, **kwargs):
439 retrylog = self.retrylog or noop
439 retrylog = self.retrylog or noop
440 funcname = fn.__name__
440 funcname = fn.__name__
441 i = 0
441 i = 0
442 while i < self.numattempts:
442 while i < self.numattempts:
443 if i > 0:
443 if i > 0:
444 retrylog(
444 retrylog(
445 b're-attempting (n=%d) %s\n'
445 b're-attempting (n=%d) %s\n'
446 % (i, pycompat.sysbytes(funcname))
446 % (i, pycompat.sysbytes(funcname))
447 )
447 )
448 self.markforrefresh()
448 self.markforrefresh()
449 i += 1
449 i += 1
450 try:
450 try:
451 return fn(self, *args, **kwargs)
451 return fn(self, *args, **kwargs)
452 except KeyError:
452 except KeyError:
453 if i == self.numattempts:
453 if i == self.numattempts:
454 # retries exhausted
454 # retries exhausted
455 retrylog(
455 retrylog(
456 b'retries exhausted in %s, raising KeyError\n'
456 b'retries exhausted in %s, raising KeyError\n'
457 % pycompat.sysbytes(funcname)
457 % pycompat.sysbytes(funcname)
458 )
458 )
459 raise
459 raise
460
460
461 return wrapped
461 return wrapped
General Comments 0
You need to be logged in to leave comments. Login now