##// END OF EJS Templates
clone-bundles: add an option to generate bundles in the background...
marmoute -
r51307:3973b1dc default
parent child Browse files
Show More
@@ -1,996 +1,1022 b''
1 1 # This software may be used and distributed according to the terms of the
2 2 # GNU General Public License version 2 or any later version.
3 3
4 4 """advertise pre-generated bundles to seed clones
5 5
6 6 "clonebundles" is a server-side extension used to advertise the existence
7 7 of pre-generated, externally hosted bundle files to clients that are
8 8 cloning so that cloning can be faster, more reliable, and require less
9 9 resources on the server. "pullbundles" is a related feature for sending
10 10 pre-generated bundle files to clients as part of pull operations.
11 11
12 12 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
13 13 the server, in response to a client's request to clone, dynamically generates
14 14 a bundle containing the entire repository content and sends it to the client.
15 15 There is no caching on the server and the server will have to redundantly
16 16 generate the same outgoing bundle in response to each clone request. For
17 17 servers with large repositories or with high clone volume, the load from
18 18 clones can make scaling the server challenging and costly.
19 19
20 20 This extension provides server operators the ability to offload
21 21 potentially expensive clone load to an external service. Pre-generated
22 22 bundles also allow using more CPU intensive compression, reducing the
23 23 effective bandwidth requirements.
24 24
25 25 Here's how clone bundles work:
26 26
27 27 1. A server operator establishes a mechanism for making bundle files available
28 28 on a hosting service where Mercurial clients can fetch them.
29 29 2. A manifest file listing available bundle URLs and some optional metadata
30 30 is added to the Mercurial repository on the server.
31 31 3. A client initiates a clone against a clone bundles aware server.
32 32 4. The client sees the server is advertising clone bundles and fetches the
33 33 manifest listing available bundles.
34 34 5. The client filters and sorts the available bundles based on what it
35 35 supports and prefers.
36 36 6. The client downloads and applies an available bundle from the
37 37 server-specified URL.
38 38 7. The client reconnects to the original server and performs the equivalent
39 39 of :hg:`pull` to retrieve all repository data not in the bundle. (The
40 40 repository could have been updated between when the bundle was created
41 41 and when the client started the clone.) This may use "pullbundles".
42 42
43 43 Instead of the server generating full repository bundles for every clone
44 44 request, it generates full bundles once and they are subsequently reused to
45 45 bootstrap new clones. The server may still transfer data at clone time.
46 46 However, this is only data that has been added/changed since the bundle was
47 47 created. For large, established repositories, this can reduce server load for
48 48 clones to less than 1% of original.
49 49
50 50 Here's how pullbundles work:
51 51
52 52 1. A manifest file listing available bundles and describing the revisions
53 53 is added to the Mercurial repository on the server.
54 54 2. A new-enough client informs the server that it supports partial pulls
55 55 and initiates a pull.
56 56 3. If the server has pull bundles enabled and sees the client advertising
57 57 partial pulls, it checks for a matching pull bundle in the manifest.
58 58 A bundle matches if the format is supported by the client, the client
59 59 has the required revisions already and needs something from the bundle.
60 60 4. If there is at least one matching bundle, the server sends it to the client.
61 61 5. The client applies the bundle and notices that the server reply was
62 62 incomplete. It initiates another pull.
63 63
64 64 To work, this extension requires the following of server operators:
65 65
66 66 * Generating bundle files of repository content (typically periodically,
67 67 such as once per day).
68 68 * Clone bundles: A file server that clients have network access to and that
69 69 Python knows how to talk to through its normal URL handling facility
70 70 (typically an HTTP/HTTPS server).
71 71 * A process for keeping the bundles manifest in sync with available bundle
72 72 files.
73 73
74 74 Strictly speaking, using a static file hosting server isn't required: a server
75 75 operator could use a dynamic service for retrieving bundle data. However,
76 76 static file hosting services are simple and scalable and should be sufficient
77 77 for most needs.
78 78
79 79 Bundle files can be generated with the :hg:`bundle` command. Typically
80 80 :hg:`bundle --all` is used to produce a bundle of the entire repository.
81 81
82 82 :hg:`debugcreatestreamclonebundle` can be used to produce a special
83 83 *streaming clonebundle*. These are bundle files that are extremely efficient
84 84 to produce and consume (read: fast). However, they are larger than
85 85 traditional bundle formats and require that clients support the exact set
86 86 of repository data store formats in use by the repository that created them.
87 87 Typically, a newer server can serve data that is compatible with older clients.
88 88 However, *streaming clone bundles* don't have this guarantee. **Server
89 89 operators need to be aware that newer versions of Mercurial may produce
90 90 streaming clone bundles incompatible with older Mercurial versions.**
91 91
92 92 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
93 93 file containing the list of available bundle files suitable for seeding
94 94 clones. If this file does not exist, the repository will not advertise the
95 95 existence of clone bundles when clients connect. For pull bundles,
96 96 ``.hg/pullbundles.manifest`` is used.
97 97
98 98 The manifest file contains a newline (\\n) delimited list of entries.
99 99
100 100 Each line in this file defines an available bundle. Lines have the format:
101 101
102 102 <URL> [<key>=<value>[ <key>=<value>]]
103 103
104 104 That is, a URL followed by an optional, space-delimited list of key=value
105 105 pairs describing additional properties of this bundle. Both keys and values
106 106 are URI encoded.
107 107
108 108 For pull bundles, the URL is a path under the ``.hg`` directory of the
109 109 repository.
110 110
111 111 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
112 112 All non-uppercase keys can be used by site installations. An example use
113 113 for custom properties is to use the *datacenter* attribute to define which
114 114 data center a file is hosted in. Clients could then prefer a server in the
115 115 data center closest to them.
116 116
117 117 The following reserved keys are currently defined:
118 118
119 119 BUNDLESPEC
120 120 A "bundle specification" string that describes the type of the bundle.
121 121
122 122 These are string values that are accepted by the "--type" argument of
123 123 :hg:`bundle`.
124 124
125 125 The values are parsed in strict mode, which means they must be of the
126 126 "<compression>-<type>" form. See
127 127 mercurial.exchange.parsebundlespec() for more details.
128 128
129 129 :hg:`debugbundle --spec` can be used to print the bundle specification
130 130 string for a bundle file. The output of this command can be used verbatim
131 131 for the value of ``BUNDLESPEC`` (it is already escaped).
132 132
133 133 Clients will automatically filter out specifications that are unknown or
134 134 unsupported so they won't attempt to download something that likely won't
135 135 apply.
136 136
137 137 The actual value doesn't impact client behavior beyond filtering:
138 138 clients will still sniff the bundle type from the header of downloaded
139 139 files.
140 140
141 141 **Use of this key is highly recommended**, as it allows clients to
142 142 easily skip unsupported bundles. If this key is not defined, an old
143 143 client may attempt to apply a bundle that it is incapable of reading.
144 144
145 145 REQUIRESNI
146 146 Whether Server Name Indication (SNI) is required to connect to the URL.
147 147 SNI allows servers to use multiple certificates on the same IP. It is
148 148 somewhat common in CDNs and other hosting providers. Older Python
149 149 versions do not support SNI. Defining this attribute enables clients
150 150 with older Python versions to filter this entry without experiencing
151 151 an opaque SSL failure at connection time.
152 152
153 153 If this is defined, it is important to advertise a non-SNI fallback
154 154 URL or clients running old Python releases may not be able to clone
155 155 with the clonebundles facility.
156 156
157 157 Value should be "true".
158 158
159 159 REQUIREDRAM
160 160 Value specifies expected memory requirements to decode the payload.
161 161 Values can have suffixes for common bytes sizes. e.g. "64MB".
162 162
163 163 This key is often used with zstd-compressed bundles using a high
164 164 compression level / window size, which can require 100+ MB of memory
165 165 to decode.
166 166
167 167 heads
168 168 Used for pull bundles. This contains the ``;`` separated changeset
169 169 hashes of the heads of the bundle content.
170 170
171 171 bases
172 172 Used for pull bundles. This contains the ``;`` separated changeset
173 173 hashes of the roots of the bundle content. This can be skipped if
174 174 the bundle was created without ``--base``.
175 175
176 176 Manifests can contain multiple entries. Assuming metadata is defined, clients
177 177 will filter entries from the manifest that they don't support. The remaining
178 178 entries are optionally sorted by client preferences
179 179 (``ui.clonebundleprefers`` config option). The client then attempts
180 180 to fetch the bundle at the first URL in the remaining list.
181 181
182 182 **Errors when downloading a bundle will fail the entire clone operation:
183 183 clients do not automatically fall back to a traditional clone.** The reason
184 184 for this is that if a server is using clone bundles, it is probably doing so
185 185 because the feature is necessary to help it scale. In other words, there
186 186 is an assumption that clone load will be offloaded to another service and
187 187 that the Mercurial server isn't responsible for serving this clone load.
188 188 If that other service experiences issues and clients start mass falling back to
189 189 the original Mercurial server, the added clone load could overwhelm the server
190 190 due to unexpected load and effectively take it offline. Not having clients
191 191 automatically fall back to cloning from the original server mitigates this
192 192 scenario.
193 193
194 194 Because there is no automatic Mercurial server fallback on failure of the
195 195 bundle hosting service, it is important for server operators to view the bundle
196 196 hosting service as an extension of the Mercurial server in terms of
197 197 availability and service level agreements: if the bundle hosting service goes
198 198 down, so does the ability for clients to clone. Note: clients will see a
199 199 message informing them how to bypass the clone bundles facility when a failure
200 200 occurs. So server operators should prepare for some people to follow these
201 201 instructions when a failure occurs, thus driving more load to the original
202 202 Mercurial server when the bundle hosting service fails.
203 203
204 204
205 205 auto-generation of clone bundles
206 206 --------------------------------
207 207
208 208 It is possible to set Mercurial to automatically re-generate clone bundles when
209 209 enough new content is available.
210 210
211 211 Mercurial will take care of the process asynchronously. The defined list of
212 212 bundle-type will be generated, uploaded, and advertised. Older bundles will get
213 213 decommissioned as newer ones replace them.
214 214
215 215 Bundles Generation:
216 216 ...................
217 217
218 218 The extension can generate multiple variants of the clone bundle. Each
219 219 different variant will be defined by the "bundle-spec" they use::
220 220
221 221 [clone-bundles]
222 222 auto-generate.formats= zstd-v2, gzip-v2
223 223
224 224 See `hg help bundlespec` for details about available options.
225 225
226 226 By default, new bundles are generated when 5% of the repository contents or at
227 227 least 1000 revisions are not contained in the cached bundles. This option can
228 228 be controlled by the `clone-bundles.trigger.below-bundled-ratio` option
229 229 (default 0.95) and the `clone-bundles.trigger.revs` option (default 1000)::
230 230
231 231 [clone-bundles]
232 232 trigger.below-bundled-ratio=0.95
233 233 trigger.revs=1000
234 234
235 235 This logic can be manually triggered using the `admin::clone-bundles-refresh`
236 236 command, or automatically on each repository change if
237 237 `clone-bundles.auto-generate.on-change` is set to `yes`.
238 238
239 239 [clone-bundles]
240 240 auto-generate.on-change=yes
241 241 auto-generate.formats= zstd-v2, gzip-v2
242 242
243 243 Bundles Upload and Serving:
244 244 ...........................
245 245
246 246 The generated bundles need to be made available to users through a "public" URL.
247 247 This should be donne through `clone-bundles.upload-command` configuration. The
248 248 value of this command should be a shell command. It will have access to the
249 249 bundle file path through the `$HGCB_BUNDLE_PATH` variable. And the expected
250 250 basename in the "public" URL is accessible at::
251 251
252 252 [clone-bundles]
253 253 upload-command=sftp put $HGCB_BUNDLE_PATH \
254 254 sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
255 255
256 256 If the file was already uploaded, the command must still succeed.
257 257
258 258 After upload, the file should be available at an url defined by
259 259 `clone-bundles.url-template`.
260 260
261 261 [clone-bundles]
262 262 url-template=https://bundles.host/cache/clone-bundles/{basename}
263 263
264 264 Old bundles cleanup:
265 265 ....................
266 266
267 267 When new bundles are generated, the older ones are no longer necessary and can
268 268 be removed from storage. This is done through the `clone-bundles.delete-command`
269 269 configuration. The command is given the url of the artifact to delete through
270 270 the `$HGCB_BUNDLE_URL` environment variable.
271 271
272 272 [clone-bundles]
273 273 delete-command=sftp rm sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
274 274
275 275 If the file was already deleted, the command must still succeed.
276 276 """
277 277
278 278
279 279 import os
280 280 import weakref
281 281
282 282 from mercurial.i18n import _
283 283
284 284 from mercurial import (
285 285 bundlecaches,
286 286 commands,
287 287 error,
288 288 extensions,
289 289 localrepo,
290 290 lock,
291 291 node,
292 292 registrar,
293 293 util,
294 294 wireprotov1server,
295 295 )
296 296
297 297
298 298 from mercurial.utils import (
299 299 procutil,
300 300 )
301 301
302 302 testedwith = b'ships-with-hg-core'
303 303
304 304
305 305 def capabilities(orig, repo, proto):
306 306 caps = orig(repo, proto)
307 307
308 308 # Only advertise if a manifest exists. This does add some I/O to requests.
309 309 # But this should be cheaper than a wasted network round trip due to
310 310 # missing file.
311 311 if repo.vfs.exists(bundlecaches.CB_MANIFEST_FILE):
312 312 caps.append(b'clonebundles')
313 313
314 314 return caps
315 315
316 316
317 317 def extsetup(ui):
318 318 extensions.wrapfunction(wireprotov1server, b'_capabilities', capabilities)
319 319
320 320
321 321 # logic for bundle auto-generation
322 322
323 323
324 324 configtable = {}
325 325 configitem = registrar.configitem(configtable)
326 326
327 327 cmdtable = {}
328 328 command = registrar.command(cmdtable)
329 329
330 330 configitem(b'clone-bundles', b'auto-generate.on-change', default=False)
331 331 configitem(b'clone-bundles', b'auto-generate.formats', default=list)
332 332 configitem(b'clone-bundles', b'trigger.below-bundled-ratio', default=0.95)
333 333 configitem(b'clone-bundles', b'trigger.revs', default=1000)
334 334
335 335 configitem(b'clone-bundles', b'upload-command', default=None)
336 336
337 337 configitem(b'clone-bundles', b'delete-command', default=None)
338 338
339 339 configitem(b'clone-bundles', b'url-template', default=None)
340 340
341 341 configitem(b'devel', b'debug.clonebundles', default=False)
342 342
343 343
344 344 # category for the post-close transaction hooks
345 345 CAT_POSTCLOSE = b"clonebundles-autobundles"
346 346
347 347 # template for bundle file names
348 348 BUNDLE_MASK = (
349 349 b"full-%(bundle_type)s-%(revs)d_revs-%(tip_short)s_tip-%(op_id)s.hg"
350 350 )
351 351
352 352
353 353 # file in .hg/ use to track clonebundles being auto-generated
354 354 AUTO_GEN_FILE = b'clonebundles.auto-gen'
355 355
356 356
357 357 class BundleBase(object):
358 358 """represents the core of properties that matters for us in a bundle
359 359
360 360 :bundle_type: the bundlespec (see hg help bundlespec)
361 361 :revs: the number of revisions in the repo at bundle creation time
362 362 :tip_rev: the rev-num of the tip revision
363 363 :tip_node: the node id of the tip-most revision in the bundle
364 364
365 365 :ready: True if the bundle is ready to be served
366 366 """
367 367
368 368 ready = False
369 369
370 370 def __init__(self, bundle_type, revs, tip_rev, tip_node):
371 371 self.bundle_type = bundle_type
372 372 self.revs = revs
373 373 self.tip_rev = tip_rev
374 374 self.tip_node = tip_node
375 375
376 376 def valid_for(self, repo):
377 377 """is this bundle applicable to the current repository
378 378
379 379 This is useful for detecting bundles made irrelevant by stripping.
380 380 """
381 381 tip_node = node.bin(self.tip_node)
382 382 return repo.changelog.index.get_rev(tip_node) == self.tip_rev
383 383
384 384 def __eq__(self, other):
385 385 left = (self.ready, self.bundle_type, self.tip_rev, self.tip_node)
386 386 right = (other.ready, other.bundle_type, other.tip_rev, other.tip_node)
387 387 return left == right
388 388
389 389 def __neq__(self, other):
390 390 return not self == other
391 391
392 392 def __cmp__(self, other):
393 393 if self == other:
394 394 return 0
395 395 return -1
396 396
397 397
398 398 class RequestedBundle(BundleBase):
399 399 """A bundle that should be generated.
400 400
401 401 Additional attributes compared to BundleBase
402 402 :heads: list of head revisions (as rev-num)
403 403 :op_id: a "unique" identifier for the operation triggering the change
404 404 """
405 405
406 406 def __init__(self, bundle_type, revs, tip_rev, tip_node, head_revs, op_id):
407 407 self.head_revs = head_revs
408 408 self.op_id = op_id
409 409 super(RequestedBundle, self).__init__(
410 410 bundle_type,
411 411 revs,
412 412 tip_rev,
413 413 tip_node,
414 414 )
415 415
416 416 @property
417 417 def suggested_filename(self):
418 418 """A filename that can be used for the generated bundle"""
419 419 data = {
420 420 b'bundle_type': self.bundle_type,
421 421 b'revs': self.revs,
422 422 b'heads': self.head_revs,
423 423 b'tip_rev': self.tip_rev,
424 424 b'tip_node': self.tip_node,
425 425 b'tip_short': self.tip_node[:12],
426 426 b'op_id': self.op_id,
427 427 }
428 428 return BUNDLE_MASK % data
429 429
430 430 def generate_bundle(self, repo, file_path):
431 431 """generate the bundle at `filepath`"""
432 432 commands.bundle(
433 433 repo.ui,
434 434 repo,
435 435 file_path,
436 436 base=[b"null"],
437 437 rev=self.head_revs,
438 438 type=self.bundle_type,
439 439 quiet=True,
440 440 )
441 441
442 442 def generating(self, file_path, hostname=None, pid=None):
443 443 """return a GeneratingBundle object from this object"""
444 444 if pid is None:
445 445 pid = os.getpid()
446 446 if hostname is None:
447 447 hostname = lock._getlockprefix()
448 448 return GeneratingBundle(
449 449 self.bundle_type,
450 450 self.revs,
451 451 self.tip_rev,
452 452 self.tip_node,
453 453 hostname,
454 454 pid,
455 455 file_path,
456 456 )
457 457
458 458
459 459 class GeneratingBundle(BundleBase):
460 460 """A bundle being generated
461 461
462 462 extra attributes compared to BundleBase:
463 463
464 464 :hostname: the hostname of the machine generating the bundle
465 465 :pid: the pid of the process generating the bundle
466 466 :filepath: the target filename of the bundle
467 467
468 468 These attributes exist to help detect stalled generation processes.
469 469 """
470 470
471 471 ready = False
472 472
473 473 def __init__(
474 474 self, bundle_type, revs, tip_rev, tip_node, hostname, pid, filepath
475 475 ):
476 476 self.hostname = hostname
477 477 self.pid = pid
478 478 self.filepath = filepath
479 479 super(GeneratingBundle, self).__init__(
480 480 bundle_type, revs, tip_rev, tip_node
481 481 )
482 482
483 483 @classmethod
484 484 def from_line(cls, line):
485 485 """create an object by deserializing a line from AUTO_GEN_FILE"""
486 486 assert line.startswith(b'PENDING-v1 ')
487 487 (
488 488 __,
489 489 bundle_type,
490 490 revs,
491 491 tip_rev,
492 492 tip_node,
493 493 hostname,
494 494 pid,
495 495 filepath,
496 496 ) = line.split()
497 497 hostname = util.urlreq.unquote(hostname)
498 498 filepath = util.urlreq.unquote(filepath)
499 499 revs = int(revs)
500 500 tip_rev = int(tip_rev)
501 501 pid = int(pid)
502 502 return cls(
503 503 bundle_type, revs, tip_rev, tip_node, hostname, pid, filepath
504 504 )
505 505
506 506 def to_line(self):
507 507 """serialize the object to include as a line in AUTO_GEN_FILE"""
508 508 templ = b"PENDING-v1 %s %d %d %s %s %d %s"
509 509 data = (
510 510 self.bundle_type,
511 511 self.revs,
512 512 self.tip_rev,
513 513 self.tip_node,
514 514 util.urlreq.quote(self.hostname),
515 515 self.pid,
516 516 util.urlreq.quote(self.filepath),
517 517 )
518 518 return templ % data
519 519
520 520 def __eq__(self, other):
521 521 if not super(GeneratingBundle, self).__eq__(other):
522 522 return False
523 523 left = (self.hostname, self.pid, self.filepath)
524 524 right = (other.hostname, other.pid, other.filepath)
525 525 return left == right
526 526
527 527 def uploaded(self, url, basename):
528 528 """return a GeneratedBundle from this object"""
529 529 return GeneratedBundle(
530 530 self.bundle_type,
531 531 self.revs,
532 532 self.tip_rev,
533 533 self.tip_node,
534 534 url,
535 535 basename,
536 536 )
537 537
538 538
539 539 class GeneratedBundle(BundleBase):
540 540 """A bundle that is done being generated and can be served
541 541
542 542 extra attributes compared to BundleBase:
543 543
544 544 :file_url: the url where the bundle is available.
545 545 :basename: the "basename" used to upload (useful for deletion)
546 546
547 547 These attributes exist to generate a bundle manifest
548 548 (.hg/pullbundles.manifest)
549 549 """
550 550
551 551 ready = True
552 552
553 553 def __init__(
554 554 self, bundle_type, revs, tip_rev, tip_node, file_url, basename
555 555 ):
556 556 self.file_url = file_url
557 557 self.basename = basename
558 558 super(GeneratedBundle, self).__init__(
559 559 bundle_type, revs, tip_rev, tip_node
560 560 )
561 561
562 562 @classmethod
563 563 def from_line(cls, line):
564 564 """create an object by deserializing a line from AUTO_GEN_FILE"""
565 565 assert line.startswith(b'DONE-v1 ')
566 566 (
567 567 __,
568 568 bundle_type,
569 569 revs,
570 570 tip_rev,
571 571 tip_node,
572 572 file_url,
573 573 basename,
574 574 ) = line.split()
575 575 revs = int(revs)
576 576 tip_rev = int(tip_rev)
577 577 file_url = util.urlreq.unquote(file_url)
578 578 return cls(bundle_type, revs, tip_rev, tip_node, file_url, basename)
579 579
580 580 def to_line(self):
581 581 """serialize the object to include as a line in AUTO_GEN_FILE"""
582 582 templ = b"DONE-v1 %s %d %d %s %s %s"
583 583 data = (
584 584 self.bundle_type,
585 585 self.revs,
586 586 self.tip_rev,
587 587 self.tip_node,
588 588 util.urlreq.quote(self.file_url),
589 589 self.basename,
590 590 )
591 591 return templ % data
592 592
593 593 def manifest_line(self):
594 594 """serialize the object to include as a line in pullbundles.manifest"""
595 595 templ = b"%s BUNDLESPEC=%s REQUIRESNI=true"
596 596 return templ % (self.file_url, self.bundle_type)
597 597
598 598 def __eq__(self, other):
599 599 if not super(GeneratedBundle, self).__eq__(other):
600 600 return False
601 601 return self.file_url == other.file_url
602 602
603 603
604 604 def parse_auto_gen(content):
605 605 """parse the AUTO_GEN_FILE to return a list of Bundle object"""
606 606 bundles = []
607 607 for line in content.splitlines():
608 608 if line.startswith(b'PENDING-v1 '):
609 609 bundles.append(GeneratingBundle.from_line(line))
610 610 elif line.startswith(b'DONE-v1 '):
611 611 bundles.append(GeneratedBundle.from_line(line))
612 612 return bundles
613 613
614 614
615 615 def dumps_auto_gen(bundles):
616 616 """serialize a list of Bundle as a AUTO_GEN_FILE content"""
617 617 lines = []
618 618 for b in bundles:
619 619 lines.append(b"%s\n" % b.to_line())
620 620 lines.sort()
621 621 return b"".join(lines)
622 622
623 623
624 624 def read_auto_gen(repo):
625 625 """read the AUTO_GEN_FILE for the <repo> a list of Bundle object"""
626 626 data = repo.vfs.tryread(AUTO_GEN_FILE)
627 627 if not data:
628 628 return []
629 629 return parse_auto_gen(data)
630 630
631 631
632 632 def write_auto_gen(repo, bundles):
633 633 """write a list of Bundle objects into the repo's AUTO_GEN_FILE"""
634 634 assert repo._cb_lock_ref is not None
635 635 data = dumps_auto_gen(bundles)
636 636 with repo.vfs(AUTO_GEN_FILE, mode=b'wb', atomictemp=True) as f:
637 637 f.write(data)
638 638
639 639
640 640 def generate_manifest(bundles):
641 641 """write a list of Bundle objects into the repo's AUTO_GEN_FILE"""
642 642 bundles = list(bundles)
643 643 bundles.sort(key=lambda b: b.bundle_type)
644 644 lines = []
645 645 for b in bundles:
646 646 lines.append(b"%s\n" % b.manifest_line())
647 647 return b"".join(lines)
648 648
649 649
650 650 def update_ondisk_manifest(repo):
651 651 """update the clonebundle manifest with latest url"""
652 652 with repo.clonebundles_lock():
653 653 bundles = read_auto_gen(repo)
654 654
655 655 per_types = {}
656 656 for b in bundles:
657 657 if not (b.ready and b.valid_for(repo)):
658 658 continue
659 659 current = per_types.get(b.bundle_type)
660 660 if current is not None and current.revs >= b.revs:
661 661 continue
662 662 per_types[b.bundle_type] = b
663 663 manifest = generate_manifest(per_types.values())
664 664 with repo.vfs(
665 665 bundlecaches.CB_MANIFEST_FILE, mode=b"wb", atomictemp=True
666 666 ) as f:
667 667 f.write(manifest)
668 668
669 669
670 670 def update_bundle_list(repo, new_bundles=(), del_bundles=()):
671 671 """modify the repo's AUTO_GEN_FILE
672 672
673 673 This method also regenerates the clone bundle manifest when needed"""
674 674 with repo.clonebundles_lock():
675 675 bundles = read_auto_gen(repo)
676 676 if del_bundles:
677 677 bundles = [b for b in bundles if b not in del_bundles]
678 678 new_bundles = [b for b in new_bundles if b not in bundles]
679 679 bundles.extend(new_bundles)
680 680 write_auto_gen(repo, bundles)
681 681 all_changed = []
682 682 all_changed.extend(new_bundles)
683 683 all_changed.extend(del_bundles)
684 684 if any(b.ready for b in all_changed):
685 685 update_ondisk_manifest(repo)
686 686
687 687
688 688 def cleanup_tmp_bundle(repo, target):
689 689 """remove a GeneratingBundle file and entry"""
690 690 assert not target.ready
691 691 with repo.clonebundles_lock():
692 692 repo.vfs.tryunlink(target.filepath)
693 693 update_bundle_list(repo, del_bundles=[target])
694 694
695 695
696 696 def finalize_one_bundle(repo, target):
697 697 """upload a generated bundle and advertise it in the clonebundles.manifest"""
698 698 with repo.clonebundles_lock():
699 699 bundles = read_auto_gen(repo)
700 700 if target in bundles and target.valid_for(repo):
701 701 result = upload_bundle(repo, target)
702 702 update_bundle_list(repo, new_bundles=[result])
703 703 cleanup_tmp_bundle(repo, target)
704 704
705 705
706 706 def find_outdated_bundles(repo, bundles):
707 707 """finds outdated bundles"""
708 708 olds = []
709 709 per_types = {}
710 710 for b in bundles:
711 711 if not b.valid_for(repo):
712 712 olds.append(b)
713 713 continue
714 714 l = per_types.setdefault(b.bundle_type, [])
715 715 l.append(b)
716 716 for key in sorted(per_types):
717 717 all = per_types[key]
718 718 if len(all) > 1:
719 719 all.sort(key=lambda b: b.revs, reverse=True)
720 720 olds.extend(all[1:])
721 721 return olds
722 722
723 723
724 724 def collect_garbage(repo):
725 725 """finds outdated bundles and get them deleted"""
726 726 with repo.clonebundles_lock():
727 727 bundles = read_auto_gen(repo)
728 728 olds = find_outdated_bundles(repo, bundles)
729 729 for o in olds:
730 730 delete_bundle(repo, o)
731 731 update_bundle_list(repo, del_bundles=olds)
732 732
733 733
734 734 def upload_bundle(repo, bundle):
735 735 """upload the result of a GeneratingBundle and return a GeneratedBundle
736 736
737 737 The upload is done using the `clone-bundles.upload-command`
738 738 """
739 739 cmd = repo.ui.config(b'clone-bundles', b'upload-command')
740 740 url = repo.ui.config(b'clone-bundles', b'url-template')
741 741 basename = repo.vfs.basename(bundle.filepath)
742 742 filepath = procutil.shellquote(bundle.filepath)
743 743 variables = {
744 744 b'HGCB_BUNDLE_PATH': filepath,
745 745 b'HGCB_BUNDLE_BASENAME': basename,
746 746 }
747 747 env = procutil.shellenviron(environ=variables)
748 748 ret = repo.ui.system(cmd, environ=env)
749 749 if ret:
750 750 raise error.Abort(b"command returned status %d: %s" % (ret, cmd))
751 751 url = (
752 752 url.decode('utf8')
753 753 .format(basename=basename.decode('utf8'))
754 754 .encode('utf8')
755 755 )
756 756 return bundle.uploaded(url, basename)
757 757
758 758
759 759 def delete_bundle(repo, bundle):
760 760 """delete a bundle from storage"""
761 761 assert bundle.ready
762 762 msg = b'clone-bundles: deleting bundle %s\n'
763 763 msg %= bundle.basename
764 764 if repo.ui.configbool(b'devel', b'debug.clonebundles'):
765 765 repo.ui.write(msg)
766 766 else:
767 767 repo.ui.debug(msg)
768 768
769 769 cmd = repo.ui.config(b'clone-bundles', b'delete-command')
770 770 variables = {
771 771 b'HGCB_BUNDLE_URL': bundle.file_url,
772 772 b'HGCB_BASENAME': bundle.basename,
773 773 }
774 774 env = procutil.shellenviron(environ=variables)
775 775 ret = repo.ui.system(cmd, environ=env)
776 776 if ret:
777 777 raise error.Abort(b"command returned status %d: %s" % (ret, cmd))
778 778
779 779
780 780 def auto_bundle_needed_actions(repo, bundles, op_id):
781 781 """find the list of bundles that need action
782 782
783 783 returns a list of RequestedBundle objects that need to be generated and
784 784 uploaded."""
785 785 create_bundles = []
786 786 delete_bundles = []
787 787 repo = repo.filtered(b"immutable")
788 788 targets = repo.ui.configlist(b'clone-bundles', b'auto-generate.formats')
789 789 ratio = float(
790 790 repo.ui.config(b'clone-bundles', b'trigger.below-bundled-ratio')
791 791 )
792 792 abs_revs = repo.ui.configint(b'clone-bundles', b'trigger.revs')
793 793 revs = len(repo.changelog)
794 794 generic_data = {
795 795 'revs': revs,
796 796 'head_revs': repo.changelog.headrevs(),
797 797 'tip_rev': repo.changelog.tiprev(),
798 798 'tip_node': node.hex(repo.changelog.tip()),
799 799 'op_id': op_id,
800 800 }
801 801 for t in targets:
802 802 if new_bundle_needed(repo, bundles, ratio, abs_revs, t, revs):
803 803 data = generic_data.copy()
804 804 data['bundle_type'] = t
805 805 b = RequestedBundle(**data)
806 806 create_bundles.append(b)
807 807 delete_bundles.extend(find_outdated_bundles(repo, bundles))
808 808 return create_bundles, delete_bundles
809 809
810 810
811 811 def new_bundle_needed(repo, bundles, ratio, abs_revs, bundle_type, revs):
812 812 """consider the current cached content and trigger new bundles if needed"""
813 813 threshold = max((revs * ratio), (revs - abs_revs))
814 814 for b in bundles:
815 815 if not b.valid_for(repo) or b.bundle_type != bundle_type:
816 816 continue
817 817 if b.revs > threshold:
818 818 return False
819 819 return True
820 820
821 821
822 822 def start_one_bundle(repo, bundle):
823 823 """start the generation of a single bundle file
824 824
825 825 the `bundle` argument should be a RequestedBundle object.
826 826
827 827 This data is passed to the `debugmakeclonebundles` "as is".
828 828 """
829 829 data = util.pickle.dumps(bundle)
830 830 cmd = [procutil.hgexecutable(), b'--cwd', repo.path, INTERNAL_CMD]
831 831 env = procutil.shellenviron()
832 832 msg = b'clone-bundles: starting bundle generation: %s\n'
833 833 stdout = None
834 834 stderr = None
835 835 waits = []
836 836 record_wait = None
837 837 if repo.ui.configbool(b'devel', b'debug.clonebundles'):
838 838 stdout = procutil.stdout
839 839 stderr = procutil.stderr
840 840 repo.ui.write(msg % bundle.bundle_type)
841 841 record_wait = waits.append
842 842 else:
843 843 repo.ui.debug(msg % bundle.bundle_type)
844 844 bg = procutil.runbgcommand
845 845 bg(
846 846 cmd,
847 847 env,
848 848 stdin_bytes=data,
849 849 stdout=stdout,
850 850 stderr=stderr,
851 851 record_wait=record_wait,
852 852 )
853 853 for f in waits:
854 854 f()
855 855
856 856
857 857 INTERNAL_CMD = b'debug::internal-make-clone-bundles'
858 858
859 859
860 860 @command(INTERNAL_CMD, [], b'')
861 861 def debugmakeclonebundles(ui, repo):
862 862 """Internal command to auto-generate debug bundles"""
863 863 requested_bundle = util.pickle.load(procutil.stdin)
864 864 procutil.stdin.close()
865 865
866 866 collect_garbage(repo)
867 867
868 868 fname = requested_bundle.suggested_filename
869 869 fpath = repo.vfs.makedirs(b'tmp-bundles')
870 870 fpath = repo.vfs.join(b'tmp-bundles', fname)
871 871 bundle = requested_bundle.generating(fpath)
872 872 update_bundle_list(repo, new_bundles=[bundle])
873 873
874 874 requested_bundle.generate_bundle(repo, fpath)
875 875
876 876 repo.invalidate()
877 877 finalize_one_bundle(repo, bundle)
878 878
879 879
880 880 def make_auto_bundler(source_repo):
881 881 reporef = weakref.ref(source_repo)
882 882
883 883 def autobundle(tr):
884 884 repo = reporef()
885 885 assert repo is not None
886 886 bundles = read_auto_gen(repo)
887 887 new, __ = auto_bundle_needed_actions(repo, bundles, b"%d_txn" % id(tr))
888 888 for data in new:
889 889 start_one_bundle(repo, data)
890 890 return None
891 891
892 892 return autobundle
893 893
894 894
895 895 def reposetup(ui, repo):
896 896 """install the two pieces needed for automatic clonebundle generation
897 897
898 898 - add a "post-close" hook that fires bundling when needed
899 899 - introduce a clone-bundle lock to let multiple processes meddle with the
900 900 state files.
901 901 """
902 902 if not repo.local():
903 903 return
904 904
905 905 class autobundlesrepo(repo.__class__):
906 906 def transaction(self, *args, **kwargs):
907 907 tr = super(autobundlesrepo, self).transaction(*args, **kwargs)
908 908 enabled = repo.ui.configbool(
909 909 b'clone-bundles',
910 910 b'auto-generate.on-change',
911 911 )
912 912 targets = repo.ui.configlist(
913 913 b'clone-bundles', b'auto-generate.formats'
914 914 )
915 915 if enabled and targets:
916 916 tr.addpostclose(CAT_POSTCLOSE, make_auto_bundler(self))
917 917 return tr
918 918
919 919 @localrepo.unfilteredmethod
920 920 def clonebundles_lock(self, wait=True):
921 921 '''Lock the repository file related to clone bundles'''
922 922 if not util.safehasattr(self, '_cb_lock_ref'):
923 923 self._cb_lock_ref = None
924 924 l = self._currentlock(self._cb_lock_ref)
925 925 if l is not None:
926 926 l.lock()
927 927 return l
928 928
929 929 l = self._lock(
930 930 vfs=self.vfs,
931 931 lockname=b"clonebundleslock",
932 932 wait=wait,
933 933 releasefn=None,
934 934 acquirefn=None,
935 935 desc=_(b'repository %s') % self.origroot,
936 936 )
937 937 self._cb_lock_ref = weakref.ref(l)
938 938 return l
939 939
940 940 repo._wlockfreeprefix.add(AUTO_GEN_FILE)
941 941 repo._wlockfreeprefix.add(bundlecaches.CB_MANIFEST_FILE)
942 942 repo.__class__ = autobundlesrepo
943 943
944 944
945 @command(b'admin::clone-bundles-refresh', [], b'')
946 def cmd_admin_clone_bundles_refresh(ui, repo: localrepo.localrepository):
945 @command(
946 b'admin::clone-bundles-refresh',
947 [
948 (
949 b'',
950 b'background',
951 False,
952 _(b'start bundle generation in the background'),
953 ),
954 ],
955 b'',
956 )
957 def cmd_admin_clone_bundles_refresh(
958 ui,
959 repo: localrepo.localrepository,
960 background=False,
961 ):
947 962 """generate clone bundles according to the configuration
948 963
949 964 This runs the logic for automatic generation, removing outdated bundles and
950 965 generating new ones if necessary. See :hg:`help -e clone-bundles` for
951 966 details about how to configure this feature.
952 967 """
953 968 debug = repo.ui.configbool(b'devel', b'debug.clonebundles')
954 969 bundles = read_auto_gen(repo)
955 970 op_id = b"%d_acbr" % os.getpid()
956 971 create, delete = auto_bundle_needed_actions(repo, bundles, op_id)
957 972
958 # we clean up outdated bundle before generating new one to keep the last
959 # two version of the bundle around for a while and avoid having to deal
960 # client that just got served a manifest.
961 for o in delete:
962 delete_bundle(repo, o)
963 update_bundle_list(repo, del_bundles=delete)
973 # if some bundles are scheduled for creation in the background, they will
974 # deal with garbage collection too, so no need to synchroniously do it.
975 #
976 # However if no bundles are scheduled for creation, we need to explicitly do
977 # it here.
978 if not (background and create):
979 # we clean up outdated bundles before generating new ones to keep the
980 # last two versions of the bundle around for a while and avoid having to
981 # deal with clients that just got served a manifest.
982 for o in delete:
983 delete_bundle(repo, o)
984 update_bundle_list(repo, del_bundles=delete)
964 985
965 986 if create:
966 987 fpath = repo.vfs.makedirs(b'tmp-bundles')
967 for requested_bundle in create:
968 if debug:
969 msg = b'clone-bundles: starting bundle generation: %s\n'
970 repo.ui.write(msg % requested_bundle.bundle_type)
971 fname = requested_bundle.suggested_filename
972 fpath = repo.vfs.join(b'tmp-bundles', fname)
973 generating_bundle = requested_bundle.generating(fpath)
974 update_bundle_list(repo, new_bundles=[generating_bundle])
975 requested_bundle.generate_bundle(repo, fpath)
976 result = upload_bundle(repo, generating_bundle)
977 update_bundle_list(repo, new_bundles=[result])
978 update_ondisk_manifest(repo)
979 cleanup_tmp_bundle(repo, generating_bundle)
988
989 if background:
990 for requested_bundle in create:
991 start_one_bundle(repo, requested_bundle)
992 else:
993 for requested_bundle in create:
994 if debug:
995 msg = b'clone-bundles: starting bundle generation: %s\n'
996 repo.ui.write(msg % requested_bundle.bundle_type)
997 fname = requested_bundle.suggested_filename
998 fpath = repo.vfs.join(b'tmp-bundles', fname)
999 generating_bundle = requested_bundle.generating(fpath)
1000 update_bundle_list(repo, new_bundles=[generating_bundle])
1001 requested_bundle.generate_bundle(repo, fpath)
1002 result = upload_bundle(repo, generating_bundle)
1003 update_bundle_list(repo, new_bundles=[result])
1004 update_ondisk_manifest(repo)
1005 cleanup_tmp_bundle(repo, generating_bundle)
980 1006
981 1007
982 1008 @command(b'admin::clone-bundles-clear', [], b'')
983 1009 def cmd_admin_clone_bundles_clear(ui, repo: localrepo.localrepository):
984 1010 """remove existing clone bundle caches
985 1011
986 1012 See `hg help admin::clone-bundles-refresh` for details on how to regenerate
987 1013 them.
988 1014
989 1015 This command will only affect bundles currently available, it will not
990 1016 affect bundles being asynchronously generated.
991 1017 """
992 1018 bundles = read_auto_gen(repo)
993 1019 delete = [b for b in bundles if b.ready]
994 1020 for o in delete:
995 1021 delete_bundle(repo, o)
996 1022 update_bundle_list(repo, del_bundles=delete)
@@ -1,337 +1,360 b''
1 1
2 2 #require no-reposimplestore no-chg
3 3
4 4 initial setup
5 5
6 6 $ hg init server
7 7 $ cat >> server/.hg/hgrc << EOF
8 8 > [extensions]
9 9 > clonebundles =
10 10 >
11 11 > [clone-bundles]
12 12 > auto-generate.on-change = yes
13 13 > auto-generate.formats = v2
14 14 > upload-command = cp "\$HGCB_BUNDLE_PATH" "$TESTTMP"/final-upload/
15 15 > delete-command = rm -f "$TESTTMP/final-upload/\$HGCB_BASENAME"
16 16 > url-template = file://$TESTTMP/final-upload/{basename}
17 17 >
18 18 > [devel]
19 19 > debug.clonebundles=yes
20 20 > EOF
21 21
22 22 $ mkdir final-upload
23 23 $ hg clone server client
24 24 updating to branch default
25 25 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
26 26 $ cd client
27 27
28 28 Test bundles are generated on push
29 29 ==================================
30 30
31 31 $ touch foo
32 32 $ hg -q commit -A -m 'add foo'
33 33 $ touch bar
34 34 $ hg -q commit -A -m 'add bar'
35 35 $ hg push
36 36 pushing to $TESTTMP/server
37 37 searching for changes
38 38 adding changesets
39 39 adding manifests
40 40 adding file changes
41 41 2 changesets found
42 42 added 2 changesets with 2 changes to 2 files
43 43 clone-bundles: starting bundle generation: v2
44 44 $ cat ../server/.hg/clonebundles.manifest
45 45 file:/*/$TESTTMP/final-upload/full-v2-2_revs-aaff8d2ffbbf_tip-*_txn.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
46 46 $ ls -1 ../final-upload
47 47 full-v2-2_revs-aaff8d2ffbbf_tip-*_txn.hg (glob)
48 48 $ ls -1 ../server/.hg/tmp-bundles
49 49
50 50 Newer bundles are generated with more pushes
51 51 --------------------------------------------
52 52
53 53 $ touch baz
54 54 $ hg -q commit -A -m 'add baz'
55 55 $ touch buz
56 56 $ hg -q commit -A -m 'add buz'
57 57 $ hg push
58 58 pushing to $TESTTMP/server
59 59 searching for changes
60 60 adding changesets
61 61 adding manifests
62 62 adding file changes
63 63 4 changesets found
64 64 added 2 changesets with 2 changes to 2 files
65 65 clone-bundles: starting bundle generation: v2
66 66
67 67 $ cat ../server/.hg/clonebundles.manifest
68 68 file:/*/$TESTTMP/final-upload/full-v2-4_revs-6427147b985a_tip-*_txn.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
69 69 $ ls -1 ../final-upload
70 70 full-v2-2_revs-aaff8d2ffbbf_tip-*_txn.hg (glob)
71 71 full-v2-4_revs-6427147b985a_tip-*_txn.hg (glob)
72 72 $ ls -1 ../server/.hg/tmp-bundles
73 73
74 74 Older bundles are cleaned up with more pushes
75 75 ---------------------------------------------
76 76
77 77 $ touch faz
78 78 $ hg -q commit -A -m 'add faz'
79 79 $ touch fuz
80 80 $ hg -q commit -A -m 'add fuz'
81 81 $ hg push
82 82 pushing to $TESTTMP/server
83 83 searching for changes
84 84 adding changesets
85 85 adding manifests
86 86 adding file changes
87 87 clone-bundles: deleting bundle full-v2-2_revs-aaff8d2ffbbf_tip-*_txn.hg (glob)
88 88 6 changesets found
89 89 added 2 changesets with 2 changes to 2 files
90 90 clone-bundles: starting bundle generation: v2
91 91
92 92 $ cat ../server/.hg/clonebundles.manifest
93 93 file:/*/$TESTTMP/final-upload/full-v2-6_revs-b1010e95ea00_tip-*_txn.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
94 94 $ ls -1 ../final-upload
95 95 full-v2-4_revs-6427147b985a_tip-*_txn.hg (glob)
96 96 full-v2-6_revs-b1010e95ea00_tip-*_txn.hg (glob)
97 97 $ ls -1 ../server/.hg/tmp-bundles
98 98
99 99 Test conditions to get them generated
100 100 =====================================
101 101
102 102 Check ratio
103 103
104 104 $ cat >> ../server/.hg/hgrc << EOF
105 105 > [clone-bundles]
106 106 > trigger.below-bundled-ratio = 0.5
107 107 > EOF
108 108 $ touch far
109 109 $ hg -q commit -A -m 'add far'
110 110 $ hg push
111 111 pushing to $TESTTMP/server
112 112 searching for changes
113 113 adding changesets
114 114 adding manifests
115 115 adding file changes
116 116 added 1 changesets with 1 changes to 1 files
117 117 $ cat ../server/.hg/clonebundles.manifest
118 118 file:/*/$TESTTMP/final-upload/full-v2-6_revs-b1010e95ea00_tip-*_txn.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
119 119 $ ls -1 ../final-upload
120 120 full-v2-4_revs-6427147b985a_tip-*_txn.hg (glob)
121 121 full-v2-6_revs-b1010e95ea00_tip-*_txn.hg (glob)
122 122 $ ls -1 ../server/.hg/tmp-bundles
123 123
124 124 Check absolute number of revisions
125 125
126 126 $ cat >> ../server/.hg/hgrc << EOF
127 127 > [clone-bundles]
128 128 > trigger.revs = 2
129 129 > EOF
130 130 $ touch bur
131 131 $ hg -q commit -A -m 'add bur'
132 132 $ hg push
133 133 pushing to $TESTTMP/server
134 134 searching for changes
135 135 adding changesets
136 136 adding manifests
137 137 adding file changes
138 138 clone-bundles: deleting bundle full-v2-4_revs-6427147b985a_tip-*_txn.hg (glob)
139 139 8 changesets found
140 140 added 1 changesets with 1 changes to 1 files
141 141 clone-bundles: starting bundle generation: v2
142 142 $ cat ../server/.hg/clonebundles.manifest
143 143 file:/*/$TESTTMP/final-upload/full-v2-8_revs-8353e8af1306_tip-*_txn.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
144 144 $ ls -1 ../final-upload
145 145 full-v2-6_revs-b1010e95ea00_tip-*_txn.hg (glob)
146 146 full-v2-8_revs-8353e8af1306_tip-*_txn.hg (glob)
147 147 $ ls -1 ../server/.hg/tmp-bundles
148 148
149 149 (that one would not generate new bundles)
150 150
151 151 $ touch tur
152 152 $ hg -q commit -A -m 'add tur'
153 153 $ hg push
154 154 pushing to $TESTTMP/server
155 155 searching for changes
156 156 adding changesets
157 157 adding manifests
158 158 adding file changes
159 159 added 1 changesets with 1 changes to 1 files
160 160 $ cat ../server/.hg/clonebundles.manifest
161 161 file:/*/$TESTTMP/final-upload/full-v2-8_revs-8353e8af1306_tip-*_txn.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
162 162 $ ls -1 ../final-upload
163 163 full-v2-6_revs-b1010e95ea00_tip-*_txn.hg (glob)
164 164 full-v2-8_revs-8353e8af1306_tip-*_txn.hg (glob)
165 165 $ ls -1 ../server/.hg/tmp-bundles
166 166
167 167 Test generation through the dedicated command
168 168 =============================================
169 169
170 170 $ cat >> ../server/.hg/hgrc << EOF
171 171 > [clone-bundles]
172 172 > auto-generate.on-change = no
173 173 > EOF
174 174
175 175 Check the command can generate content when needed
176 176 --------------------------------------------------
177 177
178 178 Do a push that makes the condition fulfilled,
179 179 Yet it should not automatically generate a bundle with
180 180 "auto-generate.on-change" not set.
181 181
182 182 $ touch quoi
183 183 $ hg -q commit -A -m 'add quoi'
184 184
185 185 $ pre_push_manifest=`cat ../server/.hg/clonebundles.manifest|f --sha256 | sed 's/.*=//' | cat`
186 186 $ pre_push_upload=`ls -1 ../final-upload|f --sha256 | sed 's/.*=//' | cat`
187 187 $ ls -1 ../server/.hg/tmp-bundles
188 188
189 189 $ hg push
190 190 pushing to $TESTTMP/server
191 191 searching for changes
192 192 adding changesets
193 193 adding manifests
194 194 adding file changes
195 195 added 1 changesets with 1 changes to 1 files
196 196
197 197 $ post_push_manifest=`cat ../server/.hg/clonebundles.manifest|f --sha256 | sed 's/.*=//' | cat`
198 198 $ post_push_upload=`ls -1 ../final-upload|f --sha256 | sed 's/.*=//' | cat`
199 199 $ ls -1 ../server/.hg/tmp-bundles
200 200 $ test "$pre_push_manifest" = "$post_push_manifest"
201 201 $ test "$pre_push_upload" = "$post_push_upload"
202 202
203 203 Running the command should detect the stale bundles, and do the full automatic
204 204 generation logic.
205 205
206 206 $ hg -R ../server/ admin::clone-bundles-refresh
207 207 clone-bundles: deleting bundle full-v2-6_revs-b1010e95ea00_tip-*_txn.hg (glob)
208 208 clone-bundles: starting bundle generation: v2
209 209 10 changesets found
210 210 $ cat ../server/.hg/clonebundles.manifest
211 211 file:/*/$TESTTMP/final-upload/full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
212 212 $ ls -1 ../final-upload
213 213 full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg (glob)
214 214 full-v2-8_revs-8353e8af1306_tip-*_txn.hg (glob)
215 215 $ ls -1 ../server/.hg/tmp-bundles
216 216
217 217 Check the command cleans up older bundles when possible
218 218 -------------------------------------------------------
219 219
220 220 $ hg -R ../server/ admin::clone-bundles-refresh
221 221 clone-bundles: deleting bundle full-v2-8_revs-8353e8af1306_tip-*_txn.hg (glob)
222 222 $ cat ../server/.hg/clonebundles.manifest
223 223 file:/*/$TESTTMP/final-upload/full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
224 224 $ ls -1 ../final-upload
225 225 full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg (glob)
226 226 $ ls -1 ../server/.hg/tmp-bundles
227 227
228 228 Nothing is generated when the bundles are sufficiently up to date
229 229 -----------------------------------------------------------------
230 230
231 231 $ touch feur
232 232 $ hg -q commit -A -m 'add feur'
233 233
234 234 $ pre_push_manifest=`cat ../server/.hg/clonebundles.manifest|f --sha256 | sed 's/.*=//' | cat`
235 235 $ pre_push_upload=`ls -1 ../final-upload|f --sha256 | sed 's/.*=//' | cat`
236 236 $ ls -1 ../server/.hg/tmp-bundles
237 237
238 238 $ hg push
239 239 pushing to $TESTTMP/server
240 240 searching for changes
241 241 adding changesets
242 242 adding manifests
243 243 adding file changes
244 244 added 1 changesets with 1 changes to 1 files
245 245
246 246 $ post_push_manifest=`cat ../server/.hg/clonebundles.manifest|f --sha256 | sed 's/.*=//' | cat`
247 247 $ post_push_upload=`ls -1 ../final-upload|f --sha256 | sed 's/.*=//' | cat`
248 248 $ ls -1 ../server/.hg/tmp-bundles
249 249 $ test "$pre_push_manifest" = "$post_push_manifest"
250 250 $ test "$pre_push_upload" = "$post_push_upload"
251 251
252 252 $ hg -R ../server/ admin::clone-bundles-refresh
253 253
254 254 $ post_refresh_manifest=`cat ../server/.hg/clonebundles.manifest|f --sha256 | sed 's/.*=//' | cat`
255 255 $ post_refresh_upload=`ls -1 ../final-upload|f --sha256 | sed 's/.*=//' | cat`
256 256 $ ls -1 ../server/.hg/tmp-bundles
257 257 $ test "$pre_push_manifest" = "$post_refresh_manifest"
258 258 $ test "$pre_push_upload" = "$post_refresh_upload"
259 259
260 260 Test modification of configuration
261 261 ==================================
262 262
263 263 Testing that later runs adapt to configuration changes even if the repository is
264 264 unchanged.
265 265
266 266 adding more formats
267 267 -------------------
268 268
269 269 bundle for added formats should be generated
270 270
271 271 change configuration
272 272
273 273 $ cat >> ../server/.hg/hgrc << EOF
274 274 > [clone-bundles]
275 275 > auto-generate.formats = v1, v2
276 276 > EOF
277 277
278 278 refresh the bundles
279 279
280 280 $ hg -R ../server/ admin::clone-bundles-refresh
281 281 clone-bundles: starting bundle generation: v1
282 282 11 changesets found
283 283
284 284 the bundle for the "new" format should have been added
285 285
286 286 $ cat ../server/.hg/clonebundles.manifest
287 287 file:/*/$TESTTMP/final-upload/full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v1 REQUIRESNI=true (glob)
288 288 file:/*/$TESTTMP/final-upload/full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
289 289 $ ls -1 ../final-upload
290 290 full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
291 291 full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg (glob)
292 292 $ ls -1 ../server/.hg/tmp-bundles
293 293
294 294 Changing the ratio
295 295 ------------------
296 296
297 297 Changing the ratio to something that would have triggered a bundle during the last push.
298 298
299 299 $ cat >> ../server/.hg/hgrc << EOF
300 300 > [clone-bundles]
301 301 > trigger.below-bundled-ratio = 0.95
302 302 > EOF
303 303
304 304 refresh the bundles
305 305
306 306 $ hg -R ../server/ admin::clone-bundles-refresh
307 307 clone-bundles: starting bundle generation: v2
308 308 11 changesets found
309 309
310 310
311 311 the "outdated' bundle should be refreshed
312 312
313 313 $ cat ../server/.hg/clonebundles.manifest
314 314 file:/*/$TESTTMP/final-upload/full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v1 REQUIRESNI=true (glob)
315 315 file:/*/$TESTTMP/final-upload/full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
316 316 $ ls -1 ../final-upload
317 317 full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
318 318 full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg (glob)
319 319 full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
320 320 $ ls -1 ../server/.hg/tmp-bundles
321 321
322 322 Test more command options
323 323 =========================
324 324
325 325 bundle clearing
326 326 ---------------
327 327
328 328 $ hg -R ../server/ admin::clone-bundles-clear
329 329 clone-bundles: deleting bundle full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
330 330 clone-bundles: deleting bundle full-v2-10_revs-3b6f57f17d70_tip-*_acbr.hg (glob)
331 331 clone-bundles: deleting bundle full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
332 332
333 333 Nothing should remain
334 334
335 335 $ cat ../server/.hg/clonebundles.manifest
336 336 $ ls -1 ../final-upload
337 337 $ ls -1 ../server/.hg/tmp-bundles
338
339 background generation
340 ---------------------
341
342 generate bundle using background subprocess
343 (since we are in devel mode, the command will still wait for the background
344 process to end)
345
346 $ hg -R ../server/ admin::clone-bundles-refresh --background
347 11 changesets found
348 11 changesets found
349 clone-bundles: starting bundle generation: v1
350 clone-bundles: starting bundle generation: v2
351
352 bundles should have been generated
353
354 $ cat ../server/.hg/clonebundles.manifest
355 file:/*/$TESTTMP/final-upload/full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v1 REQUIRESNI=true (glob)
356 file:/*/$TESTTMP/final-upload/full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
357 $ ls -1 ../final-upload
358 full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
359 full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
360 $ ls -1 ../server/.hg/tmp-bundles
General Comments 0
You need to be logged in to leave comments. Login now