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