hgext/clonebundles.py
changeset 50404 971dc2369b04
parent 50403 5ae30ff79c76
child 50405 5b70b9f5a2f9
equal deleted inserted replaced
50403:5ae30ff79c76 50404:971dc2369b04
   207 
   207 
   208 It is possible to set Mercurial to automatically re-generate clone bundles when
   208 It is possible to set Mercurial to automatically re-generate clone bundles when
   209 new content is available.
   209 new content is available.
   210 
   210 
   211 Mercurial will take care of the process asynchronously. The defined list of
   211 Mercurial will take care of the process asynchronously. The defined list of
   212 bundle type will be generated, uploaded, and advertised.
   212 bundle-type will be generated, uploaded, and advertised. Older bundles will get
       
   213 decommissioned as newer ones replace them.
   213 
   214 
   214 Bundles Generation:
   215 Bundles Generation:
   215 ...................
   216 ...................
   216 
   217 
   217 The extension can generate multiple variants of the clone bundle. Each
   218 The extension can generate multiple variants of the clone bundle. Each
   233 
   234 
   234   [clone-bundles]
   235   [clone-bundles]
   235   upload-command=sftp put $HGCB_BUNDLE_PATH \
   236   upload-command=sftp put $HGCB_BUNDLE_PATH \
   236       sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
   237       sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
   237 
   238 
       
   239 If the file was already uploaded, the command must still succeed.
       
   240 
   238 After upload, the file should be available at an url defined by
   241 After upload, the file should be available at an url defined by
   239 `clone-bundles.url-template`.
   242 `clone-bundles.url-template`.
   240 
   243 
   241   [clone-bundles]
   244   [clone-bundles]
   242   url-template=https://bundles.host/cache/clone-bundles/{basename}
   245   url-template=https://bundles.host/cache/clone-bundles/{basename}
       
   246 
       
   247 Old bundles cleanup:
       
   248 ....................
       
   249 
       
   250 When new bundles are generated, the older ones are no longer necessary and can
       
   251 be removed from storage. This is done through the `clone-bundles.delete-command`
       
   252 configuration. The command is given the url of the artifact to delete through
       
   253 the `$HGCB_BUNDLE_URL` environment variable.
       
   254 
       
   255   [clone-bundles]
       
   256   delete-command=sftp rm sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
       
   257 
       
   258 If the file was already deleted, the command must still succeed.
   243 """
   259 """
   244 
   260 
   245 
   261 
   246 import os
   262 import os
   247 import weakref
   263 import weakref
   296 
   312 
   297 configitem(b'clone-bundles', b'auto-generate.formats', default=list)
   313 configitem(b'clone-bundles', b'auto-generate.formats', default=list)
   298 
   314 
   299 
   315 
   300 configitem(b'clone-bundles', b'upload-command', default=None)
   316 configitem(b'clone-bundles', b'upload-command', default=None)
       
   317 
       
   318 configitem(b'clone-bundles', b'delete-command', default=None)
   301 
   319 
   302 configitem(b'clone-bundles', b'url-template', default=None)
   320 configitem(b'clone-bundles', b'url-template', default=None)
   303 
   321 
   304 configitem(b'devel', b'debug.clonebundles', default=False)
   322 configitem(b'devel', b'debug.clonebundles', default=False)
   305 
   323 
   664             result = upload_bundle(repo, target)
   682             result = upload_bundle(repo, target)
   665             update_bundle_list(repo, new_bundles=[result])
   683             update_bundle_list(repo, new_bundles=[result])
   666     cleanup_tmp_bundle(repo, target)
   684     cleanup_tmp_bundle(repo, target)
   667 
   685 
   668 
   686 
       
   687 def find_outdated_bundles(repo, bundles):
       
   688     """finds outdated bundles"""
       
   689     olds = []
       
   690     per_types = {}
       
   691     for b in bundles:
       
   692         if not b.valid_for(repo):
       
   693             olds.append(b)
       
   694             continue
       
   695         l = per_types.setdefault(b.bundle_type, [])
       
   696         l.append(b)
       
   697     for key in sorted(per_types):
       
   698         all = per_types[key]
       
   699         if len(all) > 1:
       
   700             all.sort(key=lambda b: b.revs, reverse=True)
       
   701             olds.extend(all[1:])
       
   702     return olds
       
   703 
       
   704 
       
   705 def collect_garbage(repo):
       
   706     """finds outdated bundles and get them deleted"""
       
   707     with repo.clonebundles_lock():
       
   708         bundles = read_auto_gen(repo)
       
   709         olds = find_outdated_bundles(repo, bundles)
       
   710         for o in olds:
       
   711             delete_bundle(repo, o)
       
   712         update_bundle_list(repo, del_bundles=olds)
       
   713 
       
   714 
   669 def upload_bundle(repo, bundle):
   715 def upload_bundle(repo, bundle):
   670     """upload the result of a GeneratingBundle and return a GeneratedBundle
   716     """upload the result of a GeneratingBundle and return a GeneratedBundle
   671 
   717 
   672     The upload is done using the `clone-bundles.upload-command`
   718     The upload is done using the `clone-bundles.upload-command`
   673     """
   719     """
   689         .encode('utf8')
   735         .encode('utf8')
   690     )
   736     )
   691     return bundle.uploaded(url, basename)
   737     return bundle.uploaded(url, basename)
   692 
   738 
   693 
   739 
       
   740 def delete_bundle(repo, bundle):
       
   741     """delete a bundle from storage"""
       
   742     assert bundle.ready
       
   743     msg = b'clone-bundles: deleting bundle %s\n'
       
   744     msg %= bundle.basename
       
   745     if repo.ui.configbool(b'devel', b'debug.clonebundles'):
       
   746         repo.ui.write(msg)
       
   747     else:
       
   748         repo.ui.debug(msg)
       
   749 
       
   750     cmd = repo.ui.config(b'clone-bundles', b'delete-command')
       
   751     variables = {
       
   752         b'HGCB_BUNDLE_URL': bundle.file_url,
       
   753         b'HGCB_BASENAME': bundle.basename,
       
   754     }
       
   755     env = procutil.shellenviron(environ=variables)
       
   756     ret = repo.ui.system(cmd, environ=env)
       
   757     if ret:
       
   758         raise error.Abort(b"command returned status %d: %s" % (ret, cmd))
       
   759 
       
   760 
   694 def auto_bundle_needed_actions(repo, bundles, op_id):
   761 def auto_bundle_needed_actions(repo, bundles, op_id):
   695     """find the list of bundles that need action
   762     """find the list of bundles that need action
   696 
   763 
   697     returns a list of RequestedBundle objects that need to be generated and
   764     returns a list of RequestedBundle objects that need to be generated and
   698     uploaded."""
   765     uploaded."""
   699     create_bundles = []
   766     create_bundles = []
       
   767     delete_bundles = []
   700     repo = repo.filtered(b"immutable")
   768     repo = repo.filtered(b"immutable")
   701     targets = repo.ui.configlist(b'clone-bundles', b'auto-generate.formats')
   769     targets = repo.ui.configlist(b'clone-bundles', b'auto-generate.formats')
   702     revs = len(repo.changelog)
   770     revs = len(repo.changelog)
   703     generic_data = {
   771     generic_data = {
   704         'revs': revs,
   772         'revs': revs,
   710     for t in targets:
   778     for t in targets:
   711         data = generic_data.copy()
   779         data = generic_data.copy()
   712         data['bundle_type'] = t
   780         data['bundle_type'] = t
   713         b = RequestedBundle(**data)
   781         b = RequestedBundle(**data)
   714         create_bundles.append(b)
   782         create_bundles.append(b)
   715     return create_bundles
   783     delete_bundles.extend(find_outdated_bundles(repo, bundles))
       
   784     return create_bundles, delete_bundles
   716 
   785 
   717 
   786 
   718 def start_one_bundle(repo, bundle):
   787 def start_one_bundle(repo, bundle):
   719     """start the generation of a single bundle file
   788     """start the generation of a single bundle file
   720 
   789 
   757 def debugmakeclonebundles(ui, repo):
   826 def debugmakeclonebundles(ui, repo):
   758     """Internal command to auto-generate debug bundles"""
   827     """Internal command to auto-generate debug bundles"""
   759     requested_bundle = util.pickle.load(procutil.stdin)
   828     requested_bundle = util.pickle.load(procutil.stdin)
   760     procutil.stdin.close()
   829     procutil.stdin.close()
   761 
   830 
       
   831     collect_garbage(repo)
       
   832 
   762     fname = requested_bundle.suggested_filename
   833     fname = requested_bundle.suggested_filename
   763     fpath = repo.vfs.makedirs(b'tmp-bundles')
   834     fpath = repo.vfs.makedirs(b'tmp-bundles')
   764     fpath = repo.vfs.join(b'tmp-bundles', fname)
   835     fpath = repo.vfs.join(b'tmp-bundles', fname)
   765     bundle = requested_bundle.generating(fpath)
   836     bundle = requested_bundle.generating(fpath)
   766     update_bundle_list(repo, new_bundles=[bundle])
   837     update_bundle_list(repo, new_bundles=[bundle])
   776 
   847 
   777     def autobundle(tr):
   848     def autobundle(tr):
   778         repo = reporef()
   849         repo = reporef()
   779         assert repo is not None
   850         assert repo is not None
   780         bundles = read_auto_gen(repo)
   851         bundles = read_auto_gen(repo)
   781         new = auto_bundle_needed_actions(repo, bundles, b"%d_txn" % id(tr))
   852         new, __ = auto_bundle_needed_actions(repo, bundles, b"%d_txn" % id(tr))
   782         for data in new:
   853         for data in new:
   783             start_one_bundle(repo, data)
   854             start_one_bundle(repo, data)
   784         return None
   855         return None
   785 
   856 
   786     return autobundle
   857     return autobundle