comparison mercurial/wireprotov2server.py @ 39641:aa7e312375cf

wireprotov2: let clients drive delta behavior Previously, the "manifestdata" and "filedata" commands assumed the receiver had all parent revisions for requested nodes. Unless the revision had no parents, they emitted a delta instead of a fulltext. This strategy isn't appropriate for shallow clones and for clients that only want to access fulltext revision data for a single node without fetching their parent revisions. This commit adds an "haveparents" argument to the "manifestdata" and "filedata" commands that controls delta generation behavior. Unless "haveparents" is set, the server assumes that the client doesn't have parent revisions unless they were previously sent as part of the current group of revisions. This change allows the fulltext revision data of any individual revision to be obtained. This will facilitate shallow clones and other data retrieval strategies that don't require all previous revisions of an entity to be fetched. Differential Revision: https://phab.mercurial-scm.org/D4492
author Gregory Szorc <gregory.szorc@gmail.com>
date Thu, 30 Aug 2018 14:55:34 -0700
parents 0e03e6a44dee
children 0b61d21f05cc
comparison
equal deleted inserted replaced
39640:039bf1eddc2e 39641:aa7e312375cf
413 caps['rawrepoformats'] = sorted(repo.requirements & 413 caps['rawrepoformats'] = sorted(repo.requirements &
414 repo.supportedformats) 414 repo.supportedformats)
415 415
416 return proto.addcapabilities(repo, caps) 416 return proto.addcapabilities(repo, caps)
417 417
418 def builddeltarequests(store, nodes): 418 def builddeltarequests(store, nodes, haveparents):
419 """Build a series of revision delta requests against a backend store. 419 """Build a series of revision delta requests against a backend store.
420 420
421 Returns a list of revision numbers in the order they should be sent 421 Returns a list of revision numbers in the order they should be sent
422 and a list of ``irevisiondeltarequest`` instances to be made against 422 and a list of ``irevisiondeltarequest`` instances to be made against
423 the backend store. 423 the backend store.
428 # a list of nodes and delta preconditions over a figurative wall and 428 # a list of nodes and delta preconditions over a figurative wall and
429 # have the storage backend figure it out for us. 429 # have the storage backend figure it out for us.
430 revs = dagop.linearize({store.rev(n) for n in nodes}, store.parentrevs) 430 revs = dagop.linearize({store.rev(n) for n in nodes}, store.parentrevs)
431 431
432 requests = [] 432 requests = []
433 seenrevs = set()
433 434
434 for rev in revs: 435 for rev in revs:
435 node = store.node(rev) 436 node = store.node(rev)
436 parents = store.parents(node) 437 parentnodes = store.parents(node)
437 deltaparent = store.node(store.deltaparent(rev)) 438 parentrevs = [store.rev(n) for n in parentnodes]
438 439 deltabaserev = store.deltaparent(rev)
439 # There is a delta in storage. That means we can send the delta 440 deltabasenode = store.node(deltabaserev)
440 # efficiently. 441
442 # The choice of whether to send a fulltext revision or a delta and
443 # what delta to send is governed by a few factors.
441 # 444 #
442 # But, the delta may be against a revision the receiver doesn't 445 # To send a delta, we need to ensure the receiver is capable of
443 # have (e.g. shallow clone or when the delta isn't against a parent 446 # decoding it. And that requires the receiver to have the base
444 # revision). For now, we ignore the problem of shallow clone. As 447 # revision the delta is against.
445 # long as a delta exists against a parent, we send it.
446 # TODO allow arguments to control this behavior, as the receiver
447 # may not have the base revision in some scenarios.
448 if deltaparent != nullid and deltaparent in parents:
449 basenode = deltaparent
450
451 # Else there is no delta parent in storage or the delta that is
452 # # there isn't suitable. Let's use a delta against a parent
453 # revision, if possible.
454 # 448 #
455 # There is room to check if the delta parent is in the ancestry of 449 # We can only guarantee the receiver has the base revision if
456 # this node. But there isn't an API on the manifest storage object 450 # a) we've already sent the revision as part of this group
457 # for that. So ignore this case for now. 451 # b) the receiver has indicated they already have the revision.
458 452 # And the mechanism for "b" is the client indicating they have
459 elif parents[0] != nullid: 453 # parent revisions. So this means we can only send the delta if
460 basenode = parents[0] 454 # it is sent before or it is against a delta and the receiver says
461 elif parents[1] != nullid: 455 # they have a parent.
462 basenode = parents[1] 456
463 457 # We can send storage delta if it is against a revision we've sent
464 # No potential bases to delta against. Send a full revision. 458 # in this group.
459 if deltabaserev != nullrev and deltabaserev in seenrevs:
460 basenode = deltabasenode
461
462 # We can send storage delta if it is against a parent revision and
463 # the receiver indicates they have the parents.
464 elif (deltabaserev != nullrev and deltabaserev in parentrevs
465 and haveparents):
466 basenode = deltabasenode
467
468 # Otherwise the storage delta isn't appropriate. Fall back to
469 # using another delta, if possible.
470
471 # Use p1 if we've emitted it or receiver says they have it.
472 elif parentrevs[0] != nullrev and (
473 parentrevs[0] in seenrevs or haveparents):
474 basenode = parentnodes[0]
475
476 # Use p2 if we've emitted it or receiver says they have it.
477 elif parentrevs[1] != nullrev and (
478 parentrevs[1] in seenrevs or haveparents):
479 basenode = parentnodes[1]
480
481 # Nothing appropriate to delta against. Send the full revision.
465 else: 482 else:
466 basenode = nullid 483 basenode = nullid
467 484
468 requests.append(changegroup.revisiondeltarequest( 485 requests.append(changegroup.revisiondeltarequest(
469 node=node, 486 node=node,
470 p1node=parents[0], 487 p1node=parentnodes[0],
471 p2node=parents[1], 488 p2node=parentnodes[1],
472 # Receiver deals with linknode resolution. 489 # Receiver deals with linknode resolution.
473 linknode=nullid, 490 linknode=nullid,
474 basenode=basenode, 491 basenode=basenode,
475 )) 492 ))
493
494 seenrevs.add(rev)
476 495
477 return revs, requests 496 return revs, requests
478 497
479 def wireprotocommand(name, args=None, permission='push'): 498 def wireprotocommand(name, args=None, permission='push'):
480 """Decorator to declare a wire protocol command. 499 """Decorator to declare a wire protocol command.
672 691
673 return fl 692 return fl
674 693
675 @wireprotocommand('filedata', 694 @wireprotocommand('filedata',
676 args={ 695 args={
696 'haveparents': True,
677 'nodes': [b'0123456...'], 697 'nodes': [b'0123456...'],
678 'fields': [b'parents', b'revision'], 698 'fields': [b'parents', b'revision'],
679 'path': b'foo.txt', 699 'path': b'foo.txt',
680 }, 700 },
681 permission='pull') 701 permission='pull')
682 def filedata(repo, proto, nodes=None, fields=None, path=None): 702 def filedata(repo, proto, haveparents=False, nodes=None, fields=None,
703 path=None):
683 fields = fields or set() 704 fields = fields or set()
684 705
685 if nodes is None: 706 if nodes is None:
686 raise error.WireprotoCommandError('nodes argument must be defined') 707 raise error.WireprotoCommandError('nodes argument must be defined')
687 708
700 store.rev(node) 721 store.rev(node)
701 except error.LookupError: 722 except error.LookupError:
702 raise error.WireprotoCommandError('unknown file node: %s', 723 raise error.WireprotoCommandError('unknown file node: %s',
703 (hex(node),)) 724 (hex(node),))
704 725
705 revs, requests = builddeltarequests(store, nodes) 726 revs, requests = builddeltarequests(store, nodes, haveparents)
706 727
707 yield { 728 yield {
708 b'totalitems': len(revs), 729 b'totalitems': len(revs),
709 } 730 }
710 731
802 yield node 823 yield node
803 824
804 @wireprotocommand('manifestdata', 825 @wireprotocommand('manifestdata',
805 args={ 826 args={
806 'nodes': [b'0123456...'], 827 'nodes': [b'0123456...'],
828 'haveparents': True,
807 'fields': [b'parents', b'revision'], 829 'fields': [b'parents', b'revision'],
808 'tree': b'', 830 'tree': b'',
809 }, 831 },
810 permission='pull') 832 permission='pull')
811 def manifestdata(repo, proto, nodes=None, fields=None, tree=None): 833 def manifestdata(repo, proto, haveparents=False, nodes=None, fields=None,
834 tree=None):
812 fields = fields or set() 835 fields = fields or set()
813 836
814 if nodes is None: 837 if nodes is None:
815 raise error.WireprotoCommandError( 838 raise error.WireprotoCommandError(
816 'nodes argument must be defined') 839 'nodes argument must be defined')
827 store.rev(node) 850 store.rev(node)
828 except error.LookupError: 851 except error.LookupError:
829 raise error.WireprotoCommandError( 852 raise error.WireprotoCommandError(
830 'unknown node: %s', (node,)) 853 'unknown node: %s', (node,))
831 854
832 revs, requests = builddeltarequests(store, nodes) 855 revs, requests = builddeltarequests(store, nodes, haveparents)
833 856
834 yield { 857 yield {
835 b'totalitems': len(revs), 858 b'totalitems': len(revs),
836 } 859 }
837 860