Spaces:
Running
Running
Bundle URIs | |
=========== | |
Git bundles are files that store a pack-file along with some extra metadata, | |
including a set of refs and a (possibly empty) set of necessary commits. See | |
linkgit:git-bundle[1] and linkgit:gitformat-bundle[5] for more information. | |
Bundle URIs are locations where Git can download one or more bundles in | |
order to bootstrap the object database in advance of fetching the remaining | |
objects from a remote. | |
One goal is to speed up clones and fetches for users with poor network | |
connectivity to the origin server. Another benefit is to allow heavy users, | |
such as CI build farms, to use local resources for the majority of Git data | |
and thereby reducing the load on the origin server. | |
To enable the bundle URI feature, users can specify a bundle URI using | |
command-line options or the origin server can advertise one or more URIs | |
via a protocol v2 capability. | |
Design Goals | |
------------ | |
The bundle URI standard aims to be flexible enough to satisfy multiple | |
workloads. The bundle provider and the Git client have several choices in | |
how they create and consume bundle URIs. | |
* Bundles can have whatever name the server desires. This name could refer | |
to immutable data by using a hash of the bundle contents. However, this | |
means that a new URI will be needed after every update of the content. | |
This might be acceptable if the server is advertising the URI (and the | |
server is aware of new bundles being generated) but would not be | |
ergonomic for users using the command line option. | |
* The bundles could be organized specifically for bootstrapping full | |
clones, but could also be organized with the intention of bootstrapping | |
incremental fetches. The bundle provider must decide on one of several | |
organization schemes to minimize client downloads during incremental | |
fetches, but the Git client can also choose whether to use bundles for | |
either of these operations. | |
* The bundle provider can choose to support full clones, partial clones, | |
or both. The client can detect which bundles are appropriate for the | |
repository's partial clone filter, if any. | |
* The bundle provider can use a single bundle (for clones only), or a | |
list of bundles. When using a list of bundles, the provider can specify | |
whether or not the client needs _all_ of the bundle URIs for a full | |
clone, or if _any_ one of the bundle URIs is sufficient. This allows the | |
bundle provider to use different URIs for different geographies. | |
* The bundle provider can organize the bundles using heuristics, such as | |
creation tokens, to help the client prevent downloading bundles it does | |
not need. When the bundle provider does not provide these heuristics, | |
the client can use optimizations to minimize how much of the data is | |
downloaded. | |
* The bundle provider does not need to be associated with the Git server. | |
The client can choose to use the bundle provider without it being | |
advertised by the Git server. | |
* The client can choose to discover bundle providers that are advertised | |
by the Git server. This could happen during `git clone`, during | |
`git fetch`, both, or neither. The user can choose which combination | |
works best for them. | |
* The client can choose to configure a bundle provider manually at any | |
time. The client can also choose to specify a bundle provider manually | |
as a command-line option to `git clone`. | |
Each repository is different and every Git server has different needs. | |
Hopefully the bundle URI feature is flexible enough to satisfy all needs. | |
If not, then the feature can be extended through its versioning mechanism. | |
Server requirements | |
------------------- | |
To provide a server-side implementation of bundle servers, no other parts | |
of the Git protocol are required. This allows server maintainers to use | |
static content solutions such as CDNs in order to serve the bundle files. | |
At the current scope of the bundle URI feature, all URIs are expected to | |
be HTTP(S) URLs where content is downloaded to a local file using a `GET` | |
request to that URL. The server could include authentication requirements | |
to those requests with the aim of triggering the configured credential | |
helper for secure access. (Future extensions could use "file://" URIs or | |
SSH URIs.) | |
Assuming a `200 OK` response from the server, the content at the URL is | |
inspected. First, Git attempts to parse the file as a bundle file of | |
version 2 or higher. If the file is not a bundle, then the file is parsed | |
as a plain-text file using Git's config parser. The key-value pairs in | |
that config file are expected to describe a list of bundle URIs. If | |
neither of these parse attempts succeed, then Git will report an error to | |
the user that the bundle URI provided erroneous data. | |
Any other data provided by the server is considered erroneous. | |
Bundle Lists | |
------------ | |
The Git server can advertise bundle URIs using a set of `key=value` pairs. | |
A bundle URI can also serve a plain-text file in the Git config format | |
containing these same `key=value` pairs. In both cases, we consider this | |
to be a _bundle list_. The pairs specify information about the bundles | |
that the client can use to make decisions for which bundles to download | |
and which to ignore. | |
A few keys focus on properties of the list itself. | |
bundle.version:: | |
(Required) This value provides a version number for the bundle | |
list. If a future Git change enables a feature that needs the Git | |
client to react to a new key in the bundle list file, then this version | |
will increment. The only current version number is 1, and if any other | |
value is specified then Git will fail to use this file. | |
bundle.mode:: | |
(Required) This value has one of two values: `all` and `any`. When `all` | |
is specified, then the client should expect to need all of the listed | |
bundle URIs that match their repository's requirements. When `any` is | |
specified, then the client should expect that any one of the bundle URIs | |
that match their repository's requirements will suffice. Typically, the | |
`any` option is used to list a number of different bundle servers | |
located in different geographies. | |
bundle.heuristic:: | |
If this string-valued key exists, then the bundle list is designed to | |
work well with incremental `git fetch` commands. The heuristic signals | |
that there are additional keys available for each bundle that help | |
determine which subset of bundles the client should download. The only | |
heuristic currently planned is `creationToken`. | |
The remaining keys include an `<id>` segment which is a server-designated | |
name for each available bundle. The `<id>` must contain only alphanumeric | |
and `-` characters. | |
bundle.<id>.uri:: | |
(Required) This string value is the URI for downloading bundle `<id>`. | |
If the URI begins with a protocol (`http://` or `https://`) then the URI | |
is absolute. Otherwise, the URI is interpreted as relative to the URI | |
used for the bundle list. If the URI begins with `/`, then that relative | |
path is relative to the domain name used for the bundle list. (This use | |
of relative paths is intended to make it easier to distribute a set of | |
bundles across a large number of servers or CDNs with different domain | |
names.) | |
bundle.<id>.filter:: | |
This string value represents an object filter that should also appear in | |
the header of this bundle. The server uses this value to differentiate | |
different kinds of bundles from which the client can choose those that | |
match their object filters. | |
bundle.<id>.creationToken:: | |
This value is a nonnegative 64-bit integer used for sorting the bundles | |
list. This is used to download a subset of bundles during a fetch when | |
`bundle.heuristic=creationToken`. | |
bundle.<id>.location:: | |
This string value advertises a real-world location from where the bundle | |
URI is served. This can be used to present the user with an option for | |
which bundle URI to use or simply as an informative indicator of which | |
bundle URI was selected by Git. This is only valuable when | |
`bundle.mode` is `any`. | |
Here is an example bundle list using the Git config format: | |
[bundle] | |
version = 1 | |
mode = all | |
heuristic = creationToken | |
[bundle "2022-02-09-1644442601-daily"] | |
uri = https://bundles.example.com/git/git/2022-02-09-1644442601-daily.bundle | |
creationToken = 1644442601 | |
[bundle "2022-02-02-1643842562"] | |
uri = https://bundles.example.com/git/git/2022-02-02-1643842562.bundle | |
creationToken = 1643842562 | |
[bundle "2022-02-09-1644442631-daily-blobless"] | |
uri = 2022-02-09-1644442631-daily-blobless.bundle | |
creationToken = 1644442631 | |
filter = blob:none | |
[bundle "2022-02-02-1643842568-blobless"] | |
uri = /git/git/2022-02-02-1643842568-blobless.bundle | |
creationToken = 1643842568 | |
filter = blob:none | |
This example uses `bundle.mode=all` as well as the | |
`bundle.<id>.creationToken` heuristic. It also uses the `bundle.<id>.filter` | |
options to present two parallel sets of bundles: one for full clones and | |
another for blobless partial clones. | |
Suppose that this bundle list was found at the URI | |
`https://bundles.example.com/git/git/` and so the two blobless bundles have | |
the following fully-expanded URIs: | |
* `https://bundles.example.com/git/git/2022-02-09-1644442631-daily-blobless.bundle` | |
* `https://bundles.example.com/git/git/2022-02-02-1643842568-blobless.bundle` | |
Advertising Bundle URIs | |
----------------------- | |
If a user knows a bundle URI for the repository they are cloning, then | |
they can specify that URI manually through a command-line option. However, | |
a Git host may want to advertise bundle URIs during the clone operation, | |
helping users unaware of the feature. | |
The only thing required for this feature is that the server can advertise | |
one or more bundle URIs. This advertisement takes the form of a new | |
protocol v2 capability specifically for discovering bundle URIs. | |
The client could choose an arbitrary bundle URI as an option _or_ select | |
the URI with best performance by some exploratory checks. It is up to the | |
bundle provider to decide if having multiple URIs is preferable to a | |
single URI that is geodistributed through server-side infrastructure. | |
Cloning with Bundle URIs | |
------------------------ | |
The primary need for bundle URIs is to speed up clones. The Git client | |
will interact with bundle URIs according to the following flow: | |
1. The user specifies a bundle URI with the `--bundle-uri` command-line | |
option _or_ the client discovers a bundle list advertised by the | |
Git server. | |
2. If the downloaded data from a bundle URI is a bundle, then the client | |
inspects the bundle headers to check that the prerequisite commit OIDs | |
are present in the client repository. If some are missing, then the | |
client delays unbundling until other bundles have been unbundled, | |
making those OIDs present. When all required OIDs are present, the | |
client unbundles that data using a refspec. The default refspec is | |
`+refs/heads/*:refs/bundles/*`, but this can be configured. These refs | |
are stored so that later `git fetch` negotiations can communicate each | |
bundled ref as a `have`, reducing the size of the fetch over the Git | |
protocol. To allow pruning refs from this ref namespace, Git may | |
introduce a numbered namespace (such as `refs/bundles/<i>/*`) such that | |
stale bundle refs can be deleted. | |
3. If the file is instead a bundle list, then the client inspects the | |
`bundle.mode` to see if the list is of the `all` or `any` form. | |
a. If `bundle.mode=all`, then the client considers all bundle | |
URIs. The list is reduced based on the `bundle.<id>.filter` options | |
matching the client repository's partial clone filter. Then, all | |
bundle URIs are requested. If the `bundle.<id>.creationToken` | |
heuristic is provided, then the bundles are downloaded in decreasing | |
order by the creation token, stopping when a bundle has all required | |
OIDs. The bundles can then be unbundled in increasing creation token | |
order. The client stores the latest creation token as a heuristic | |
for avoiding future downloads if the bundle list does not advertise | |
bundles with larger creation tokens. | |
b. If `bundle.mode=any`, then the client can choose any one of the | |
bundle URIs to inspect. The client can use a variety of ways to | |
choose among these URIs. The client can also fallback to another URI | |
if the initial choice fails to return a result. | |
Note that during a clone we expect that all bundles will be required, and | |
heuristics such as `bundle.<uri>.creationToken` can be used to download | |
bundles in chronological order or in parallel. | |
If a given bundle URI is a bundle list with a `bundle.heuristic` | |
value, then the client can choose to store that URI as its chosen bundle | |
URI. The client can then navigate directly to that URI during later `git | |
fetch` calls. | |
When downloading bundle URIs, the client can choose to inspect the initial | |
content before committing to downloading the entire content. This may | |
provide enough information to determine if the URI is a bundle list or | |
a bundle. In the case of a bundle, the client may inspect the bundle | |
header to determine that all advertised tips are already in the client | |
repository and cancel the remaining download. | |
Fetching with Bundle URIs | |
------------------------- | |
When the client fetches new data, it can decide to fetch from bundle | |
servers before fetching from the origin remote. This could be done via a | |
command-line option, but it is more likely useful to use a config value | |
such as the one specified during the clone. | |
The fetch operation follows the same procedure to download bundles from a | |
bundle list (although we do _not_ want to use parallel downloads here). We | |
expect that the process will end when all prerequisite commit OIDs in a | |
thin bundle are already in the object database. | |
When using the `creationToken` heuristic, the client can avoid downloading | |
any bundles if their creation tokens are not larger than the stored | |
creation token. After fetching new bundles, Git updates this local | |
creation token. | |
If the bundle provider does not provide a heuristic, then the client | |
should attempt to inspect the bundle headers before downloading the full | |
bundle data in case the bundle tips already exist in the client | |
repository. | |
Error Conditions | |
---------------- | |
If the Git client discovers something unexpected while downloading | |
information according to a bundle URI or the bundle list found at that | |
location, then Git can ignore that data and continue as if it was not | |
given a bundle URI. The remote Git server is the ultimate source of truth, | |
not the bundle URI. | |
Here are a few example error conditions: | |
* The client fails to connect with a server at the given URI or a connection | |
is lost without any chance to recover. | |
* The client receives a 400-level response (such as `404 Not Found` or | |
`401 Not Authorized`). The client should use the credential helper to | |
find and provide a credential for the URI, but match the semantics of | |
Git's other HTTP protocols in terms of handling specific 400-level | |
errors. | |
* The server reports any other failure response. | |
* The client receives data that is not parsable as a bundle or bundle list. | |
* A bundle includes a filter that does not match expectations. | |
* The client cannot unbundle the bundles because the prerequisite commit OIDs | |
are not in the object database and there are no more bundles to download. | |
There are also situations that could be seen as wasteful, but are not | |
error conditions: | |
* The downloaded bundles contain more information than is requested by | |
the clone or fetch request. A primary example is if the user requests | |
a clone with `--single-branch` but downloads bundles that store every | |
reachable commit from all `refs/heads/*` references. This might be | |
initially wasteful, but perhaps these objects will become reachable by | |
a later ref update that the client cares about. | |
* A bundle download during a `git fetch` contains objects already in the | |
object database. This is probably unavoidable if we are using bundles | |
for fetches, since the client will almost always be slightly ahead of | |
the bundle servers after performing its "catch-up" fetch to the remote | |
server. This extra work is most wasteful when the client is fetching | |
much more frequently than the server is computing bundles, such as if | |
the client is using hourly prefetches with background maintenance, but | |
the server is computing bundles weekly. For this reason, the client | |
should not use bundle URIs for fetch unless the server has explicitly | |
recommended it through a `bundle.heuristic` value. | |
Example Bundle Provider organization | |
------------------------------------ | |
The bundle URI feature is intentionally designed to be flexible to | |
different ways a bundle provider wants to organize the object data. | |
However, it can be helpful to have a complete organization model described | |
here so providers can start from that base. | |
This example organization is a simplified model of what is used by the | |
GVFS Cache Servers (see section near the end of this document) which have | |
been beneficial in speeding up clones and fetches for very large | |
repositories, although using extra software outside of Git. | |
The bundle provider deploys servers across multiple geographies. Each | |
server manages its own bundle set. The server can track a number of Git | |
repositories, but provides a bundle list for each based on a pattern. For | |
example, when mirroring a repository at `https://<domain>/<org>/<repo>` | |
the bundle server could have its bundle list available at | |
`https://<server-url>/<domain>/<org>/<repo>`. The origin Git server can | |
list all of these servers under the "any" mode: | |
[bundle] | |
version = 1 | |
mode = any | |
[bundle "eastus"] | |
uri = https://eastus.example.com/<domain>/<org>/<repo> | |
[bundle "europe"] | |
uri = https://europe.example.com/<domain>/<org>/<repo> | |
[bundle "apac"] | |
uri = https://apac.example.com/<domain>/<org>/<repo> | |
This "list of lists" is static and only changes if a bundle server is | |
added or removed. | |
Each bundle server manages its own set of bundles. The initial bundle list | |
contains only a single bundle, containing all of the objects received from | |
cloning the repository from the origin server. The list uses the | |
`creationToken` heuristic and a `creationToken` is made for the bundle | |
based on the server's timestamp. | |
The bundle server runs regularly-scheduled updates for the bundle list, | |
such as once a day. During this task, the server fetches the latest | |
contents from the origin server and generates a bundle containing the | |
objects reachable from the latest origin refs, but not contained in a | |
previously-computed bundle. This bundle is added to the list, with care | |
that the `creationToken` is strictly greater than the previous maximum | |
`creationToken`. | |
When the bundle list grows too large, say more than 30 bundles, then the | |
oldest "_N_ minus 30" bundles are combined into a single bundle. This | |
bundle's `creationToken` is equal to the maximum `creationToken` among the | |
merged bundles. | |
An example bundle list is provided here, although it only has two daily | |
bundles and not a full list of 30: | |
[bundle] | |
version = 1 | |
mode = all | |
heuristic = creationToken | |
[bundle "2022-02-13-1644770820-daily"] | |
uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644770820-daily.bundle | |
creationToken = 1644770820 | |
[bundle "2022-02-09-1644442601-daily"] | |
uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644442601-daily.bundle | |
creationToken = 1644442601 | |
[bundle "2022-02-02-1643842562"] | |
uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-02-1643842562.bundle | |
creationToken = 1643842562 | |
To avoid storing and serving object data in perpetuity despite becoming | |
unreachable in the origin server, this bundle merge can be more careful. | |
Instead of taking an absolute union of the old bundles, instead the bundle | |
can be created by looking at the newer bundles and ensuring that their | |
necessary commits are all available in this merged bundle (or in another | |
one of the newer bundles). This allows "expiring" object data that is not | |
being used by new commits in this window of time. That data could be | |
reintroduced by a later push. | |
The intention of this data organization has two main goals. First, initial | |
clones of the repository become faster by downloading precomputed object | |
data from a closer source. Second, `git fetch` commands can be faster, | |
especially if the client has not fetched for a few days. However, if a | |
client does not fetch for 30 days, then the bundle list organization would | |
cause redownloading a large amount of object data. | |
One way to make this organization more useful to users who fetch frequently | |
is to have more frequent bundle creation. For example, bundles could be | |
created every hour, and then once a day those "hourly" bundles could be | |
merged into a "daily" bundle. The daily bundles are merged into the | |
oldest bundle after 30 days. | |
It is recommended that this bundle strategy is repeated with the `blob:none` | |
filter if clients of this repository are expecting to use blobless partial | |
clones. This list of blobless bundles stays in the same list as the full | |
bundles, but uses the `bundle.<id>.filter` key to separate the two groups. | |
For very large repositories, the bundle provider may want to _only_ provide | |
blobless bundles. | |
Implementation Plan | |
------------------- | |
This design document is being submitted on its own as an aspirational | |
document, with the goal of implementing all of the mentioned client | |
features over the course of several patch series. Here is a potential | |
outline for submitting these features: | |
1. Integrate bundle URIs into `git clone` with a `--bundle-uri` option. | |
This will include a new `git fetch --bundle-uri` mode for use as the | |
implementation underneath `git clone`. The initial version here will | |
expect a single bundle at the given URI. | |
2. Implement the ability to parse a bundle list from a bundle URI and | |
update the `git fetch --bundle-uri` logic to properly distinguish | |
between `bundle.mode` options. Specifically design the feature so | |
that the config format parsing feeds a list of key-value pairs into the | |
bundle list logic. | |
3. Create the `bundle-uri` protocol v2 command so Git servers can advertise | |
bundle URIs using the key-value pairs. Plug into the existing key-value | |
input to the bundle list logic. Allow `git clone` to discover these | |
bundle URIs and bootstrap the client repository from the bundle data. | |
(This choice is an opt-in via a config option and a command-line | |
option.) | |
4. Allow the client to understand the `bundle.heuristic` configuration key | |
and the `bundle.<id>.creationToken` heuristic. When `git clone` | |
discovers a bundle URI with `bundle.heuristic`, it configures the client | |
repository to check that bundle URI during later `git fetch <remote>` | |
commands. | |
5. Allow clients to discover bundle URIs during `git fetch` and configure | |
a bundle URI for later fetches if `bundle.heuristic` is set. | |
6. Implement the "inspect headers" heuristic to reduce data downloads when | |
the `bundle.<id>.creationToken` heuristic is not available. | |
As these features are reviewed, this plan might be updated. We also expect | |
that new designs will be discovered and implemented as this feature | |
matures and becomes used in real-world scenarios. | |
Related Work: Packfile URIs | |
--------------------------- | |
The Git protocol already has a capability where the Git server can list | |
a set of URLs along with the packfile response when serving a client | |
request. The client is then expected to download the packfiles at those | |
locations in order to have a complete understanding of the response. | |
This mechanism is used by the Gerrit server (implemented with JGit) and | |
has been effective at reducing CPU load and improving user performance for | |
clones. | |
A major downside to this mechanism is that the origin server needs to know | |
_exactly_ what is in those packfiles, and the packfiles need to be available | |
to the user for some time after the server has responded. This coupling | |
between the origin and the packfile data is difficult to manage. | |
Further, this implementation is extremely hard to make work with fetches. | |
Related Work: GVFS Cache Servers | |
-------------------------------- | |
The GVFS Protocol [2] is a set of HTTP endpoints designed independently of | |
the Git project before Git's partial clone was created. One feature of this | |
protocol is the idea of a "cache server" which can be colocated with build | |
machines or developer offices to transfer Git data without overloading the | |
central server. | |
The endpoint that VFS for Git is famous for is the `GET /gvfs/objects/{oid}` | |
endpoint, which allows downloading an object on-demand. This is a critical | |
piece of the filesystem virtualization of that product. | |
However, a more subtle need is the `GET /gvfs/prefetch?lastPackTimestamp=<t>` | |
endpoint. Given an optional timestamp, the cache server responds with a list | |
of precomputed packfiles containing the commits and trees that were introduced | |
in those time intervals. | |
The cache server computes these "prefetch" packfiles using the following | |
strategy: | |
1. Every hour, an "hourly" pack is generated with a given timestamp. | |
2. Nightly, the previous 24 hourly packs are rolled up into a "daily" pack. | |
3. Nightly, all prefetch packs more than 30 days old are rolled up into | |
one pack. | |
When a user runs `gvfs clone` or `scalar clone` against a repo with cache | |
servers, the client requests all prefetch packfiles, which is at most | |
`24 + 30 + 1` packfiles downloading only commits and trees. The client | |
then follows with a request to the origin server for the references, and | |
attempts to checkout that tip reference. (There is an extra endpoint that | |
helps get all reachable trees from a given commit, in case that commit | |
was not already in a prefetch packfile.) | |
During a `git fetch`, a hook requests the prefetch endpoint using the | |
most-recent timestamp from a previously-downloaded prefetch packfile. | |
Only the list of packfiles with later timestamps are downloaded. Most | |
users fetch hourly, so they get at most one hourly prefetch pack. Users | |
whose machines have been off or otherwise have not fetched in over 30 days | |
might redownload all prefetch packfiles. This is rare. | |
It is important to note that the clients always contact the origin server | |
for the refs advertisement, so the refs are frequently "ahead" of the | |
prefetched pack data. The missing objects are downloaded on-demand using | |
the `GET gvfs/objects/{oid}` requests, when needed by a command such as | |
`git checkout` or `git log`. Some Git optimizations disable checks that | |
would cause these on-demand downloads to be too aggressive. | |
See Also | |
-------- | |
[1] https://lore.kernel.org/git/[email protected]/ | |
An earlier RFC for a bundle URI feature. | |
[2] https://github.com/microsoft/VFSForGit/blob/master/Protocol.md | |
The GVFS Protocol | |