Mercurial > hg
comparison mercurial/repair.py @ 30779:38aa1ca97b6a
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.
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Sun, 18 Dec 2016 17:00:15 -0800 |
parents | 7de7afd8bdd9 |
children | 2603d04889e1 |
comparison
equal
deleted
inserted
replaced
30778:1c7368d1a25f | 30779:38aa1ca97b6a |
---|---|
9 from __future__ import absolute_import | 9 from __future__ import absolute_import |
10 | 10 |
11 import errno | 11 import errno |
12 import hashlib | 12 import hashlib |
13 import tempfile | 13 import tempfile |
14 import time | |
14 | 15 |
15 from .i18n import _ | 16 from .i18n import _ |
16 from .node import short | 17 from .node import short |
17 from . import ( | 18 from . import ( |
18 bundle2, | 19 bundle2, |
19 changegroup, | 20 changegroup, |
21 changelog, | |
20 error, | 22 error, |
21 exchange, | 23 exchange, |
24 manifest, | |
22 obsolete, | 25 obsolete, |
26 revlog, | |
23 scmutil, | 27 scmutil, |
24 util, | 28 util, |
25 ) | 29 ) |
26 | 30 |
27 def _bundle(repo, bases, heads, node, suffix, compress=True): | 31 def _bundle(repo, bases, heads, node, suffix, compress=True): |
637 # FUTURE consider adding some optimizations here for certain transitions. | 641 # FUTURE consider adding some optimizations here for certain transitions. |
638 # e.g. adding generaldelta could schedule parent redeltas. | 642 # e.g. adding generaldelta could schedule parent redeltas. |
639 | 643 |
640 return newactions | 644 return newactions |
641 | 645 |
646 def _revlogfrompath(repo, path): | |
647 """Obtain a revlog from a repo path. | |
648 | |
649 An instance of the appropriate class is returned. | |
650 """ | |
651 if path == '00changelog.i': | |
652 return changelog.changelog(repo.svfs) | |
653 elif path.endswith('00manifest.i'): | |
654 mandir = path[:-len('00manifest.i')] | |
655 return manifest.manifestrevlog(repo.svfs, dir=mandir) | |
656 else: | |
657 # Filelogs don't do anything special with settings. So we can use a | |
658 # vanilla revlog. | |
659 return revlog.revlog(repo.svfs, path) | |
660 | |
661 def _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse, aggressivemergedeltas): | |
662 """Copy revlogs between 2 repos.""" | |
663 revcount = 0 | |
664 srcsize = 0 | |
665 srcrawsize = 0 | |
666 dstsize = 0 | |
667 fcount = 0 | |
668 frevcount = 0 | |
669 fsrcsize = 0 | |
670 frawsize = 0 | |
671 fdstsize = 0 | |
672 mcount = 0 | |
673 mrevcount = 0 | |
674 msrcsize = 0 | |
675 mrawsize = 0 | |
676 mdstsize = 0 | |
677 crevcount = 0 | |
678 csrcsize = 0 | |
679 crawsize = 0 | |
680 cdstsize = 0 | |
681 | |
682 # Perform a pass to collect metadata. This validates we can open all | |
683 # source files and allows a unified progress bar to be displayed. | |
684 for unencoded, encoded, size in srcrepo.store.walk(): | |
685 if unencoded.endswith('.d'): | |
686 continue | |
687 | |
688 rl = _revlogfrompath(srcrepo, unencoded) | |
689 revcount += len(rl) | |
690 | |
691 datasize = 0 | |
692 rawsize = 0 | |
693 idx = rl.index | |
694 for rev in rl: | |
695 e = idx[rev] | |
696 datasize += e[1] | |
697 rawsize += e[2] | |
698 | |
699 srcsize += datasize | |
700 srcrawsize += rawsize | |
701 | |
702 # This is for the separate progress bars. | |
703 if isinstance(rl, changelog.changelog): | |
704 crevcount += len(rl) | |
705 csrcsize += datasize | |
706 crawsize += rawsize | |
707 elif isinstance(rl, manifest.manifestrevlog): | |
708 mcount += 1 | |
709 mrevcount += len(rl) | |
710 msrcsize += datasize | |
711 mrawsize += rawsize | |
712 elif isinstance(rl, revlog.revlog): | |
713 fcount += 1 | |
714 frevcount += len(rl) | |
715 fsrcsize += datasize | |
716 frawsize += rawsize | |
717 | |
718 if not revcount: | |
719 return | |
720 | |
721 ui.write(_('migrating %d total revisions (%d in filelogs, %d in manifests, ' | |
722 '%d in changelog)\n') % | |
723 (revcount, frevcount, mrevcount, crevcount)) | |
724 ui.write(_('migrating %s in store; %s tracked data\n') % ( | |
725 (util.bytecount(srcsize), util.bytecount(srcrawsize)))) | |
726 | |
727 # Used to keep track of progress. | |
728 progress = [] | |
729 def oncopiedrevision(rl, rev, node): | |
730 progress[1] += 1 | |
731 srcrepo.ui.progress(progress[0], progress[1], total=progress[2]) | |
732 | |
733 # Do the actual copying. | |
734 # FUTURE this operation can be farmed off to worker processes. | |
735 seen = set() | |
736 for unencoded, encoded, size in srcrepo.store.walk(): | |
737 if unencoded.endswith('.d'): | |
738 continue | |
739 | |
740 oldrl = _revlogfrompath(srcrepo, unencoded) | |
741 newrl = _revlogfrompath(dstrepo, unencoded) | |
742 | |
743 if isinstance(oldrl, changelog.changelog) and 'c' not in seen: | |
744 ui.write(_('finished migrating %d manifest revisions across %d ' | |
745 'manifests; change in size: %s\n') % | |
746 (mrevcount, mcount, util.bytecount(mdstsize - msrcsize))) | |
747 | |
748 ui.write(_('migrating changelog containing %d revisions ' | |
749 '(%s in store; %s tracked data)\n') % | |
750 (crevcount, util.bytecount(csrcsize), | |
751 util.bytecount(crawsize))) | |
752 seen.add('c') | |
753 progress[:] = [_('changelog revisions'), 0, crevcount] | |
754 elif isinstance(oldrl, manifest.manifestrevlog) and 'm' not in seen: | |
755 ui.write(_('finished migrating %d filelog revisions across %d ' | |
756 'filelogs; change in size: %s\n') % | |
757 (frevcount, fcount, util.bytecount(fdstsize - fsrcsize))) | |
758 | |
759 ui.write(_('migrating %d manifests containing %d revisions ' | |
760 '(%s in store; %s tracked data)\n') % | |
761 (mcount, mrevcount, util.bytecount(msrcsize), | |
762 util.bytecount(mrawsize))) | |
763 seen.add('m') | |
764 progress[:] = [_('manifest revisions'), 0, mrevcount] | |
765 elif 'f' not in seen: | |
766 ui.write(_('migrating %d filelogs containing %d revisions ' | |
767 '(%s in store; %s tracked data)\n') % | |
768 (fcount, frevcount, util.bytecount(fsrcsize), | |
769 util.bytecount(frawsize))) | |
770 seen.add('f') | |
771 progress[:] = [_('file revisions'), 0, frevcount] | |
772 | |
773 ui.progress(progress[0], progress[1], total=progress[2]) | |
774 | |
775 ui.note(_('cloning %d revisions from %s\n') % (len(oldrl), unencoded)) | |
776 oldrl.clone(tr, newrl, addrevisioncb=oncopiedrevision, | |
777 deltareuse=deltareuse, | |
778 aggressivemergedeltas=aggressivemergedeltas) | |
779 | |
780 datasize = 0 | |
781 idx = newrl.index | |
782 for rev in newrl: | |
783 datasize += idx[rev][1] | |
784 | |
785 dstsize += datasize | |
786 | |
787 if isinstance(newrl, changelog.changelog): | |
788 cdstsize += datasize | |
789 elif isinstance(newrl, manifest.manifestrevlog): | |
790 mdstsize += datasize | |
791 else: | |
792 fdstsize += datasize | |
793 | |
794 ui.progress(progress[0], None) | |
795 | |
796 ui.write(_('finished migrating %d changelog revisions; change in size: ' | |
797 '%s\n') % (crevcount, util.bytecount(cdstsize - csrcsize))) | |
798 | |
799 ui.write(_('finished migrating %d total revisions; total change in store ' | |
800 'size: %s\n') % (revcount, util.bytecount(dstsize - srcsize))) | |
801 | |
642 def _upgraderepo(ui, srcrepo, dstrepo, requirements, actions): | 802 def _upgraderepo(ui, srcrepo, dstrepo, requirements, actions): |
643 """Do the low-level work of upgrading a repository. | 803 """Do the low-level work of upgrading a repository. |
644 | 804 |
645 The upgrade is effectively performed as a copy between a source | 805 The upgrade is effectively performed as a copy between a source |
646 repository and a temporary destination repository. | 806 repository and a temporary destination repository. |
650 readers and without corrupting the source repository. | 810 readers and without corrupting the source repository. |
651 """ | 811 """ |
652 assert srcrepo.currentwlock() | 812 assert srcrepo.currentwlock() |
653 assert dstrepo.currentwlock() | 813 assert dstrepo.currentwlock() |
654 | 814 |
655 # TODO copy store | 815 ui.write(_('(it is safe to interrupt this process any time before ' |
816 'data migration completes)\n')) | |
817 | |
818 if 'redeltaall' in actions: | |
819 deltareuse = revlog.revlog.DELTAREUSENEVER | |
820 elif 'redeltaparent' in actions: | |
821 deltareuse = revlog.revlog.DELTAREUSESAMEREVS | |
822 elif 'redeltamultibase' in actions: | |
823 deltareuse = revlog.revlog.DELTAREUSESAMEREVS | |
824 else: | |
825 deltareuse = revlog.revlog.DELTAREUSEALWAYS | |
826 | |
827 with dstrepo.transaction('upgrade') as tr: | |
828 _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse, | |
829 'redeltamultibase' in actions) | |
830 | |
831 # TODO copy non-revlog store files | |
832 | |
833 ui.write(_('data fully migrated to temporary repository\n')) | |
656 | 834 |
657 backuppath = tempfile.mkdtemp(prefix='upgradebackup.', dir=srcrepo.path) | 835 backuppath = tempfile.mkdtemp(prefix='upgradebackup.', dir=srcrepo.path) |
658 backupvfs = scmutil.vfs(backuppath) | 836 backupvfs = scmutil.vfs(backuppath) |
659 | 837 |
660 # Make a backup of requires file first, as it is the first to be modified. | 838 # Make a backup of requires file first, as it is the first to be modified. |
671 | 849 |
672 ui.write(_('starting in-place swap of repository data\n')) | 850 ui.write(_('starting in-place swap of repository data\n')) |
673 ui.write(_('replaced files will be backed up at %s\n') % | 851 ui.write(_('replaced files will be backed up at %s\n') % |
674 backuppath) | 852 backuppath) |
675 | 853 |
676 # TODO do the store swap here. | 854 # Now swap in the new store directory. Doing it as a rename should make |
855 # the operation nearly instantaneous and atomic (at least in well-behaved | |
856 # environments). | |
857 ui.write(_('replacing store...\n')) | |
858 tstart = time.time() | |
859 util.rename(srcrepo.spath, backupvfs.join('store')) | |
860 util.rename(dstrepo.spath, srcrepo.spath) | |
861 elapsed = time.time() - tstart | |
862 ui.write(_('store replacement complete; repository was inconsistent for ' | |
863 '%0.1fs\n') % elapsed) | |
677 | 864 |
678 # We first write the requirements file. Any new requirements will lock | 865 # We first write the requirements file. Any new requirements will lock |
679 # out legacy clients. | 866 # out legacy clients. |
680 ui.write(_('finalizing requirements file and making repository readable ' | 867 ui.write(_('finalizing requirements file and making repository readable ' |
681 'again\n')) | 868 'again\n')) |