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'))