##// END OF EJS Templates
clonebundles: adds a auto-generate.serve-inline option...
marmoute -
r51604:40638610 default
parent child Browse files
Show More
@@ -1,1039 +1,1081 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 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 Automatic Inline serving
258 ........................
259
260 The simplest way to serve the generated bundle is through the Mercurial
261 protocol. However it is not the most efficient as request will still be served
262 by that main server. It is useful in case where authentication is complexe or
263 when an efficient mirror system is already in use anyway. See the `inline
264 clonebundles` section above for details about inline clonebundles
265
266 To automatically serve generated bundle through inline clonebundle, simply set
267 the following option::
268
269 auto-generate.serve-inline=yes
270
271 Enabling this option disable the managed upload and serving explained below.
272
257 273 Bundles Upload and Serving:
258 274 ...........................
259 275
276 This is the most efficient way to serve automatically generated clone bundles,
277 but requires some setup.
278
260 279 The generated bundles need to be made available to users through a "public" URL.
261 280 This should be donne through `clone-bundles.upload-command` configuration. The
262 281 value of this command should be a shell command. It will have access to the
263 282 bundle file path through the `$HGCB_BUNDLE_PATH` variable. And the expected
264 283 basename in the "public" URL is accessible at::
265 284
266 285 [clone-bundles]
267 286 upload-command=sftp put $HGCB_BUNDLE_PATH \
268 287 sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
269 288
270 289 If the file was already uploaded, the command must still succeed.
271 290
272 291 After upload, the file should be available at an url defined by
273 292 `clone-bundles.url-template`.
274 293
275 294 [clone-bundles]
276 295 url-template=https://bundles.host/cache/clone-bundles/{basename}
277 296
278 297 Old bundles cleanup:
279 298 ....................
280 299
281 300 When new bundles are generated, the older ones are no longer necessary and can
282 301 be removed from storage. This is done through the `clone-bundles.delete-command`
283 302 configuration. The command is given the url of the artifact to delete through
284 303 the `$HGCB_BUNDLE_URL` environment variable.
285 304
286 305 [clone-bundles]
287 306 delete-command=sftp rm sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
288 307
289 308 If the file was already deleted, the command must still succeed.
290 309 """
291 310
292 311
293 312 import os
294 313 import weakref
295 314
296 315 from mercurial.i18n import _
297 316
298 317 from mercurial import (
299 318 bundlecaches,
300 319 commands,
301 320 error,
302 321 extensions,
303 322 localrepo,
304 323 lock,
305 324 node,
306 325 registrar,
307 326 util,
308 327 wireprotov1server,
309 328 )
310 329
311 330
312 331 from mercurial.utils import (
313 332 procutil,
314 333 )
315 334
316 335 testedwith = b'ships-with-hg-core'
317 336
318 337
319 338 def capabilities(orig, repo, proto):
320 339 caps = orig(repo, proto)
321 340
322 341 # Only advertise if a manifest exists. This does add some I/O to requests.
323 342 # But this should be cheaper than a wasted network round trip due to
324 343 # missing file.
325 344 if repo.vfs.exists(bundlecaches.CB_MANIFEST_FILE):
326 345 caps.append(b'clonebundles')
327 346 caps.append(b'clonebundles_manifest')
328 347
329 348 return caps
330 349
331 350
332 351 def extsetup(ui):
333 352 extensions.wrapfunction(wireprotov1server, b'_capabilities', capabilities)
334 353
335 354
336 355 # logic for bundle auto-generation
337 356
338 357
339 358 configtable = {}
340 359 configitem = registrar.configitem(configtable)
341 360
342 361 cmdtable = {}
343 362 command = registrar.command(cmdtable)
344 363
345 364 configitem(b'clone-bundles', b'auto-generate.on-change', default=False)
346 365 configitem(b'clone-bundles', b'auto-generate.formats', default=list)
366 configitem(b'clone-bundles', b'auto-generate.serve-inline', default=False)
347 367 configitem(b'clone-bundles', b'trigger.below-bundled-ratio', default=0.95)
348 368 configitem(b'clone-bundles', b'trigger.revs', default=1000)
349 369
350 370 configitem(b'clone-bundles', b'upload-command', default=None)
351 371
352 372 configitem(b'clone-bundles', b'delete-command', default=None)
353 373
354 374 configitem(b'clone-bundles', b'url-template', default=None)
355 375
356 376 configitem(b'devel', b'debug.clonebundles', default=False)
357 377
358 378
359 379 # category for the post-close transaction hooks
360 380 CAT_POSTCLOSE = b"clonebundles-autobundles"
361 381
362 382 # template for bundle file names
363 383 BUNDLE_MASK = (
364 384 b"full-%(bundle_type)s-%(revs)d_revs-%(tip_short)s_tip-%(op_id)s.hg"
365 385 )
366 386
367 387
368 388 # file in .hg/ use to track clonebundles being auto-generated
369 389 AUTO_GEN_FILE = b'clonebundles.auto-gen'
370 390
371 391
372 392 class BundleBase(object):
373 393 """represents the core of properties that matters for us in a bundle
374 394
375 395 :bundle_type: the bundlespec (see hg help bundlespec)
376 396 :revs: the number of revisions in the repo at bundle creation time
377 397 :tip_rev: the rev-num of the tip revision
378 398 :tip_node: the node id of the tip-most revision in the bundle
379 399
380 400 :ready: True if the bundle is ready to be served
381 401 """
382 402
383 403 ready = False
384 404
385 405 def __init__(self, bundle_type, revs, tip_rev, tip_node):
386 406 self.bundle_type = bundle_type
387 407 self.revs = revs
388 408 self.tip_rev = tip_rev
389 409 self.tip_node = tip_node
390 410
391 411 def valid_for(self, repo):
392 412 """is this bundle applicable to the current repository
393 413
394 414 This is useful for detecting bundles made irrelevant by stripping.
395 415 """
396 416 tip_node = node.bin(self.tip_node)
397 417 return repo.changelog.index.get_rev(tip_node) == self.tip_rev
398 418
399 419 def __eq__(self, other):
400 420 left = (self.ready, self.bundle_type, self.tip_rev, self.tip_node)
401 421 right = (other.ready, other.bundle_type, other.tip_rev, other.tip_node)
402 422 return left == right
403 423
404 424 def __neq__(self, other):
405 425 return not self == other
406 426
407 427 def __cmp__(self, other):
408 428 if self == other:
409 429 return 0
410 430 return -1
411 431
412 432
413 433 class RequestedBundle(BundleBase):
414 434 """A bundle that should be generated.
415 435
416 436 Additional attributes compared to BundleBase
417 437 :heads: list of head revisions (as rev-num)
418 438 :op_id: a "unique" identifier for the operation triggering the change
419 439 """
420 440
421 441 def __init__(self, bundle_type, revs, tip_rev, tip_node, head_revs, op_id):
422 442 self.head_revs = head_revs
423 443 self.op_id = op_id
424 444 super(RequestedBundle, self).__init__(
425 445 bundle_type,
426 446 revs,
427 447 tip_rev,
428 448 tip_node,
429 449 )
430 450
431 451 @property
432 452 def suggested_filename(self):
433 453 """A filename that can be used for the generated bundle"""
434 454 data = {
435 455 b'bundle_type': self.bundle_type,
436 456 b'revs': self.revs,
437 457 b'heads': self.head_revs,
438 458 b'tip_rev': self.tip_rev,
439 459 b'tip_node': self.tip_node,
440 460 b'tip_short': self.tip_node[:12],
441 461 b'op_id': self.op_id,
442 462 }
443 463 return BUNDLE_MASK % data
444 464
445 465 def generate_bundle(self, repo, file_path):
446 466 """generate the bundle at `filepath`"""
447 467 commands.bundle(
448 468 repo.ui,
449 469 repo,
450 470 file_path,
451 471 base=[b"null"],
452 472 rev=self.head_revs,
453 473 type=self.bundle_type,
454 474 quiet=True,
455 475 )
456 476
457 477 def generating(self, file_path, hostname=None, pid=None):
458 478 """return a GeneratingBundle object from this object"""
459 479 if pid is None:
460 480 pid = os.getpid()
461 481 if hostname is None:
462 482 hostname = lock._getlockprefix()
463 483 return GeneratingBundle(
464 484 self.bundle_type,
465 485 self.revs,
466 486 self.tip_rev,
467 487 self.tip_node,
468 488 hostname,
469 489 pid,
470 490 file_path,
471 491 )
472 492
473 493
474 494 class GeneratingBundle(BundleBase):
475 495 """A bundle being generated
476 496
477 497 extra attributes compared to BundleBase:
478 498
479 499 :hostname: the hostname of the machine generating the bundle
480 500 :pid: the pid of the process generating the bundle
481 501 :filepath: the target filename of the bundle
482 502
483 503 These attributes exist to help detect stalled generation processes.
484 504 """
485 505
486 506 ready = False
487 507
488 508 def __init__(
489 509 self, bundle_type, revs, tip_rev, tip_node, hostname, pid, filepath
490 510 ):
491 511 self.hostname = hostname
492 512 self.pid = pid
493 513 self.filepath = filepath
494 514 super(GeneratingBundle, self).__init__(
495 515 bundle_type, revs, tip_rev, tip_node
496 516 )
497 517
498 518 @classmethod
499 519 def from_line(cls, line):
500 520 """create an object by deserializing a line from AUTO_GEN_FILE"""
501 521 assert line.startswith(b'PENDING-v1 ')
502 522 (
503 523 __,
504 524 bundle_type,
505 525 revs,
506 526 tip_rev,
507 527 tip_node,
508 528 hostname,
509 529 pid,
510 530 filepath,
511 531 ) = line.split()
512 532 hostname = util.urlreq.unquote(hostname)
513 533 filepath = util.urlreq.unquote(filepath)
514 534 revs = int(revs)
515 535 tip_rev = int(tip_rev)
516 536 pid = int(pid)
517 537 return cls(
518 538 bundle_type, revs, tip_rev, tip_node, hostname, pid, filepath
519 539 )
520 540
521 541 def to_line(self):
522 542 """serialize the object to include as a line in AUTO_GEN_FILE"""
523 543 templ = b"PENDING-v1 %s %d %d %s %s %d %s"
524 544 data = (
525 545 self.bundle_type,
526 546 self.revs,
527 547 self.tip_rev,
528 548 self.tip_node,
529 549 util.urlreq.quote(self.hostname),
530 550 self.pid,
531 551 util.urlreq.quote(self.filepath),
532 552 )
533 553 return templ % data
534 554
535 555 def __eq__(self, other):
536 556 if not super(GeneratingBundle, self).__eq__(other):
537 557 return False
538 558 left = (self.hostname, self.pid, self.filepath)
539 559 right = (other.hostname, other.pid, other.filepath)
540 560 return left == right
541 561
542 562 def uploaded(self, url, basename):
543 563 """return a GeneratedBundle from this object"""
544 564 return GeneratedBundle(
545 565 self.bundle_type,
546 566 self.revs,
547 567 self.tip_rev,
548 568 self.tip_node,
549 569 url,
550 570 basename,
551 571 )
552 572
553 573
554 574 class GeneratedBundle(BundleBase):
555 575 """A bundle that is done being generated and can be served
556 576
557 577 extra attributes compared to BundleBase:
558 578
559 579 :file_url: the url where the bundle is available.
560 580 :basename: the "basename" used to upload (useful for deletion)
561 581
562 582 These attributes exist to generate a bundle manifest
563 583 (.hg/pullbundles.manifest)
564 584 """
565 585
566 586 ready = True
567 587
568 588 def __init__(
569 589 self, bundle_type, revs, tip_rev, tip_node, file_url, basename
570 590 ):
571 591 self.file_url = file_url
572 592 self.basename = basename
573 593 super(GeneratedBundle, self).__init__(
574 594 bundle_type, revs, tip_rev, tip_node
575 595 )
576 596
577 597 @classmethod
578 598 def from_line(cls, line):
579 599 """create an object by deserializing a line from AUTO_GEN_FILE"""
580 600 assert line.startswith(b'DONE-v1 ')
581 601 (
582 602 __,
583 603 bundle_type,
584 604 revs,
585 605 tip_rev,
586 606 tip_node,
587 607 file_url,
588 608 basename,
589 609 ) = line.split()
590 610 revs = int(revs)
591 611 tip_rev = int(tip_rev)
592 612 file_url = util.urlreq.unquote(file_url)
593 613 return cls(bundle_type, revs, tip_rev, tip_node, file_url, basename)
594 614
595 615 def to_line(self):
596 616 """serialize the object to include as a line in AUTO_GEN_FILE"""
597 617 templ = b"DONE-v1 %s %d %d %s %s %s"
598 618 data = (
599 619 self.bundle_type,
600 620 self.revs,
601 621 self.tip_rev,
602 622 self.tip_node,
603 623 util.urlreq.quote(self.file_url),
604 624 self.basename,
605 625 )
606 626 return templ % data
607 627
608 628 def manifest_line(self):
609 629 """serialize the object to include as a line in pullbundles.manifest"""
610 630 templ = b"%s BUNDLESPEC=%s"
611 631 if self.file_url.startswith(b'http'):
612 632 templ += b" REQUIRESNI=true"
613 633 return templ % (self.file_url, self.bundle_type)
614 634
615 635 def __eq__(self, other):
616 636 if not super(GeneratedBundle, self).__eq__(other):
617 637 return False
618 638 return self.file_url == other.file_url
619 639
620 640
621 641 def parse_auto_gen(content):
622 642 """parse the AUTO_GEN_FILE to return a list of Bundle object"""
623 643 bundles = []
624 644 for line in content.splitlines():
625 645 if line.startswith(b'PENDING-v1 '):
626 646 bundles.append(GeneratingBundle.from_line(line))
627 647 elif line.startswith(b'DONE-v1 '):
628 648 bundles.append(GeneratedBundle.from_line(line))
629 649 return bundles
630 650
631 651
632 652 def dumps_auto_gen(bundles):
633 653 """serialize a list of Bundle as a AUTO_GEN_FILE content"""
634 654 lines = []
635 655 for b in bundles:
636 656 lines.append(b"%s\n" % b.to_line())
637 657 lines.sort()
638 658 return b"".join(lines)
639 659
640 660
641 661 def read_auto_gen(repo):
642 662 """read the AUTO_GEN_FILE for the <repo> a list of Bundle object"""
643 663 data = repo.vfs.tryread(AUTO_GEN_FILE)
644 664 if not data:
645 665 return []
646 666 return parse_auto_gen(data)
647 667
648 668
649 669 def write_auto_gen(repo, bundles):
650 670 """write a list of Bundle objects into the repo's AUTO_GEN_FILE"""
651 671 assert repo._cb_lock_ref is not None
652 672 data = dumps_auto_gen(bundles)
653 673 with repo.vfs(AUTO_GEN_FILE, mode=b'wb', atomictemp=True) as f:
654 674 f.write(data)
655 675
656 676
657 677 def generate_manifest(bundles):
658 678 """write a list of Bundle objects into the repo's AUTO_GEN_FILE"""
659 679 bundles = list(bundles)
660 680 bundles.sort(key=lambda b: b.bundle_type)
661 681 lines = []
662 682 for b in bundles:
663 683 lines.append(b"%s\n" % b.manifest_line())
664 684 return b"".join(lines)
665 685
666 686
667 687 def update_ondisk_manifest(repo):
668 688 """update the clonebundle manifest with latest url"""
669 689 with repo.clonebundles_lock():
670 690 bundles = read_auto_gen(repo)
671 691
672 692 per_types = {}
673 693 for b in bundles:
674 694 if not (b.ready and b.valid_for(repo)):
675 695 continue
676 696 current = per_types.get(b.bundle_type)
677 697 if current is not None and current.revs >= b.revs:
678 698 continue
679 699 per_types[b.bundle_type] = b
680 700 manifest = generate_manifest(per_types.values())
681 701 with repo.vfs(
682 702 bundlecaches.CB_MANIFEST_FILE, mode=b"wb", atomictemp=True
683 703 ) as f:
684 704 f.write(manifest)
685 705
686 706
687 707 def update_bundle_list(repo, new_bundles=(), del_bundles=()):
688 708 """modify the repo's AUTO_GEN_FILE
689 709
690 710 This method also regenerates the clone bundle manifest when needed"""
691 711 with repo.clonebundles_lock():
692 712 bundles = read_auto_gen(repo)
693 713 if del_bundles:
694 714 bundles = [b for b in bundles if b not in del_bundles]
695 715 new_bundles = [b for b in new_bundles if b not in bundles]
696 716 bundles.extend(new_bundles)
697 717 write_auto_gen(repo, bundles)
698 718 all_changed = []
699 719 all_changed.extend(new_bundles)
700 720 all_changed.extend(del_bundles)
701 721 if any(b.ready for b in all_changed):
702 722 update_ondisk_manifest(repo)
703 723
704 724
705 725 def cleanup_tmp_bundle(repo, target):
706 726 """remove a GeneratingBundle file and entry"""
707 727 assert not target.ready
708 728 with repo.clonebundles_lock():
709 729 repo.vfs.tryunlink(target.filepath)
710 730 update_bundle_list(repo, del_bundles=[target])
711 731
712 732
713 733 def finalize_one_bundle(repo, target):
714 734 """upload a generated bundle and advertise it in the clonebundles.manifest"""
715 735 with repo.clonebundles_lock():
716 736 bundles = read_auto_gen(repo)
717 737 if target in bundles and target.valid_for(repo):
718 738 result = upload_bundle(repo, target)
719 739 update_bundle_list(repo, new_bundles=[result])
720 740 cleanup_tmp_bundle(repo, target)
721 741
722 742
723 743 def find_outdated_bundles(repo, bundles):
724 744 """finds outdated bundles"""
725 745 olds = []
726 746 per_types = {}
727 747 for b in bundles:
728 748 if not b.valid_for(repo):
729 749 olds.append(b)
730 750 continue
731 751 l = per_types.setdefault(b.bundle_type, [])
732 752 l.append(b)
733 753 for key in sorted(per_types):
734 754 all = per_types[key]
735 755 if len(all) > 1:
736 756 all.sort(key=lambda b: b.revs, reverse=True)
737 757 olds.extend(all[1:])
738 758 return olds
739 759
740 760
741 761 def collect_garbage(repo):
742 762 """finds outdated bundles and get them deleted"""
743 763 with repo.clonebundles_lock():
744 764 bundles = read_auto_gen(repo)
745 765 olds = find_outdated_bundles(repo, bundles)
746 766 for o in olds:
747 767 delete_bundle(repo, o)
748 768 update_bundle_list(repo, del_bundles=olds)
749 769
750 770
751 771 def upload_bundle(repo, bundle):
752 772 """upload the result of a GeneratingBundle and return a GeneratedBundle
753 773
754 774 The upload is done using the `clone-bundles.upload-command`
755 775 """
776 inline = repo.ui.config(b'clone-bundles', b'auto-generate.serve-inline')
777 basename = repo.vfs.basename(bundle.filepath)
778 if inline:
779 dest_dir = repo.vfs.join(bundlecaches.BUNDLE_CACHE_DIR)
780 repo.vfs.makedirs(dest_dir)
781 dest = repo.vfs.join(dest_dir, basename)
782 util.copyfiles(bundle.filepath, dest, hardlink=True)
783 url = bundlecaches.CLONEBUNDLESCHEME + basename
784 return bundle.uploaded(url, basename)
785 else:
756 786 cmd = repo.ui.config(b'clone-bundles', b'upload-command')
757 787 url = repo.ui.config(b'clone-bundles', b'url-template')
758 basename = repo.vfs.basename(bundle.filepath)
759 788 filepath = procutil.shellquote(bundle.filepath)
760 789 variables = {
761 790 b'HGCB_BUNDLE_PATH': filepath,
762 791 b'HGCB_BUNDLE_BASENAME': basename,
763 792 }
764 793 env = procutil.shellenviron(environ=variables)
765 794 ret = repo.ui.system(cmd, environ=env)
766 795 if ret:
767 796 raise error.Abort(b"command returned status %d: %s" % (ret, cmd))
768 797 url = (
769 798 url.decode('utf8')
770 799 .format(basename=basename.decode('utf8'))
771 800 .encode('utf8')
772 801 )
773 802 return bundle.uploaded(url, basename)
774 803
775 804
776 805 def delete_bundle(repo, bundle):
777 806 """delete a bundle from storage"""
778 807 assert bundle.ready
808
809 inline = bundle.file_url.startswith(bundlecaches.CLONEBUNDLESCHEME)
810
811 if inline:
812 msg = b'clone-bundles: deleting inline bundle %s\n'
813 else:
779 814 msg = b'clone-bundles: deleting bundle %s\n'
780 815 msg %= bundle.basename
781 816 if repo.ui.configbool(b'devel', b'debug.clonebundles'):
782 817 repo.ui.write(msg)
783 818 else:
784 819 repo.ui.debug(msg)
785 820
821 if inline:
822 inline_path = repo.vfs.join(
823 bundlecaches.BUNDLE_CACHE_DIR,
824 bundle.basename,
825 )
826 util.tryunlink(inline_path)
827 else:
786 828 cmd = repo.ui.config(b'clone-bundles', b'delete-command')
787 829 variables = {
788 830 b'HGCB_BUNDLE_URL': bundle.file_url,
789 831 b'HGCB_BASENAME': bundle.basename,
790 832 }
791 833 env = procutil.shellenviron(environ=variables)
792 834 ret = repo.ui.system(cmd, environ=env)
793 835 if ret:
794 836 raise error.Abort(b"command returned status %d: %s" % (ret, cmd))
795 837
796 838
797 839 def auto_bundle_needed_actions(repo, bundles, op_id):
798 840 """find the list of bundles that need action
799 841
800 842 returns a list of RequestedBundle objects that need to be generated and
801 843 uploaded."""
802 844 create_bundles = []
803 845 delete_bundles = []
804 846 repo = repo.filtered(b"immutable")
805 847 targets = repo.ui.configlist(b'clone-bundles', b'auto-generate.formats')
806 848 ratio = float(
807 849 repo.ui.config(b'clone-bundles', b'trigger.below-bundled-ratio')
808 850 )
809 851 abs_revs = repo.ui.configint(b'clone-bundles', b'trigger.revs')
810 852 revs = len(repo.changelog)
811 853 generic_data = {
812 854 'revs': revs,
813 855 'head_revs': repo.changelog.headrevs(),
814 856 'tip_rev': repo.changelog.tiprev(),
815 857 'tip_node': node.hex(repo.changelog.tip()),
816 858 'op_id': op_id,
817 859 }
818 860 for t in targets:
819 861 if new_bundle_needed(repo, bundles, ratio, abs_revs, t, revs):
820 862 data = generic_data.copy()
821 863 data['bundle_type'] = t
822 864 b = RequestedBundle(**data)
823 865 create_bundles.append(b)
824 866 delete_bundles.extend(find_outdated_bundles(repo, bundles))
825 867 return create_bundles, delete_bundles
826 868
827 869
828 870 def new_bundle_needed(repo, bundles, ratio, abs_revs, bundle_type, revs):
829 871 """consider the current cached content and trigger new bundles if needed"""
830 872 threshold = max((revs * ratio), (revs - abs_revs))
831 873 for b in bundles:
832 874 if not b.valid_for(repo) or b.bundle_type != bundle_type:
833 875 continue
834 876 if b.revs > threshold:
835 877 return False
836 878 return True
837 879
838 880
839 881 def start_one_bundle(repo, bundle):
840 882 """start the generation of a single bundle file
841 883
842 884 the `bundle` argument should be a RequestedBundle object.
843 885
844 886 This data is passed to the `debugmakeclonebundles` "as is".
845 887 """
846 888 data = util.pickle.dumps(bundle)
847 889 cmd = [procutil.hgexecutable(), b'--cwd', repo.path, INTERNAL_CMD]
848 890 env = procutil.shellenviron()
849 891 msg = b'clone-bundles: starting bundle generation: %s\n'
850 892 stdout = None
851 893 stderr = None
852 894 waits = []
853 895 record_wait = None
854 896 if repo.ui.configbool(b'devel', b'debug.clonebundles'):
855 897 stdout = procutil.stdout
856 898 stderr = procutil.stderr
857 899 repo.ui.write(msg % bundle.bundle_type)
858 900 record_wait = waits.append
859 901 else:
860 902 repo.ui.debug(msg % bundle.bundle_type)
861 903 bg = procutil.runbgcommand
862 904 bg(
863 905 cmd,
864 906 env,
865 907 stdin_bytes=data,
866 908 stdout=stdout,
867 909 stderr=stderr,
868 910 record_wait=record_wait,
869 911 )
870 912 for f in waits:
871 913 f()
872 914
873 915
874 916 INTERNAL_CMD = b'debug::internal-make-clone-bundles'
875 917
876 918
877 919 @command(INTERNAL_CMD, [], b'')
878 920 def debugmakeclonebundles(ui, repo):
879 921 """Internal command to auto-generate debug bundles"""
880 922 requested_bundle = util.pickle.load(procutil.stdin)
881 923 procutil.stdin.close()
882 924
883 925 collect_garbage(repo)
884 926
885 927 fname = requested_bundle.suggested_filename
886 928 fpath = repo.vfs.makedirs(b'tmp-bundles')
887 929 fpath = repo.vfs.join(b'tmp-bundles', fname)
888 930 bundle = requested_bundle.generating(fpath)
889 931 update_bundle_list(repo, new_bundles=[bundle])
890 932
891 933 requested_bundle.generate_bundle(repo, fpath)
892 934
893 935 repo.invalidate()
894 936 finalize_one_bundle(repo, bundle)
895 937
896 938
897 939 def make_auto_bundler(source_repo):
898 940 reporef = weakref.ref(source_repo)
899 941
900 942 def autobundle(tr):
901 943 repo = reporef()
902 944 assert repo is not None
903 945 bundles = read_auto_gen(repo)
904 946 new, __ = auto_bundle_needed_actions(repo, bundles, b"%d_txn" % id(tr))
905 947 for data in new:
906 948 start_one_bundle(repo, data)
907 949 return None
908 950
909 951 return autobundle
910 952
911 953
912 954 def reposetup(ui, repo):
913 955 """install the two pieces needed for automatic clonebundle generation
914 956
915 957 - add a "post-close" hook that fires bundling when needed
916 958 - introduce a clone-bundle lock to let multiple processes meddle with the
917 959 state files.
918 960 """
919 961 if not repo.local():
920 962 return
921 963
922 964 class autobundlesrepo(repo.__class__):
923 965 def transaction(self, *args, **kwargs):
924 966 tr = super(autobundlesrepo, self).transaction(*args, **kwargs)
925 967 enabled = repo.ui.configbool(
926 968 b'clone-bundles',
927 969 b'auto-generate.on-change',
928 970 )
929 971 targets = repo.ui.configlist(
930 972 b'clone-bundles', b'auto-generate.formats'
931 973 )
932 974 if enabled and targets:
933 975 tr.addpostclose(CAT_POSTCLOSE, make_auto_bundler(self))
934 976 return tr
935 977
936 978 @localrepo.unfilteredmethod
937 979 def clonebundles_lock(self, wait=True):
938 980 '''Lock the repository file related to clone bundles'''
939 981 if not util.safehasattr(self, '_cb_lock_ref'):
940 982 self._cb_lock_ref = None
941 983 l = self._currentlock(self._cb_lock_ref)
942 984 if l is not None:
943 985 l.lock()
944 986 return l
945 987
946 988 l = self._lock(
947 989 vfs=self.vfs,
948 990 lockname=b"clonebundleslock",
949 991 wait=wait,
950 992 releasefn=None,
951 993 acquirefn=None,
952 994 desc=_(b'repository %s') % self.origroot,
953 995 )
954 996 self._cb_lock_ref = weakref.ref(l)
955 997 return l
956 998
957 999 repo._wlockfreeprefix.add(AUTO_GEN_FILE)
958 1000 repo._wlockfreeprefix.add(bundlecaches.CB_MANIFEST_FILE)
959 1001 repo.__class__ = autobundlesrepo
960 1002
961 1003
962 1004 @command(
963 1005 b'admin::clone-bundles-refresh',
964 1006 [
965 1007 (
966 1008 b'',
967 1009 b'background',
968 1010 False,
969 1011 _(b'start bundle generation in the background'),
970 1012 ),
971 1013 ],
972 1014 b'',
973 1015 )
974 1016 def cmd_admin_clone_bundles_refresh(
975 1017 ui,
976 1018 repo: localrepo.localrepository,
977 1019 background=False,
978 1020 ):
979 1021 """generate clone bundles according to the configuration
980 1022
981 1023 This runs the logic for automatic generation, removing outdated bundles and
982 1024 generating new ones if necessary. See :hg:`help -e clone-bundles` for
983 1025 details about how to configure this feature.
984 1026 """
985 1027 debug = repo.ui.configbool(b'devel', b'debug.clonebundles')
986 1028 bundles = read_auto_gen(repo)
987 1029 op_id = b"%d_acbr" % os.getpid()
988 1030 create, delete = auto_bundle_needed_actions(repo, bundles, op_id)
989 1031
990 1032 # if some bundles are scheduled for creation in the background, they will
991 1033 # deal with garbage collection too, so no need to synchroniously do it.
992 1034 #
993 1035 # However if no bundles are scheduled for creation, we need to explicitly do
994 1036 # it here.
995 1037 if not (background and create):
996 1038 # we clean up outdated bundles before generating new ones to keep the
997 1039 # last two versions of the bundle around for a while and avoid having to
998 1040 # deal with clients that just got served a manifest.
999 1041 for o in delete:
1000 1042 delete_bundle(repo, o)
1001 1043 update_bundle_list(repo, del_bundles=delete)
1002 1044
1003 1045 if create:
1004 1046 fpath = repo.vfs.makedirs(b'tmp-bundles')
1005 1047
1006 1048 if background:
1007 1049 for requested_bundle in create:
1008 1050 start_one_bundle(repo, requested_bundle)
1009 1051 else:
1010 1052 for requested_bundle in create:
1011 1053 if debug:
1012 1054 msg = b'clone-bundles: starting bundle generation: %s\n'
1013 1055 repo.ui.write(msg % requested_bundle.bundle_type)
1014 1056 fname = requested_bundle.suggested_filename
1015 1057 fpath = repo.vfs.join(b'tmp-bundles', fname)
1016 1058 generating_bundle = requested_bundle.generating(fpath)
1017 1059 update_bundle_list(repo, new_bundles=[generating_bundle])
1018 1060 requested_bundle.generate_bundle(repo, fpath)
1019 1061 result = upload_bundle(repo, generating_bundle)
1020 1062 update_bundle_list(repo, new_bundles=[result])
1021 1063 update_ondisk_manifest(repo)
1022 1064 cleanup_tmp_bundle(repo, generating_bundle)
1023 1065
1024 1066
1025 1067 @command(b'admin::clone-bundles-clear', [], b'')
1026 1068 def cmd_admin_clone_bundles_clear(ui, repo: localrepo.localrepository):
1027 1069 """remove existing clone bundle caches
1028 1070
1029 1071 See `hg help admin::clone-bundles-refresh` for details on how to regenerate
1030 1072 them.
1031 1073
1032 1074 This command will only affect bundles currently available, it will not
1033 1075 affect bundles being asynchronously generated.
1034 1076 """
1035 1077 bundles = read_auto_gen(repo)
1036 1078 delete = [b for b in bundles if b.ready]
1037 1079 for o in delete:
1038 1080 delete_bundle(repo, o)
1039 1081 update_bundle_list(repo, del_bundles=delete)
@@ -1,384 +1,476 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 (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 (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 (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 (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 (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 (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 (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 (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 (glob)
288 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 314 file:/*/$TESTTMP/final-upload/full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v1 (glob)
315 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 355 file:/*/$TESTTMP/final-upload/full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v1 (glob)
356 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 361
362 362 Test HTTP URL
363 363 =========================
364 364
365 365 $ hg -R ../server/ admin::clone-bundles-clear
366 366 clone-bundles: deleting bundle full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
367 367 clone-bundles: deleting bundle full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
368 368
369 369 $ cat >> ../server/.hg/hgrc << EOF
370 370 > [clone-bundles]
371 371 > url-template = https://example.com/final-upload/{basename}
372 372 > EOF
373 373 $ hg -R ../server/ admin::clone-bundles-refresh
374 374 clone-bundles: starting bundle generation: v1
375 375 11 changesets found
376 376 clone-bundles: starting bundle generation: v2
377 377 11 changesets found
378 378
379 379
380 380 bundles should have been generated with the SNIREQUIRED option
381 381
382 382 $ cat ../server/.hg/clonebundles.manifest
383 383 https://example.com/final-upload/full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v1 REQUIRESNI=true (glob)
384 384 https://example.com/final-upload/full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v2 REQUIRESNI=true (glob)
385
386 Test serving them through inline-clone bundle
387 =============================================
388
389 $ cat >> ../server/.hg/hgrc << EOF
390 > [clone-bundles]
391 > auto-generate.serve-inline=yes
392 > EOF
393 $ hg -R ../server/ admin::clone-bundles-clear
394 clone-bundles: deleting bundle full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
395 clone-bundles: deleting bundle full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
396
397 initial generation
398 ------------------
399
400
401 $ hg -R ../server/ admin::clone-bundles-refresh
402 clone-bundles: starting bundle generation: v1
403 11 changesets found
404 clone-bundles: starting bundle generation: v2
405 11 changesets found
406 $ cat ../server/.hg/clonebundles.manifest
407 peer-bundle-cache://full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v1 (glob)
408 peer-bundle-cache://full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg BUNDLESPEC=v2 (glob)
409 $ ls -1 ../server/.hg/bundle-cache
410 full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
411 full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
412 $ ls -1 ../final-upload
413
414 Regeneration eventually cleanup the old ones
415 --------------------------------------------
416
417 create more content
418 $ touch voit
419 $ hg -q commit -A -m 'add voit'
420 $ touch ar
421 $ hg -q commit -A -m 'add ar'
422 $ hg push
423 pushing to $TESTTMP/server
424 searching for changes
425 adding changesets
426 adding manifests
427 adding file changes
428 added 2 changesets with 2 changes to 2 files
429
430 check first regeneration
431
432 $ hg -R ../server/ admin::clone-bundles-refresh
433 clone-bundles: starting bundle generation: v1
434 13 changesets found
435 clone-bundles: starting bundle generation: v2
436 13 changesets found
437 $ cat ../server/.hg/clonebundles.manifest
438 peer-bundle-cache://full-v1-13_revs-8a81f9be54ea_tip-*_acbr.hg BUNDLESPEC=v1 (glob)
439 peer-bundle-cache://full-v2-13_revs-8a81f9be54ea_tip-*_acbr.hg BUNDLESPEC=v2 (glob)
440 $ ls -1 ../server/.hg/bundle-cache
441 full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
442 full-v1-13_revs-8a81f9be54ea_tip-*_acbr.hg (glob)
443 full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
444 full-v2-13_revs-8a81f9be54ea_tip-*_acbr.hg (glob)
445 $ ls -1 ../final-upload
446
447 check first regeneration (should cleanup the one before that last)
448
449 $ touch "investi"
450 $ hg -q commit -A -m 'add investi'
451 $ touch "lesgisla"
452 $ hg -q commit -A -m 'add lesgisla'
453 $ hg push
454 pushing to $TESTTMP/server
455 searching for changes
456 adding changesets
457 adding manifests
458 adding file changes
459 added 2 changesets with 2 changes to 2 files
460
461 $ hg -R ../server/ admin::clone-bundles-refresh
462 clone-bundles: deleting inline bundle full-v1-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
463 clone-bundles: deleting inline bundle full-v2-11_revs-4226b1cd5fda_tip-*_acbr.hg (glob)
464 clone-bundles: starting bundle generation: v1
465 15 changesets found
466 clone-bundles: starting bundle generation: v2
467 15 changesets found
468 $ cat ../server/.hg/clonebundles.manifest
469 peer-bundle-cache://full-v1-15_revs-17615b3984c2_tip-*_acbr.hg BUNDLESPEC=v1 (glob)
470 peer-bundle-cache://full-v2-15_revs-17615b3984c2_tip-*_acbr.hg BUNDLESPEC=v2 (glob)
471 $ ls -1 ../server/.hg/bundle-cache
472 full-v1-13_revs-8a81f9be54ea_tip-*_acbr.hg (glob)
473 full-v1-15_revs-17615b3984c2_tip-*_acbr.hg (glob)
474 full-v2-13_revs-8a81f9be54ea_tip-*_acbr.hg (glob)
475 full-v2-15_revs-17615b3984c2_tip-*_acbr.hg (glob)
476 $ ls -1 ../final-upload
General Comments 0
You need to be logged in to leave comments. Login now