##// END OF EJS Templates
repair: migrate revlogs during upgrade...
repair: migrate revlogs during upgrade Our next step for in-place upgrade is to migrate store data. Revlogs are the biggest source of data within the store and a store is useless without them, so we implement their migration first. Our strategy for migrating revlogs is to walk the store and call `revlog.clone()` on each revlog. There are some minor complications. Because revlogs have different storage options (e.g. changelog has generaldelta and delta chains disabled), we need to obtain the correct class of revlog so inserted data is encoded properly for its type. Various attempts at implementing progress indicators that didn't lead to frustration from false "it's almost done" indicators were made. I initially used a single progress bar based on number of revlogs. However, this quickly churned through all filelogs, got to 99% then effectively froze at 99.99% when it got to the manifest. So I converted the progress bar to total revision count. This was a little bit better. But the manifest was still significantly slower than filelogs and it took forever to process the last few percent. I then tried both revision/chunk bytes and raw bytes as the denominator. This had the opposite effect: because so much data is in manifests, it would churn through filelogs without showing much progress. When it got to manifests, it would fill in 90+% of the progress bar. I finally gave up having a unified progress bar and instead implemented 3 progress bars: 1 for filelog revisions, 1 for manifest revisions, and 1 for changelog revisions. I added extra messages indicating the total number of revisions of each so users know there are more progress bars coming. I also added extra messages before and after each stage to give extra details about what is happening. Strictly speaking, this isn't necessary. But the numbers are impressive. For example, when converting a non-generaldelta mozilla-central repository, the messages you see are: migrating 2475593 total revisions (1833043 in filelogs, 321156 in manifests, 321394 in changelog) migrating 1.67 GB in store; 2508 GB tracked data migrating 267868 filelogs containing 1833043 revisions (1.09 GB in store; 57.3 GB tracked data) finished migrating 1833043 filelog revisions across 267868 filelogs; change in size: -415776 bytes migrating 1 manifests containing 321156 revisions (518 MB in store; 2451 GB tracked data) That "2508 GB" figure really blew me away. I had no clue that the raw tracked data in mozilla-central was that large. Granted, 2451 GB is in the manifest and "only" 57.3 GB is in filelogs. But still. It's worth noting that gratuitous loading of source revlogs in order to display numbers and progress bars does serve a purpose: it ensures we can open all source revlogs. We don't want to spend several minutes copying revlogs only to encounter a permissions error or similar later. As part of this commit, we also add swapping of the store directory to the upgrade function. After revlogs are converted, we move the old store into the backup directory then move the temporary repo's store into the old store's location. On well-behaved systems, this should be 2 atomic operations and the window of inconsistency show be very narrow. There are still a few improvements to be made to store copying and upgrading. But this commit gets the bulk of the work out of the way.

File last commit:

r30349:95400242 default
r30779:38aa1ca9 default
Show More
config.py
173 lines | 6.1 KiB | text/x-python | PythonLexer
# config.py - configuration parsing for Mercurial
#
# Copyright 2009 Matt Mackall <mpm@selenic.com> and others
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
import errno
import os
from .i18n import _
from . import (
error,
util,
)
class config(object):
def __init__(self, data=None, includepaths=[]):
self._data = {}
self._source = {}
self._unset = []
self._includepaths = includepaths
if data:
for k in data._data:
self._data[k] = data[k].copy()
self._source = data._source.copy()
def copy(self):
return config(self)
def __contains__(self, section):
return section in self._data
def hasitem(self, section, item):
return item in self._data.get(section, {})
def __getitem__(self, section):
return self._data.get(section, {})
def __iter__(self):
for d in self.sections():
yield d
def update(self, src):
for s, n in src._unset:
if s in self and n in self._data[s]:
del self._data[s][n]
del self._source[(s, n)]
for s in src:
if s not in self:
self._data[s] = util.sortdict()
self._data[s].update(src._data[s])
self._source.update(src._source)
def get(self, section, item, default=None):
return self._data.get(section, {}).get(item, default)
def backup(self, section, item):
"""return a tuple allowing restore to reinstall a previous value
The main reason we need it is because it handles the "no data" case.
"""
try:
value = self._data[section][item]
source = self.source(section, item)
return (section, item, value, source)
except KeyError:
return (section, item)
def source(self, section, item):
return self._source.get((section, item), "")
def sections(self):
return sorted(self._data.keys())
def items(self, section):
return self._data.get(section, {}).items()
def set(self, section, item, value, source=""):
if section not in self:
self._data[section] = util.sortdict()
self._data[section][item] = value
if source:
self._source[(section, item)] = source
def restore(self, data):
"""restore data returned by self.backup"""
if len(data) == 4:
# restore old data
section, item, value, source = data
self._data[section][item] = value
self._source[(section, item)] = source
else:
# no data before, remove everything
section, item = data
if section in self._data:
self._data[section].pop(item, None)
self._source.pop((section, item), None)
def parse(self, src, data, sections=None, remap=None, include=None):
sectionre = util.re.compile(br'\[([^\[]+)\]')
itemre = util.re.compile(br'([^=\s][^=]*?)\s*=\s*(.*\S|)')
contre = util.re.compile(br'\s+(\S|\S.*\S)\s*$')
emptyre = util.re.compile(br'(;|#|\s*$)')
commentre = util.re.compile(br'(;|#)')
unsetre = util.re.compile(br'%unset\s+(\S+)')
includere = util.re.compile(br'%include\s+(\S|\S.*\S)\s*$')
section = ""
item = None
line = 0
cont = False
for l in data.splitlines(True):
line += 1
if line == 1 and l.startswith('\xef\xbb\xbf'):
# Someone set us up the BOM
l = l[3:]
if cont:
if commentre.match(l):
continue
m = contre.match(l)
if m:
if sections and section not in sections:
continue
v = self.get(section, item) + "\n" + m.group(1)
self.set(section, item, v, "%s:%d" % (src, line))
continue
item = None
cont = False
m = includere.match(l)
if m and include:
expanded = util.expandpath(m.group(1))
includepaths = [os.path.dirname(src)] + self._includepaths
for base in includepaths:
inc = os.path.normpath(os.path.join(base, expanded))
try:
include(inc, remap=remap, sections=sections)
break
except IOError as inst:
if inst.errno != errno.ENOENT:
raise error.ParseError(_("cannot include %s (%s)")
% (inc, inst.strerror),
"%s:%s" % (src, line))
continue
if emptyre.match(l):
continue
m = sectionre.match(l)
if m:
section = m.group(1)
if remap:
section = remap.get(section, section)
if section not in self:
self._data[section] = util.sortdict()
continue
m = itemre.match(l)
if m:
item = m.group(1)
cont = True
if sections and section not in sections:
continue
self.set(section, item, m.group(2), "%s:%d" % (src, line))
continue
m = unsetre.match(l)
if m:
name = m.group(1)
if sections and section not in sections:
continue
if self.get(section, name) is not None:
del self._data[section][name]
self._unset.append((section, name))
continue
raise error.ParseError(l.rstrip(), ("%s:%s" % (src, line)))
def read(self, path, fp=None, sections=None, remap=None):
if not fp:
fp = util.posixfile(path)
self.parse(path, fp.read(), sections, remap, self.read)